diff --git a/script.js b/script.js index 1854f1c..345dc42 100644 --- a/script.js +++ b/script.js @@ -1,4 +1,69 @@ document.addEventListener('DOMContentLoaded', () => { + // --- INDEXED DB HELPERS (For >5MB Storage) --- + 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' }); + } + }; + }); + } + + async function saveImageToDB(id, dataUrl) { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction('images', 'readwrite'); + const store = tx.objectStore('images'); + store.put({ id, data: dataUrl }); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } catch (e) { + console.error('IndexedDB Save Error:', e); + throw e; + } + } + + async function getImageFromDB(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.data : 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.error('IndexedDB Delete Error:', e); + } + } + // --- QUERY SELECTORS --- const loginForm = document.querySelector('.login-form'); const loginContainer = document.querySelector('.login-container'); @@ -109,7 +174,7 @@ document.addEventListener('DOMContentLoaded', () => { void feedView.offsetWidth; // Reflow feedView.classList.add('fade-in'); - loadPostState(); // Load data + initFeed(); // Load data (Async) setTimeout(() => { feedView.classList.remove('fade-in'); @@ -169,76 +234,32 @@ document.addEventListener('DOMContentLoaded', () => { return deletedPosts.includes(id); } - function loadPostState() { - const state = getPostState(); - // 1. Remove deleted static posts - document.querySelectorAll('.post-card').forEach(card => { - const id = card.getAttribute('data-post-id'); - if (id && isPostDeleted(id)) { - card.remove(); - } else if (id && state[id]) { - // Restore Likes/Comments - const data = state[id]; - const likeIcon = card.querySelector('.heart-icon'); - const likesText = card.querySelector('.likes'); - if (likeIcon && likesText) { - 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'; + // --- ASYNC LOAD FEED --- + async function initFeed() { + // 1. Render User Posts (Fetch images from DB) + const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); + + // Use a loop to handle async properly + const postsToRender = [...savedUserPosts].reverse(); + + for (const post of postsToRender) { + if (!isPostDeleted(post.id) && !document.querySelector(`[data-post-id="${post.id}"]`)) { + // Try to get image from DB, fallback to post.imageSrc (legacy support) + let imgSrc = post.imageSrc; + if (!imgSrc || imgSrc.length < 100) { // If it's a placeholder (null) + const dbImg = await getImageFromDB(post.id); + if (dbImg) imgSrc = dbImg; } - // Restore Comments - const commentsContainer = card.querySelector('.added-comments'); - if (commentsContainer) { - commentsContainer.innerHTML = ''; - if (data.comments && data.comments.length > 0) { - data.comments.forEach(text => { - const commentEl = document.createElement('div'); - commentEl.classList.add('comment-item'); - commentEl.innerHTML = `you ${text}`; - commentsContainer.appendChild(commentEl); - }); - } + + if (imgSrc) { + const postWithImg = { ...post, imageSrc: imgSrc }; + renderPost(postWithImg, true); } } - }); - - // 2. Render User Posts (if not already present) - const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); - // We iterate correctly to put newest on top - [...savedUserPosts].reverse().forEach(post => { - if (!isPostDeleted(post.id) && !document.querySelector(`[data-post-id="${post.id}"]`)) { - renderPost(post, true); - // Re-apply state if needed? create function handles it mostly, - // but checking `state` for these dynamic posts is also good practice if we tracked likes on them. - // For now, let's assume they load fresh or we'd need to run the state logic on them too. - // Actually, logic is: render then apply state. - } - }); - - // Re-run state application for newly added dynamic posts - // Optimization: splitting this logic explicitly would be better, but re-querying all is fine for small apps. - // Let's just do a second pass for dynamic posts to support likes on them? - // Existing loop `document.querySelectorAll` covers them if they are in DOM. - // So this order (Filter Static -> Render Dynamic -> (Ideally re-check state on all)) is slightly off. - // Better: Render Dynamic First, THEN apply state to ALL. - } - - // Improve Load Flow - function initFeed() { - const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); - [...savedUserPosts].reverse().forEach(post => { - if (!isPostDeleted(post.id) && !document.querySelector(`[data-post-id="${post.id}"]`)) { - renderPost(post, true); - } - }); + } + // 2. Restore Interaction State (Likes/Comments) + // Give a small delay to ensure rendering catches up or run immediately const state = getPostState(); document.querySelectorAll('.post-card').forEach(card => { const id = card.getAttribute('data-post-id'); @@ -276,6 +297,9 @@ document.addEventListener('DOMContentLoaded', () => { // --- RENDER POST --- function renderPost(post, prepend = false) { + // Avoid duplicates + if (document.querySelector(`[data-post-id="${post.id}"]`)) return; + const feedContainer = document.querySelector('.feed-container'); const article = document.createElement('article'); article.className = 'post-card'; @@ -325,7 +349,12 @@ document.addEventListener('DOMContentLoaded', () => { `; - if (prepend) feedContainer.insertBefore(article, feedContainer.firstChild); + if (prepend) { + // Check again for safety inside async flows + if (!feedContainer.querySelector(`[data-post-id="${post.id}"]`)) { + feedContainer.insertBefore(article, feedContainer.firstChild); + } + } else feedContainer.appendChild(article); } @@ -368,6 +397,9 @@ document.addEventListener('DOMContentLoaded', () => { userPosts = userPosts.filter(p => p.id !== id); localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts)); + // Delete from DB too + deleteImageFromDB(id); + // Also ban ID to be safe const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]'); if (!deletedPosts.includes(id)) { @@ -415,18 +447,18 @@ document.addEventListener('DOMContentLoaded', () => { return; } - // 4. COMMENT TOGGLE + // 4. COMMENT TOGGLE (Updated logic) + // Identify comment button by checking if it's the 2nd one or has specific path const commentBtn = target.closest('.icon-btn'); - if (commentBtn && !commentBtn.querySelector('.heart-icon') && !commentBtn.closest('.post-actions').querySelector('.action-left').contains(commentBtn) === false) { - // The comment button is the second one in action-left. - // Easier check: + if (commentBtn) { const svgs = commentBtn.innerHTML; - if (svgs.includes('M21 11.5')) { + if (svgs.includes('M21 11.5')) { // Comment icon path substring 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(); + const input = commentSection.querySelector('input'); + if (input) input.focus(); } return; } @@ -540,9 +572,9 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // Share + // Share (Logic Updated for IndexedDB) if (sharePostBtn) { - sharePostBtn.addEventListener('click', () => { + sharePostBtn.addEventListener('click', async () => { if (!imagePreview.src || imagePreview.classList.contains('hidden')) { alert('Please select an image first.'); return; @@ -552,7 +584,8 @@ document.addEventListener('DOMContentLoaded', () => { const imageSrc = imagePreview.src; const id = 'post_' + Date.now(); - const newPost = { + // 1. Render Optimistically (Immediate Feedback) + const displayPost = { id, imageSrc, caption, @@ -560,17 +593,35 @@ document.addEventListener('DOMContentLoaded', () => { username: 'you', likes: 0 }; + renderPost(displayPost, true); + + // 2. Save Metadata to LocalStorage (Small footprint) + // IMPORTANT: We do NOT save the imageSrc string here to save space + const storagePost = { + id, + imageSrc: null, // Placeholder, will look up in DB + caption, + timestamp: displayPost.timestamp, + username: displayPost.username, + likes: 0 + }; const userPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); - userPosts.unshift(newPost); + userPosts.unshift(storagePost); localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts)); - renderPost(newPost, true); + // 3. Save Image Data to IndexedDB (Large Capacity) + // Async operation + try { + await saveImageToDB(id, imageSrc); + } catch (e) { + console.error('Failed to save to DB:', e); + alert('Uploaded, but image data count not be saved permanently due to storage error.'); + } + resetPostForm(); createPostView.classList.add('hidden'); feedView.classList.remove('hidden'); - - // Note: Don't need full reload, but let's ensure state is clean }); }