946 lines
40 KiB
JavaScript
946 lines
40 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- API CONFIG ---
|
|
const API = {
|
|
async getUsers() {
|
|
const res = await fetch('/api/users');
|
|
if (!res.ok) throw new Error(`Server Error: ${res.status}`);
|
|
return await res.json();
|
|
},
|
|
async addUser(user) {
|
|
await fetch('/api/users/add', { method: 'POST', body: JSON.stringify(user) });
|
|
},
|
|
async getPosts() {
|
|
const res = await fetch('/api/posts');
|
|
if (!res.ok) throw new Error(`Server Error: ${res.status}`);
|
|
return await res.json();
|
|
},
|
|
async addPost(post) {
|
|
await fetch('/api/posts/add', { method: 'POST', body: JSON.stringify(post) });
|
|
},
|
|
async updatePost(post) {
|
|
await fetch('/api/posts/update', { method: 'POST', body: JSON.stringify(post) });
|
|
},
|
|
async deletePost(id) {
|
|
await fetch('/api/posts/delete', { method: 'POST', body: JSON.stringify({ id }) });
|
|
}
|
|
};
|
|
|
|
// --- INDEXED DB HELPERS (Multi-Media Support) ---
|
|
const DB_NAME = 'SocialAppDB';
|
|
const DB_VERSION = 1;
|
|
|
|
function openDB() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onupgradeneeded = (e) => {
|
|
const db = e.target.result;
|
|
if (!db.objectStoreNames.contains('images')) {
|
|
db.createObjectStore('images', { keyPath: 'id' });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
// Save Array of Blobs/Strings
|
|
async function saveMediaToDB(id, mediaArray) {
|
|
try {
|
|
const db = await openDB();
|
|
return new Promise((resolve, reject) => {
|
|
const tx = db.transaction('images', 'readwrite');
|
|
const store = tx.objectStore('images');
|
|
store.put({ id, media: mediaArray });
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error);
|
|
});
|
|
} catch (e) {
|
|
console.error('IndexedDB Save Error:', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Get Media Array
|
|
async function getMediaFromDB(id) {
|
|
try {
|
|
const db = await openDB();
|
|
return new Promise((resolve, reject) => {
|
|
const tx = db.transaction('images', 'readonly');
|
|
const store = tx.objectStore('images');
|
|
const request = store.get(id);
|
|
request.onsuccess = () => resolve(request.result ? request.result.media : null);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
} catch (e) {
|
|
console.error('IndexedDB Read Error:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function deleteImageFromDB(id) {
|
|
try {
|
|
const db = await openDB();
|
|
return new Promise((resolve, reject) => {
|
|
const tx = db.transaction('images', 'readwrite');
|
|
const store = tx.objectStore('images');
|
|
store.delete(id);
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error);
|
|
});
|
|
} catch (e) {
|
|
console.log('IndexedDB Delete Error or ID not found:', e);
|
|
}
|
|
}
|
|
|
|
// --- QUERY SELECTORS ---
|
|
const loginForm = document.querySelector('.login-form');
|
|
const loginView = document.getElementById('login-view');
|
|
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 ? submitBtn.querySelector('span') : null;
|
|
const toggleLink = document.querySelector('.signup-link a');
|
|
const title = document.querySelector('.brand-section h1');
|
|
const subtitle = document.querySelector('.brand-section p');
|
|
|
|
// Onboarding Elements
|
|
const verificationView = document.getElementById('verification-view');
|
|
const onboardingView = document.getElementById('onboarding-view');
|
|
const otpInput = document.getElementById('otp-input');
|
|
const verifyBtn = document.getElementById('verify-btn');
|
|
const displayNameInput = document.getElementById('display-name');
|
|
const bioInput = document.getElementById('bio-input');
|
|
const avatarInput = document.getElementById('avatar-input');
|
|
const avatarPreview = document.getElementById('avatar-preview');
|
|
const avatarTrigger = document.getElementById('avatar-upload-trigger');
|
|
const completeSetupBtn = document.getElementById('complete-setup-btn');
|
|
|
|
// Post Creation Elements
|
|
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 mediaPreviewContainer = document.getElementById('media-preview-container');
|
|
const captionInput = document.getElementById('post-caption-input');
|
|
const navPostBtn = document.querySelector('.nav-btn:nth-child(2)');
|
|
const navHomeBtn = document.querySelector('.nav-btn:first-child');
|
|
|
|
// Lightbox Elements
|
|
const lightboxView = document.getElementById('lightbox-view');
|
|
const lightboxClose = document.getElementById('lightbox-close');
|
|
const lightboxCarousel = lightboxView?.querySelector('.lightbox-carousel');
|
|
|
|
// State
|
|
let isLoginMode = true;
|
|
let pendingUser = null;
|
|
let generatedOTP = null;
|
|
let newAvatarBase64 = null;
|
|
|
|
// --- CHECK SESSION & LOGOUT ---
|
|
async function checkSession() {
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
if (currentUser.email) {
|
|
try {
|
|
// Verify user actually exists in DB (fixes "Ghost User" after DB wipe)
|
|
const users = await API.getUsers();
|
|
const validUser = users.find(u => u.email === currentUser.email);
|
|
|
|
if (!validUser) {
|
|
console.log('Session invalid: User not found on server.');
|
|
localStorage.removeItem('currentUser');
|
|
if (loginView && !loginView.classList.contains('hidden')) return; // Already on login
|
|
location.reload();
|
|
return;
|
|
}
|
|
|
|
if (loginView) {
|
|
loginView.classList.add('hidden');
|
|
loginView.style.display = 'none';
|
|
}
|
|
if (feedView) {
|
|
feedView.classList.remove('hidden');
|
|
initFeed();
|
|
}
|
|
} catch (err) {
|
|
console.warn('Session check failed:', err);
|
|
alert('Connection to Server Failed.\n\nReason: ' + err.message + '\n\nLogging out for security.');
|
|
localStorage.removeItem('currentUser');
|
|
location.reload();
|
|
}
|
|
}
|
|
}
|
|
checkSession();
|
|
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', () => {
|
|
if (confirm('Log out?')) {
|
|
localStorage.removeItem('currentUser');
|
|
location.reload();
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- ONBOARDING LOGIC ---
|
|
if (avatarTrigger && avatarInput) {
|
|
avatarTrigger.addEventListener('click', () => avatarInput.click());
|
|
avatarInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
newAvatarBase64 = e.target.result;
|
|
avatarPreview.src = newAvatarBase64;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Verify OTP Handler
|
|
if (verifyBtn) {
|
|
verifyBtn.addEventListener('click', () => {
|
|
if (otpInput.value === generatedOTP) {
|
|
verificationView.classList.add('hidden');
|
|
onboardingView.classList.remove('hidden');
|
|
onboardingView.classList.add('fade-in');
|
|
} else {
|
|
alert('Invalid Code');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Complete Setup Handler
|
|
if (completeSetupBtn) {
|
|
completeSetupBtn.addEventListener('click', async () => {
|
|
try {
|
|
// Safety Check: If pendingUser is lost (e.g. reload), force restart
|
|
if (!pendingUser) {
|
|
alert('Session expired. Please sign up again.');
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
|
|
const name = displayNameInput.value.trim();
|
|
if (!name) { alert('Name is required'); return; }
|
|
|
|
const bio = bioInput.value.trim();
|
|
|
|
// Finalize User
|
|
const finalUser = {
|
|
...pendingUser,
|
|
username: name,
|
|
bio: bio,
|
|
avatar: newAvatarBase64 || 'https://i.pravatar.cc/150?img=12'
|
|
};
|
|
|
|
// --- ASYNC SAVE ---
|
|
await API.addUser(finalUser);
|
|
localStorage.setItem('currentUser', JSON.stringify(finalUser)); // Session persistence
|
|
|
|
// Transition
|
|
onboardingView.classList.add('hidden');
|
|
feedView.classList.remove('hidden');
|
|
feedView.classList.add('fade-in');
|
|
initFeed();
|
|
} catch (err) {
|
|
console.error('Setup Error:', err);
|
|
alert('Connection Error: ' + err.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- LIGHTBOX LOGIC ---
|
|
function openLightbox(media, startIndex) {
|
|
if (!lightboxView || !lightboxCarousel) return;
|
|
|
|
lightboxCarousel.innerHTML = '';
|
|
media.forEach((item) => {
|
|
const slide = document.createElement('div');
|
|
slide.className = 'lightbox-slide';
|
|
if (item.type === 'video') {
|
|
slide.innerHTML = `<video src="${item.src}" controls playsinline></video>`;
|
|
} else {
|
|
slide.innerHTML = `<img src="${item.src}">`;
|
|
}
|
|
lightboxCarousel.appendChild(slide);
|
|
});
|
|
|
|
lightboxView.classList.remove('hidden');
|
|
lightboxView.classList.add('fade-in');
|
|
|
|
// Scroll to index
|
|
const width = window.innerWidth;
|
|
setTimeout(() => {
|
|
lightboxCarousel.scrollLeft = width * startIndex;
|
|
}, 10);
|
|
|
|
const lightboxCounter = lightboxView.querySelector('.media-counter');
|
|
if (lightboxCounter) {
|
|
lightboxCounter.innerText = `${startIndex + 1}/${media.length}`;
|
|
lightboxCounter.style.display = media.length > 1 ? 'block' : 'none';
|
|
}
|
|
|
|
// Attach Interactions
|
|
enableDragScroll(lightboxCarousel);
|
|
lightboxCarousel.onscroll = () => {
|
|
const index = Math.round(lightboxCarousel.scrollLeft / window.innerWidth);
|
|
if (lightboxCounter) lightboxCounter.innerText = `${index + 1}/${media.length}`;
|
|
};
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
if (lightboxClose) {
|
|
lightboxClose.addEventListener('click', () => {
|
|
if (lightboxView) lightboxView.classList.add('hidden');
|
|
if (lightboxCarousel) lightboxCarousel.innerHTML = '';
|
|
document.body.style.overflow = '';
|
|
});
|
|
}
|
|
|
|
// --- AUTH LOGIC ---
|
|
if (toggleLink) {
|
|
toggleLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
isLoginMode = !isLoginMode;
|
|
updateUI();
|
|
});
|
|
}
|
|
|
|
function updateUI() {
|
|
if (!title || !subtitle || !btnText || !confirmPasswordGroup) return;
|
|
if (isLoginMode) {
|
|
title.innerText = 'Welcome Back!';
|
|
subtitle.innerText = 'Enter your details below';
|
|
btnText.innerText = 'Sign In';
|
|
const helper = document.querySelector('.signup-link');
|
|
if (helper) helper.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';
|
|
const helper = document.querySelector('.signup-link');
|
|
if (helper) helper.innerHTML = 'Already have an account? <a href="#">Sign In</a>';
|
|
confirmPasswordGroup.classList.remove('hidden');
|
|
confirmPasswordInput.required = true;
|
|
}
|
|
const newLink = document.querySelector('.signup-link a');
|
|
if (newLink) {
|
|
newLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
isLoginMode = !isLoginMode;
|
|
updateUI();
|
|
});
|
|
}
|
|
if (loginForm) loginForm.reset();
|
|
resetButton();
|
|
}
|
|
|
|
function resetButton() {
|
|
if (!submitBtn || !btnText) return;
|
|
submitBtn.style.background = '';
|
|
submitBtn.style.transform = '';
|
|
btnText.innerText = isLoginMode ? 'Sign In' : 'Sign Up';
|
|
}
|
|
|
|
function showFeedback(isSuccess, message) {
|
|
if (!btnText || !submitBtn) return;
|
|
btnText.innerText = message;
|
|
if (isSuccess) {
|
|
submitBtn.style.background = 'linear-gradient(135deg, #10B981 0%, #059669 100%)';
|
|
} else {
|
|
submitBtn.style.background = 'linear-gradient(135deg, #EF4444 0%, #B91C1C 100%)';
|
|
}
|
|
setTimeout(() => { if (!message.includes('Success')) resetButton(); }, 2000);
|
|
}
|
|
|
|
function navigateToFeed() {
|
|
if (loginView && feedView) {
|
|
loginView.style.opacity = '0';
|
|
setTimeout(() => {
|
|
loginView.classList.add('hidden');
|
|
loginView.style.display = 'none';
|
|
feedView.classList.remove('hidden');
|
|
void feedView.offsetWidth;
|
|
feedView.classList.add('fade-in');
|
|
initFeed();
|
|
setTimeout(() => feedView.classList.remove('fade-in'), 600);
|
|
}, 600);
|
|
}
|
|
}
|
|
|
|
if (loginForm) {
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const email = emailInput.value.trim().toLowerCase();
|
|
const password = passwordInput.value.trim();
|
|
const confirmPassword = confirmPasswordInput.value.trim();
|
|
const timestamp = new Date().toISOString();
|
|
|
|
if (!email || !password) { showFeedback(false, 'Missing fields'); return; }
|
|
|
|
try {
|
|
const users = await API.getUsers();
|
|
|
|
if (isLoginMode) {
|
|
const user = users.find(u => u.email === email && u.password === password);
|
|
if (user) {
|
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
|
showFeedback(true, 'Success!');
|
|
setTimeout(navigateToFeed, 1000);
|
|
} else {
|
|
showFeedback(false, 'Incorrect details');
|
|
}
|
|
} else {
|
|
if (users.find(u => u.email === email)) {
|
|
showFeedback(false, 'User exists');
|
|
return;
|
|
}
|
|
if (password !== confirmPassword) {
|
|
showFeedback(false, 'Passwords mismatch');
|
|
return;
|
|
}
|
|
|
|
// Start Verification Flow
|
|
pendingUser = { email, password, joinedAt: timestamp };
|
|
generatedOTP = Math.floor(100000 + Math.random() * 900000).toString();
|
|
alert(`[SERVER SYNC] Verification code: ${generatedOTP}`);
|
|
|
|
loginView.classList.add('hidden');
|
|
verificationView.classList.remove('hidden');
|
|
verificationView.classList.add('fade-in');
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Login Failed: ' + err.message);
|
|
showFeedback(false, 'Server Error');
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- DATA HELPERS ---
|
|
function getPostState() { return JSON.parse(localStorage.getItem('socialAppPostState') || '{}'); }
|
|
function savePostState(state) { localStorage.setItem('socialAppPostState', JSON.stringify(state)); }
|
|
function isPostDeleted(id) { const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]'); return deletedPosts.includes(id); }
|
|
|
|
// --- ASYNC LOAD FEED ---
|
|
// --- ASYNC LOAD FEED ---
|
|
async function initFeed() {
|
|
const container = document.querySelector('.posts-container');
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
|
|
try {
|
|
const posts = await API.getPosts();
|
|
if (posts.length === 0) {
|
|
container.innerHTML = '<div style="text-align:center; padding:40px; color:#666;">No posts yet. Be the first!</div>';
|
|
return;
|
|
}
|
|
|
|
for (const post of posts) {
|
|
renderPost(post, false);
|
|
}
|
|
} catch (err) {
|
|
console.error('Feed Error:', err);
|
|
container.innerHTML = '<div style="text-align:center; color:red;">Connection Failed</div>';
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// --- RENDER POST ---
|
|
function renderPost(post, prepend = false) {
|
|
if (document.querySelector(`[data-post-id="${post.id}"]`)) return;
|
|
|
|
const container = document.querySelector('.feed-container'); // Renamed variable
|
|
const article = document.createElement('article');
|
|
article.className = 'post-card';
|
|
article.setAttribute('data-post-id', post.id);
|
|
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
const currentEmail = (currentUser.email || '').trim().toLowerCase();
|
|
const postEmail = (post.userEmail || '').trim().toLowerCase();
|
|
|
|
// STRICT MODE: Email Only (as requested by User)
|
|
// This disables deleting legacy posts without email, but ensures security.
|
|
const isOwner = (postEmail && postEmail === currentEmail);
|
|
|
|
// Always show the trigger, but vary the menu content
|
|
const optionsHTML = `
|
|
<button class="icon-btn-sm options-trigger">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>
|
|
</button>
|
|
<div class="options-menu">
|
|
${isOwner ? `
|
|
<button class="menu-btn delete">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
|
Delete Post
|
|
</button>
|
|
` : `
|
|
<button class="menu-btn" style="opacity: 0.5; cursor: default;">Post Options</button>
|
|
`}
|
|
</div>
|
|
`;
|
|
|
|
let slidesHTML = '';
|
|
let dotsHTML = '';
|
|
const media = post.media || (post.imageSrc ? [{ type: 'image', src: post.imageSrc }] : []);
|
|
|
|
if (media.length > 0) {
|
|
media.forEach((item, index) => {
|
|
if (item.type === 'video') {
|
|
slidesHTML += `
|
|
<div class="media-slide" data-index="${index}">
|
|
<video src="${item.src}" controls playsinline loop></video>
|
|
</div>`;
|
|
} else {
|
|
slidesHTML += `
|
|
<div class="media-slide" data-index="${index}">
|
|
<img src="${item.src}" alt="Post Media ${index + 1}">
|
|
</div>`;
|
|
}
|
|
if (media.length > 1) {
|
|
dotsHTML += `<div class="dot ${index === 0 ? 'active' : ''}"></div>`;
|
|
}
|
|
});
|
|
}
|
|
|
|
const paginationHTML = media.length > 1 ? `<div class="carousel-dots">${dotsHTML}</div>` : '';
|
|
const counterHTML = media.length > 1 ? `<div class="media-counter">1/${media.length}</div>` : '';
|
|
|
|
article.innerHTML = `
|
|
<div class="post-header">
|
|
<div class="post-user">
|
|
<img src="${post.userAvatar || 'https://i.pravatar.cc/150?img=12'}" alt="User" class="avatar-sm">
|
|
<span class="username">${post.username}</span>
|
|
</div>
|
|
${optionsHTML}
|
|
</div>
|
|
<div class="post-image-wrapper post-media-wrapper">
|
|
<div class="media-carousel" onscroll="updateCarouselState(this)">
|
|
${slidesHTML}
|
|
</div>
|
|
${counterHTML}
|
|
${paginationHTML}
|
|
</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>
|
|
`;
|
|
|
|
if (prepend) {
|
|
if (!container.querySelector(`[data-post-id="${post.id}"]`)) {
|
|
container.insertBefore(article, container.firstChild);
|
|
}
|
|
}
|
|
else container.appendChild(article);
|
|
|
|
// Attach Drag-to-Scroll & Lightbox Click
|
|
const carousel = article.querySelector('.media-carousel');
|
|
if (carousel) {
|
|
if (media.length > 1) enableDragScroll(carousel);
|
|
|
|
// Open Lightbox on Click
|
|
carousel.addEventListener('click', (e) => {
|
|
const slide = e.target.closest('.media-slide');
|
|
if (slide && !e.target.tagName.match(/VIDEO|BUTTON/)) {
|
|
const index = parseInt(slide.getAttribute('data-index') || '0');
|
|
openLightbox(media, index);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- DRAG TO SCROLL HELPER ---
|
|
function enableDragScroll(container) {
|
|
let isDown = false;
|
|
let startX;
|
|
let scrollLeft;
|
|
|
|
container.addEventListener('mousedown', (e) => {
|
|
isDown = true;
|
|
container.style.cursor = 'grabbing';
|
|
startX = e.pageX - container.offsetLeft;
|
|
scrollLeft = container.scrollLeft;
|
|
});
|
|
container.addEventListener('mouseleave', () => { isDown = false; container.style.cursor = 'grab'; });
|
|
container.addEventListener('mouseup', () => { isDown = false; container.style.cursor = 'grab'; });
|
|
container.addEventListener('mousemove', (e) => {
|
|
if (!isDown) return;
|
|
e.preventDefault();
|
|
const x = e.pageX - container.offsetLeft;
|
|
const walk = (x - startX) * 2; // Scroll-fast
|
|
container.scrollLeft = scrollLeft - walk;
|
|
});
|
|
}
|
|
|
|
// Global helper for dots and counter
|
|
window.updateCarouselState = function (carousel) {
|
|
const index = Math.round(carousel.scrollLeft / carousel.offsetWidth);
|
|
|
|
// Update Dots
|
|
const dots = carousel.parentElement.querySelectorAll('.dot');
|
|
dots.forEach((dot, i) => {
|
|
if (i === index) dot.classList.add('active');
|
|
else dot.classList.remove('active');
|
|
});
|
|
|
|
// Update Counter
|
|
const counter = carousel.parentElement.querySelector('.media-counter');
|
|
if (counter) {
|
|
counter.innerText = `${index + 1}/${dots.length}`;
|
|
}
|
|
}
|
|
|
|
// --- FEED LISTENERS ---
|
|
if (feedView) {
|
|
// initFeed(); // REMOVED: Prevent premature rendering. Only render on login/checkSession.
|
|
feedView.addEventListener('click', (e) => {
|
|
const target = e.target;
|
|
const trigger = target.closest('.options-trigger');
|
|
if (trigger) {
|
|
const header = trigger.closest('.post-header');
|
|
const menu = header.querySelector('.options-menu');
|
|
if (menu) {
|
|
document.querySelectorAll('.options-menu.active').forEach(m => m.classList.remove('active'));
|
|
menu.classList.toggle('active');
|
|
e.stopPropagation();
|
|
}
|
|
return;
|
|
}
|
|
const deleteBtn = target.closest('.delete');
|
|
if (deleteBtn) {
|
|
const card = deleteBtn.closest('.post-card');
|
|
const id = card.getAttribute('data-post-id');
|
|
if (id && id.startsWith('post_')) {
|
|
const userPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]');
|
|
const postToDelete = userPosts.find(p => p.id === id);
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
|
|
// STRICT MODE: Email Only
|
|
const currentEmail = (currentUser.email || '').trim().toLowerCase();
|
|
const postEmail = (postToDelete.userEmail || '').trim().toLowerCase();
|
|
const isOwner = (postEmail && postEmail === currentEmail);
|
|
|
|
if (!postToDelete || !isOwner) {
|
|
console.log('Delete blocked. Owner Check Failed.');
|
|
alert('You can only delete your own posts.');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Delete this post?')) {
|
|
card.style.transition = 'opacity 0.3s, transform 0.3s';
|
|
card.style.opacity = '0';
|
|
card.style.transform = 'scale(0.9)';
|
|
setTimeout(() => card.remove(), 300);
|
|
|
|
const updatedPosts = userPosts.filter(p => p.id !== id);
|
|
localStorage.setItem('socialAppUserPosts', JSON.stringify(updatedPosts));
|
|
|
|
deleteImageFromDB(id);
|
|
|
|
const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]');
|
|
if (!deletedPosts.includes(id)) {
|
|
deletedPosts.push(id);
|
|
localStorage.setItem('socialAppDeletedPosts', JSON.stringify(deletedPosts));
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
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');
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
const userEmail = (currentUser.email || '').toLowerCase();
|
|
|
|
if (!userEmail) { alert('Please login to like'); return; }
|
|
|
|
icon.classList.toggle('liked');
|
|
let isLiked = icon.classList.contains('liked');
|
|
|
|
if (isLiked) {
|
|
icon.style.fill = '#EF4444';
|
|
icon.style.stroke = '#EF4444';
|
|
} else {
|
|
icon.style.fill = 'none';
|
|
icon.style.stroke = 'white';
|
|
}
|
|
|
|
if (postId) {
|
|
const state = getPostState();
|
|
if (!state[postId]) state[postId] = { comments: [], likedBy: [], likesCount: 0 };
|
|
if (!state[postId].likedBy) state[postId].likedBy = [];
|
|
|
|
if (isLiked) {
|
|
if (!state[postId].likedBy.includes(userEmail)) {
|
|
state[postId].likedBy.push(userEmail);
|
|
}
|
|
} else {
|
|
state[postId].likedBy = state[postId].likedBy.filter(e => e !== userEmail);
|
|
}
|
|
|
|
const count = state[postId].likedBy.length;
|
|
state[postId].likesCount = count;
|
|
likesText.innerText = count + ' likes';
|
|
savePostState(state);
|
|
}
|
|
return;
|
|
}
|
|
const commentBtn = target.closest('.icon-btn');
|
|
if (commentBtn) {
|
|
const svgs = commentBtn.innerHTML;
|
|
if (svgs.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')) {
|
|
const input = commentSection.querySelector('input');
|
|
if (input) input.focus();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
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) {
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
const username = currentUser.username || 'User';
|
|
const email = (currentUser.email || '').toLowerCase();
|
|
|
|
addComment(wrapper.closest('.comment-section'), text, username); // Visually add immediately
|
|
|
|
if (postId) {
|
|
const state = getPostState();
|
|
// Initialize if missing
|
|
if (!state[postId]) state[postId] = { likedBy: [], likesCount: parseInt(postCard.querySelector('.likes').innerText) || 0, comments: [] };
|
|
if (!state[postId].comments) state[postId].comments = [];
|
|
|
|
// Push Object
|
|
state[postId].comments.push({
|
|
text: text,
|
|
username: username,
|
|
email: email,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
savePostState(state);
|
|
}
|
|
input.value = '';
|
|
}
|
|
}
|
|
});
|
|
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) {
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
const username = currentUser.username || 'User';
|
|
const email = (currentUser.email || '').toLowerCase();
|
|
|
|
addComment(e.target.closest('.comment-section'), text, username);
|
|
|
|
if (postId) {
|
|
const state = getPostState();
|
|
if (!state[postId]) state[postId] = { likedBy: [], likesCount: parseInt(postCard.querySelector('.likes').innerText) || 0, comments: [] };
|
|
if (!state[postId].comments) state[postId].comments = [];
|
|
|
|
state[postId].comments.push({
|
|
text: text,
|
|
username: username,
|
|
email: email,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
savePostState(state);
|
|
}
|
|
e.target.value = '';
|
|
}
|
|
}
|
|
});
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.options-menu') && !e.target.closest('.options-trigger')) {
|
|
document.querySelectorAll('.options-menu.active').forEach(m => m.classList.remove('active'));
|
|
}
|
|
});
|
|
}
|
|
|
|
function addComment(section, text, authorName = 'you') {
|
|
const container = section.querySelector('.added-comments');
|
|
const commentEl = document.createElement('div');
|
|
commentEl.classList.add('comment-item');
|
|
commentEl.innerHTML = `<span class="comment-user">${authorName}</span> ${text}`;
|
|
container.appendChild(commentEl);
|
|
}
|
|
|
|
// --- CREATE POST LOGIC ---
|
|
if (navPostBtn) {
|
|
navPostBtn.addEventListener('click', () => { if (feedView && createPostView) { 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) {
|
|
navHomeBtn.addEventListener('click', () => { if (createPostView && !createPostView.classList.contains('hidden')) { resetPostForm(); createPostView.classList.add('hidden'); feedView.classList.remove('hidden'); } });
|
|
}
|
|
|
|
// Staging Area for Selected Files
|
|
let stagedMedia = [];
|
|
|
|
// Upload Handler (Multiple Files)
|
|
if (uploadTrigger && imageInput) {
|
|
uploadTrigger.addEventListener('click', () => imageInput.click());
|
|
imageInput.addEventListener('change', async (e) => {
|
|
const files = Array.from(e.target.files);
|
|
if (files.length > 0) {
|
|
stagedMedia = [];
|
|
mediaPreviewContainer.innerHTML = '';
|
|
mediaPreviewContainer.classList.remove('hidden');
|
|
document.querySelector('.upload-placeholder').classList.add('hidden');
|
|
|
|
for (const file of files) {
|
|
if (!file.type.startsWith('image/') && !file.type.startsWith('video/')) {
|
|
alert('Skipping invalid file: ' + file.name);
|
|
continue;
|
|
}
|
|
|
|
const isVideo = file.type.startsWith('video/');
|
|
const dataUrl = await new Promise((resolve) => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => resolve(e.target.result);
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
stagedMedia.push({ type: isVideo ? 'video' : 'image', src: dataUrl });
|
|
|
|
const slide = document.createElement('div');
|
|
slide.className = 'media-slide';
|
|
if (isVideo) {
|
|
slide.innerHTML = `<video src="${dataUrl}" controls style="width:100%; height:100%; object-fit:contain;"></video>`;
|
|
} else {
|
|
slide.innerHTML = `<img src="${dataUrl}" style="width:100%; height:100%; object-fit:contain;">`;
|
|
}
|
|
mediaPreviewContainer.appendChild(slide);
|
|
}
|
|
|
|
// Add Dots for Preview
|
|
const wrapper = document.querySelector('#create-post-view .image-upload-wrapper');
|
|
const oldDots = wrapper.querySelector('.carousel-dots');
|
|
if (oldDots) oldDots.remove();
|
|
|
|
if (stagedMedia.length > 1) {
|
|
const dotsContainer = document.createElement('div');
|
|
dotsContainer.className = 'carousel-dots';
|
|
stagedMedia.forEach((_, i) => {
|
|
const dot = document.createElement('div');
|
|
dot.className = `dot ${i === 0 ? 'active' : ''}`;
|
|
dotsContainer.appendChild(dot);
|
|
});
|
|
wrapper.appendChild(dotsContainer);
|
|
|
|
// Attach Listeners
|
|
mediaPreviewContainer.onscroll = () => window.updateCarouselState(mediaPreviewContainer);
|
|
enableDragScroll(mediaPreviewContainer); // Desktop Swipe Support
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Share Handler
|
|
if (sharePostBtn) {
|
|
sharePostBtn.addEventListener('click', async () => {
|
|
if (stagedMedia.length === 0) {
|
|
alert('Please select at least one photo or video.');
|
|
return;
|
|
}
|
|
|
|
const caption = captionInput.value.trim();
|
|
const id = 'post_' + Date.now();
|
|
|
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
const displayPost = {
|
|
id,
|
|
media: stagedMedia,
|
|
caption,
|
|
timestamp: new Date().toISOString(),
|
|
username: currentUser.username || 'You',
|
|
userAvatar: currentUser.avatar,
|
|
userEmail: currentUser.email, // Secure Owner ID
|
|
likes: 0
|
|
};
|
|
renderPost(displayPost, true);
|
|
|
|
const storagePost = {
|
|
id,
|
|
media: null,
|
|
caption,
|
|
timestamp: displayPost.timestamp,
|
|
username: displayPost.username,
|
|
userEmail: currentUser.email, // Secure Owner ID
|
|
likes: 0
|
|
};
|
|
|
|
const userPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]');
|
|
userPosts.unshift(storagePost);
|
|
localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts));
|
|
|
|
try {
|
|
await saveMediaToDB(id, stagedMedia);
|
|
} catch (e) {
|
|
console.error('Failed to save media array:', e);
|
|
alert('Uploaded, but media could not be saved permanently due to storage error.');
|
|
}
|
|
|
|
resetPostForm();
|
|
createPostView.classList.add('hidden');
|
|
feedView.classList.remove('hidden');
|
|
});
|
|
}
|
|
|
|
function resetPostForm() {
|
|
if (imageInput) imageInput.value = '';
|
|
if (captionInput) captionInput.value = '';
|
|
stagedMedia = [];
|
|
if (mediaPreviewContainer) {
|
|
mediaPreviewContainer.innerHTML = '';
|
|
mediaPreviewContainer.classList.add('hidden');
|
|
mediaPreviewContainer.onscroll = null;
|
|
}
|
|
const dots = document.querySelector('#create-post-view .carousel-dots');
|
|
if (dots) dots.remove();
|
|
|
|
const ph = document.querySelector('.upload-placeholder');
|
|
if (ph) ph.classList.remove('hidden');
|
|
}
|
|
});
|