Files
SocialMediaApp/script.js
2026-01-28 21:06:16 +11:00

520 lines
21 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const loginForm = document.querySelector('.login-form');
const loginContainer = document.querySelector('.login-container');
const feedView = document.getElementById('feed-view');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const confirmPasswordInput = document.getElementById('confirm-password');
const confirmPasswordGroup = document.getElementById('confirm-password-group');
const submitBtn = document.querySelector('.btn-primary');
const btnText = submitBtn.querySelector('span');
const helperText = document.querySelector('.signup-link');
const toggleLink = document.querySelector('.signup-link a');
const title = document.querySelector('.brand-section h1');
const subtitle = document.querySelector('.brand-section p');
let isLoginMode = true;
// Toggle between Login and Signup
if (toggleLink) {
toggleLink.addEventListener('click', (e) => {
e.preventDefault();
isLoginMode = !isLoginMode;
updateUI();
});
}
function updateUI() {
if (isLoginMode) {
title.innerText = 'Welcome Back';
subtitle.innerText = 'Enter your details below';
btnText.innerText = 'Sign In';
helperText.innerHTML = 'Don\'t have an account? <a href="#">Create One</a>';
confirmPasswordGroup.classList.add('hidden');
confirmPasswordInput.required = false;
} else {
title.innerText = 'Create Account';
subtitle.innerText = 'Start your journey with us';
btnText.innerText = 'Sign Up';
helperText.innerHTML = 'Already have an account? <a href="#">Sign In</a>';
confirmPasswordGroup.classList.remove('hidden');
confirmPasswordInput.required = true;
}
// Re-attach listener to the new link element
const newLink = document.querySelector('.signup-link a');
newLink.addEventListener('click', (e) => {
e.preventDefault();
isLoginMode = !isLoginMode;
updateUI();
});
// Reset form and styles
loginForm.reset();
resetButton();
}
function resetButton() {
submitBtn.style.background = '';
submitBtn.style.transform = '';
btnText.innerText = isLoginMode ? 'Sign In' : 'Sign Up';
}
function showFeedback(isSuccess, message) {
btnText.innerText = message;
if (isSuccess) {
submitBtn.style.background = 'linear-gradient(135deg, #10B981 0%, #059669 100%)'; // Success Green
} else {
submitBtn.style.background = 'linear-gradient(135deg, #EF4444 0%, #B91C1C 100%)'; // Error Red
submitBtn.animate([
{ transform: 'translateX(0)' },
{ transform: 'translateX(-5px)' },
{ transform: 'translateX(5px)' },
{ transform: 'translateX(0)' }
], { duration: 300 });
}
setTimeout(() => {
if (!message.includes('Success')) {
resetButton();
}
}, 2000);
}
function navigateToFeed() {
if (loginContainer && feedView) {
// Fade out login
loginContainer.style.opacity = '0';
setTimeout(() => {
loginContainer.classList.add('hidden');
loginContainer.style.display = 'none'; // Ensure it's gone
// Show Feed
feedView.classList.remove('hidden');
// Trigger reflow
void feedView.offsetWidth;
feedView.classList.add('fade-in');
// Load persisted state for posts
loadPostState();
// Remove fade-in class after animation to fix fixed positioning context
setTimeout(() => {
feedView.classList.remove('fade-in');
}, 600);
}, 600); // Wait for transition
}
}
if (loginForm) {
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const email = emailInput.value.trim();
const password = passwordInput.value.trim();
const confirmPassword = confirmPasswordInput.value.trim();
const timestamp = new Date().toISOString();
// Get existing users
const existingUsers = JSON.parse(localStorage.getItem('socialAppUsers') || '[]');
if (isLoginMode) {
// --- LOGIN LOGIC ---
const user = existingUsers.find(u => u.email === email && u.password === password);
if (user) {
console.log('Login Successful:', user);
showFeedback(true, 'Success!');
setTimeout(navigateToFeed, 1000);
} else {
console.warn('Login Failed: Invalid credentials');
showFeedback(false, 'Incorrect details');
}
} else {
// --- SIGNUP LOGIC ---
// 1. Check if user already exists
if (existingUsers.some(u => u.email === email)) {
showFeedback(false, 'User exists');
return;
}
// 2. Validate Password Match
if (password !== confirmPassword) {
showFeedback(false, 'Passwords do not match');
return;
}
const newUser = { email, password, joinedAt: timestamp };
existingUsers.push(newUser);
localStorage.setItem('socialAppUsers', JSON.stringify(existingUsers));
console.log('New User Registered:', newUser);
showFeedback(true, 'Account Created!');
// Switch to login mode after success
setTimeout(() => {
isLoginMode = true;
updateUI();
setTimeout(navigateToFeed, 1000);
}, 1500);
}
});
}
// --- PERSISTENCE LOGIC ---
function getPostState() {
return JSON.parse(localStorage.getItem('socialAppPostState') || '{}');
}
function savePostState(state) {
localStorage.setItem('socialAppPostState', JSON.stringify(state));
}
function loadPostState() {
const state = getPostState();
const posts = document.querySelectorAll('.post-card');
posts.forEach(card => {
const id = card.getAttribute('data-post-id');
if (id && state[id]) {
const data = state[id];
// Restore Likes
const likeIcon = card.querySelector('.heart-icon');
const likesText = card.querySelector('.likes');
if (data.liked) {
likeIcon.classList.add('liked');
likeIcon.style.fill = '#EF4444';
likeIcon.style.stroke = '#EF4444';
} else {
likeIcon.classList.remove('liked');
likeIcon.style.fill = 'none';
likeIcon.style.stroke = 'white';
}
likesText.innerText = data.likesCount + ' likes';
// Restore Comments
const commentsContainer = card.querySelector('.added-comments');
commentsContainer.innerHTML = ''; // Clear existing to prevent dupes on re-run
if (data.comments && data.comments.length > 0) {
data.comments.forEach(text => {
const commentEl = document.createElement('div');
commentEl.classList.add('comment-item');
commentEl.innerHTML = `<span class="comment-user">you</span> ${text}`;
commentsContainer.appendChild(commentEl);
});
}
}
});
}
// --- FEED INTERACTIONS ---
if (feedView) {
feedView.addEventListener('click', (e) => {
const target = e.target;
// 1. LIKE ACTION
const likeBtn = target.closest('.heart-icon')?.closest('.icon-btn');
if (likeBtn) {
const icon = likeBtn.querySelector('.heart-icon');
const postCard = likeBtn.closest('.post-card');
const likesText = postCard.querySelector('.likes');
const postId = postCard.getAttribute('data-post-id');
// Toggle State
icon.classList.toggle('liked');
// Update Count (Simple parsing)
let count = parseInt(likesText.innerText);
let isLiked = false;
if (icon.classList.contains('liked')) {
icon.style.fill = '#EF4444';
icon.style.stroke = '#EF4444';
count++;
isLiked = true;
} else {
icon.style.fill = 'none';
icon.style.stroke = 'white';
count--;
isLiked = false;
}
likesText.innerText = count + ' likes';
// Save State
if (postId) {
const state = getPostState();
if (!state[postId]) state[postId] = { comments: [] };
state[postId].liked = isLiked;
state[postId].likesCount = count;
// Preserve existing comments if we didn't just load them
if (!state[postId].comments) state[postId].comments = [];
savePostState(state);
}
return;
}
// 2. COMMENT TOGGLE ACTION
const commentBtn = target.closest('.icon-btn');
if (commentBtn && !commentBtn.querySelector('.heart-icon')) {
// Check if it's the comment button (has specific path)
const path = commentBtn.querySelector('path');
if (path && (path.getAttribute('d').startsWith('M21 11.5') || path.closest('svg')?.innerHTML.includes('M21 11.5'))) {
const postCard = commentBtn.closest('.post-card');
const commentSection = postCard.querySelector('.comment-section');
commentSection.classList.toggle('active');
if (commentSection.classList.contains('active')) {
commentSection.querySelector('input').focus();
}
return;
}
}
// 3. POST COMMENT ACTION
if (target.classList.contains('post-btn')) {
const wrapper = target.closest('.comment-input-wrapper');
const input = wrapper.querySelector('.comment-input');
const text = input.value.trim();
const postCard = wrapper.closest('.post-card');
const postId = postCard.getAttribute('data-post-id');
if (text) {
addComment(wrapper.closest('.comment-section'), text);
if (postId) {
const state = getPostState();
if (!state[postId]) state[postId] = { liked: false, likesCount: parseInt(postCard.querySelector('.likes').innerText) || 0, comments: [] };
if (!state[postId].comments) state[postId].comments = [];
state[postId].comments.push(text);
savePostState(state);
}
input.value = '';
}
}
});
// Enter key for comments
feedView.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.target.classList.contains('comment-input')) {
const text = e.target.value.trim();
const wrapper = e.target.closest('.comment-input-wrapper');
const postCard = wrapper.closest('.post-card');
const postId = postCard.getAttribute('data-post-id');
if (text) {
addComment(e.target.closest('.comment-section'), text);
if (postId) {
const state = getPostState();
if (!state[postId]) state[postId] = { liked: false, likesCount: parseInt(postCard.querySelector('.likes').innerText) || 0, comments: [] };
if (!state[postId].comments) state[postId].comments = [];
state[postId].comments.push(text);
savePostState(state);
}
e.target.value = '';
}
}
});
}
function addComment(section, text) {
const container = section.querySelector('.added-comments');
const commentEl = document.createElement('div');
commentEl.classList.add('comment-item');
commentEl.innerHTML = `<span class="comment-user">you</span> ${text}`;
container.appendChild(commentEl);
}
// --- CREATE POST LOGIC ---
const createPostView = document.getElementById('create-post-view');
const cancelPostBtn = document.getElementById('cancel-post-btn');
const sharePostBtn = document.getElementById('share-post-btn');
const uploadTrigger = document.getElementById('upload-trigger');
const imageInput = document.getElementById('post-image-input');
const imagePreview = document.getElementById('image-preview');
const captionInput = document.getElementById('post-caption-input');
const navPostBtn = document.querySelector('.nav-btn:nth-child(2)'); // Center Post Button
const navHomeBtn = document.querySelector('.nav-btn:first-child');
// 1. Navigation
if (navPostBtn) {
navPostBtn.addEventListener('click', () => {
feedView.classList.add('hidden');
createPostView.classList.remove('hidden');
createPostView.classList.add('fade-in');
});
}
if (cancelPostBtn) {
cancelPostBtn.addEventListener('click', () => {
resetPostForm();
createPostView.classList.add('hidden');
feedView.classList.remove('hidden');
});
}
if (navHomeBtn && feedView.classList.contains('hidden') && !createPostView.classList.contains('hidden')) {
// Logic handled by cancel mostly, but good to have explicit nav
navHomeBtn.addEventListener('click', () => {
if (!createPostView.classList.contains('hidden')) {
resetPostForm();
createPostView.classList.add('hidden');
feedView.classList.remove('hidden');
}
});
}
// 2. Image Selection
if (uploadTrigger) {
uploadTrigger.addEventListener('click', () => imageInput.click());
}
if (imageInput) {
imageInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
imagePreview.classList.remove('hidden');
document.querySelector('.upload-placeholder').classList.add('hidden');
};
reader.readAsDataURL(file);
}
});
}
// 3. Share Post
if (sharePostBtn) {
sharePostBtn.addEventListener('click', () => {
if (!imagePreview.src || imagePreview.classList.contains('hidden')) {
alert('Please select an image first.');
return;
}
const caption = captionInput.value.trim();
const imageSrc = imagePreview.src;
const id = 'post_' + Date.now();
const newPost = {
id,
imageSrc,
caption,
timestamp: new Date().toISOString(),
username: 'you', // Mock user
likes: 0
};
// Save to LocalStorage
const userPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]');
userPosts.unshift(newPost); // Add to top
localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts));
// Render to Feed
renderPost(newPost, true); // true = prepend
// Reset and Go Back
resetPostForm();
createPostView.classList.add('hidden');
feedView.classList.remove('hidden');
// Re-bind listeners isn't needed strictly due to delegation, but state refresh might be
loadPostState();
});
}
function resetPostForm() {
imageInput.value = '';
captionInput.value = '';
imagePreview.src = '';
imagePreview.classList.add('hidden');
document.querySelector('.upload-placeholder').classList.remove('hidden');
}
function renderPost(post, prepend = false) {
const feedContainer = document.querySelector('.feed-container');
const article = document.createElement('article');
article.className = 'post-card';
article.setAttribute('data-post-id', post.id);
article.innerHTML = `
<div class="post-header">
<div class="post-user">
<img src="https://i.pravatar.cc/150?img=12" alt="User" class="avatar-sm">
<span class="username">${post.username}</span>
</div>
<button class="icon-btn-sm">...</button>
</div>
<div class="post-image">
<img src="${post.imageSrc}" alt="Post">
</div>
<div class="post-actions">
<div class="action-left">
<button class="icon-btn"><svg viewBox="0 0 24 24" width="24" height="24" stroke="white" fill="none" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg></button>
<button class="icon-btn"><svg viewBox="0 0 24 24" width="24" height="24" stroke="white" fill="none"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg></button>
</div>
<button class="icon-btn"><svg viewBox="0 0 24 24" width="24" height="24" stroke="white" fill="none"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg></button>
</div>
<div class="post-content">
<span class="likes">0 likes</span>
<p><span class="username">${post.username}</span> ${post.caption}</p>
</div>
<div class="comment-section">
<div class="added-comments"></div>
<div class="comment-input-wrapper">
<input type="text" class="comment-input" placeholder="Add a comment...">
<button class="post-btn">Post</button>
</div>
</div>
`;
// This is a bit tricky with existing static posts.
// Ideally we keep static posts static and just inject dynamic ones.
if (prepend) {
feedContainer.insertBefore(article, feedContainer.firstChild);
// Note: index.html has static articles directly. `firstChild` might be text node or first article.
// A safer insert would be after the first spacer if I had one, or simply `prepend`.
// Let's use `prepend` on the container, but strict ordering might be weird with existing static HTML.
// Actually, `prepend` puts it before the first child, which IS the first post card. Perfect.
} else {
feedContainer.appendChild(article);
}
}
// Load User Posts on Init
const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]');
savedUserPosts.forEach(post => {
// Check if already in DOM to avoid dupes (simple check)
if (!document.querySelector(`[data-post-id="${post.id}"]`)) {
renderPost(post, true); // Keep order inverted?
// If we loop saved posts [newest, ..., oldest], we should Append?
// Actually saved as [newest, oldest].
// If we prepend one by one:
// 1. Newest -> Prepend -> Top.
// 2. Oldest -> Prepend -> Top (Wrong).
// So we should iterate in reverse if we prepend, OR just append at top once properly sorted.
// Let's keep it simple: Iterate in reverse order [oldest ... newest] and prepend each, so newest ends up on top.
}
});
// Fix order: Reverse iteration for prepend logic
// savedUserPosts.reverse().forEach(...) -> Wait, `unshift` adds to start. So [0] is newest.
// Loop:
// Post 0 (Newest) -> Prepend (become first).
// Post 1 (Older) -> Prepend (become first).
// Result: Post 1 is above Post 0. WRONG.
// Correct: Iterate from last to first (reverse) and prepend.
// Post Last (Oldest) -> Prepend.
// ...
// Post 0 (Newest) -> Prepend (Top). CORRECT.
// So:
[...savedUserPosts].reverse().forEach(post => {
if (!document.querySelector(`[data-post-id="${post.id}"]`)) {
renderPost(post, true);
}
});
});