From 6774ff272d139165274c107445c26e5f3db723b9 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 29 Jan 2026 12:30:38 +1100 Subject: [PATCH] MultipleImagesPost --- index.html | 7 +- script.js | 328 ++++++++++++++++++++++++++---------------------- styles/main.css | 88 ++++++++++++- 3 files changed, 269 insertions(+), 154 deletions(-) diff --git a/index.html b/index.html index cbd18d0..4d99cb3 100644 --- a/index.html +++ b/index.html @@ -237,16 +237,17 @@
- +
- Tap to Select Photo + Tap to Select Photos or Videos
- + +
diff --git a/script.js b/script.js index 345dc42..f10e032 100644 --- a/script.js +++ b/script.js @@ -1,5 +1,5 @@ document.addEventListener('DOMContentLoaded', () => { - // --- INDEXED DB HELPERS (For >5MB Storage) --- + // --- INDEXED DB HELPERS (Multi-Media Support) --- const DB_NAME = 'SocialAppDB'; const DB_VERSION = 1; @@ -17,13 +17,14 @@ document.addEventListener('DOMContentLoaded', () => { }); } - async function saveImageToDB(id, dataUrl) { + // 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, data: dataUrl }); + store.put({ id, media: mediaArray }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); @@ -33,14 +34,15 @@ document.addEventListener('DOMContentLoaded', () => { } } - async function getImageFromDB(id) { + // 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.data : null); + request.onsuccess = () => resolve(request.result ? request.result.media : null); request.onerror = () => reject(request.error); }); } catch (e) { @@ -60,7 +62,7 @@ document.addEventListener('DOMContentLoaded', () => { tx.onerror = () => reject(tx.error); }); } catch (e) { - console.error('IndexedDB Delete Error:', e); + console.log('IndexedDB Delete Error or ID not found:', e); } } @@ -84,7 +86,7 @@ document.addEventListener('DOMContentLoaded', () => { 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 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'); @@ -102,7 +104,6 @@ document.addEventListener('DOMContentLoaded', () => { function updateUI() { if (!title || !subtitle || !btnText || !confirmPasswordGroup) return; - if (isLoginMode) { title.innerText = 'Welcome Back'; subtitle.innerText = 'Enter your details below'; @@ -120,7 +121,6 @@ document.addEventListener('DOMContentLoaded', () => { confirmPasswordGroup.classList.remove('hidden'); confirmPasswordInput.required = true; } - const newLink = document.querySelector('.signup-link a'); if (newLink) { newLink.addEventListener('click', (e) => { @@ -129,7 +129,6 @@ document.addEventListener('DOMContentLoaded', () => { updateUI(); }); } - if (loginForm) loginForm.reset(); resetButton(); } @@ -148,19 +147,8 @@ document.addEventListener('DOMContentLoaded', () => { submitBtn.style.background = 'linear-gradient(135deg, #10B981 0%, #059669 100%)'; } else { submitBtn.style.background = 'linear-gradient(135deg, #EF4444 0%, #B91C1C 100%)'; - submitBtn.animate([ - { transform: 'translateX(0)' }, - { transform: 'translateX(-5px)' }, - { transform: 'translateX(5px)' }, - { transform: 'translateX(0)' } - ], { duration: 300 }); } - - setTimeout(() => { - if (!message.includes('Success')) { - resetButton(); - } - }, 2000); + setTimeout(() => { if (!message.includes('Success')) resetButton(); }, 2000); } function navigateToFeed() { @@ -169,16 +157,11 @@ document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { loginContainer.classList.add('hidden'); loginContainer.style.display = 'none'; - feedView.classList.remove('hidden'); - void feedView.offsetWidth; // Reflow + void feedView.offsetWidth; feedView.classList.add('fade-in'); - - initFeed(); // Load data (Async) - - setTimeout(() => { - feedView.classList.remove('fade-in'); - }, 600); + initFeed(); + setTimeout(() => feedView.classList.remove('fade-in'), 600); }, 600); } } @@ -213,60 +196,45 @@ document.addEventListener('DOMContentLoaded', () => { existingUsers.push(newUser); localStorage.setItem('socialAppUsers', JSON.stringify(existingUsers)); showFeedback(true, 'Account Created!'); - setTimeout(() => { - isLoginMode = true; - updateUI(); - setTimeout(navigateToFeed, 1000); - }, 1500); + setTimeout(() => { isLoginMode = true; updateUI(); setTimeout(navigateToFeed, 1000); }, 1500); } }); } // --- 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); - } + 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 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; + let mediaItems = post.media; + // Backwards Compatibility + if (!mediaItems && post.imageSrc) { + mediaItems = [{ type: 'image', src: post.imageSrc }]; + } + if (!mediaItems || (mediaItems.length > 0 && mediaItems[0].src && mediaItems[0].src.length < 100)) { + const dbMedia = await getMediaFromDB(post.id); + if (dbMedia) mediaItems = dbMedia; } - if (imgSrc) { - const postWithImg = { ...post, imageSrc: imgSrc }; - renderPost(postWithImg, true); + if (mediaItems && mediaItems.length > 0) { + const postWithMedia = { ...post, media: mediaItems }; + renderPost(postWithMedia, true); } } } - // 2. Restore Interaction State (Likes/Comments) - // Give a small delay to ensure rendering catches up or run immediately + // Restore State const state = getPostState(); document.querySelectorAll('.post-card').forEach(card => { const id = card.getAttribute('data-post-id'); - if (id && isPostDeleted(id)) { - card.remove(); - return; - } + if (id && isPostDeleted(id)) { card.remove(); return; } if (id && state[id]) { const data = state[id]; const likeIcon = card.querySelector('.heart-icon'); @@ -297,7 +265,6 @@ 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'); @@ -318,6 +285,31 @@ document.addEventListener('DOMContentLoaded', () => {
` : ``; + 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 += ` +
+ +
`; + } else { + slidesHTML += ` +
+ Post Media ${index + 1} +
`; + } + if (media.length > 1) { + dotsHTML += `
`; + } + }); + } + + const paginationHTML = media.length > 1 ? `` : ''; + article.innerHTML = `
@@ -326,8 +318,11 @@ document.addEventListener('DOMContentLoaded', () => {
${optionsHTML}
-
- Post +
+ + ${paginationHTML}
@@ -350,57 +345,82 @@ document.addEventListener('DOMContentLoaded', () => { `; 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); + + // Attach Drag-to-Scroll for Desktop + if (media.length > 1) { + const carousel = article.querySelector('.media-carousel'); + if (carousel) enableDragScroll(carousel); + } + } + + // --- 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 + window.updateDots = function (carousel) { + const index = Math.round(carousel.scrollLeft / carousel.offsetWidth); + const dots = carousel.parentElement.querySelectorAll('.dot'); + dots.forEach((dot, i) => { + if (i === index) dot.classList.add('active'); + else dot.classList.remove('active'); + }); } // --- FEED LISTENERS --- if (feedView) { - initFeed(); // Initial Load logic - + initFeed(); feedView.addEventListener('click', (e) => { const target = e.target; - - // 1. OPTIONS MENU 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 => { - if (m !== menu) m.classList.remove('active'); - }); + document.querySelectorAll('.options-menu.active').forEach(m => m.classList.remove('active')); menu.classList.toggle('active'); e.stopPropagation(); } return; } - - // 2. DELETE ACTION 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_')) { // Double check ownership + if (id && id.startsWith('post_')) { 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); - let userPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); 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)) { deletedPosts.push(id); @@ -410,19 +430,15 @@ document.addEventListener('DOMContentLoaded', () => { } return; } - - // 3. 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'); - icon.classList.toggle('liked'); let count = parseInt(likesText.innerText); let isLiked = false; - if (icon.classList.contains('liked')) { icon.style.fill = '#EF4444'; icon.style.stroke = '#EF4444'; @@ -435,7 +451,6 @@ document.addEventListener('DOMContentLoaded', () => { isLiked = false; } likesText.innerText = count + ' likes'; - if (postId) { const state = getPostState(); if (!state[postId]) state[postId] = { comments: [] }; @@ -446,13 +461,10 @@ document.addEventListener('DOMContentLoaded', () => { } return; } - - // 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) { const svgs = commentBtn.innerHTML; - if (svgs.includes('M21 11.5')) { // Comment icon path substring + if (svgs.includes('M21 11.5')) { const postCard = commentBtn.closest('.post-card'); const commentSection = postCard.querySelector('.comment-section'); commentSection.classList.toggle('active'); @@ -463,15 +475,12 @@ document.addEventListener('DOMContentLoaded', () => { return; } } - - // 5. POST COMMENT 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) { @@ -485,15 +494,12 @@ document.addEventListener('DOMContentLoaded', () => { } } }); - - // 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) { @@ -507,8 +513,6 @@ document.addEventListener('DOMContentLoaded', () => { } } }); - - // Close menus 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')); @@ -525,69 +529,92 @@ document.addEventListener('DOMContentLoaded', () => { } // --- CREATE POST LOGIC --- - // Navigation if (navPostBtn) { - navPostBtn.addEventListener('click', () => { - if (feedView && createPostView) { - feedView.classList.add('hidden'); - createPostView.classList.remove('hidden'); - createPostView.classList.add('fade-in'); - } - }); + navPostBtn.addEventListener('click', () => { if (feedView && createPostView) { feedView.classList.add('hidden'); createPostView.classList.remove('hidden'); createPostView.classList.add('fade-in'); } }); } - - // Cancel if (cancelPostBtn) { - cancelPostBtn.addEventListener('click', () => { - resetPostForm(); - createPostView.classList.add('hidden'); - feedView.classList.remove('hidden'); - }); + cancelPostBtn.addEventListener('click', () => { resetPostForm(); createPostView.classList.add('hidden'); feedView.classList.remove('hidden'); }); } - // Home Nav if (navHomeBtn) { - navHomeBtn.addEventListener('click', () => { - if (createPostView && !createPostView.classList.contains('hidden')) { - resetPostForm(); - createPostView.classList.add('hidden'); - feedView.classList.remove('hidden'); - } - }); + navHomeBtn.addEventListener('click', () => { if (createPostView && !createPostView.classList.contains('hidden')) { resetPostForm(); createPostView.classList.add('hidden'); feedView.classList.remove('hidden'); } }); } - // Upload + // Staging Area for Selected Files + let stagedMedia = []; + + // Upload Handler (Multiple Files) if (uploadTrigger && imageInput) { uploadTrigger.addEventListener('click', () => imageInput.click()); - 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); + 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 = ``; + } else { + slide.innerHTML = ``; + } + 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.updateDots(mediaPreviewContainer); + enableDragScroll(mediaPreviewContainer); // Desktop Swipe Support + } } }); } - // Share (Logic Updated for IndexedDB) + // Share Handler if (sharePostBtn) { sharePostBtn.addEventListener('click', async () => { - if (!imagePreview.src || imagePreview.classList.contains('hidden')) { - alert('Please select an image first.'); + if (stagedMedia.length === 0) { + alert('Please select at least one photo or video.'); return; } const caption = captionInput.value.trim(); - const imageSrc = imagePreview.src; const id = 'post_' + Date.now(); - // 1. Render Optimistically (Immediate Feedback) const displayPost = { id, - imageSrc, + media: stagedMedia, caption, timestamp: new Date().toISOString(), username: 'you', @@ -595,11 +622,9 @@ document.addEventListener('DOMContentLoaded', () => { }; 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 + media: null, caption, timestamp: displayPost.timestamp, username: displayPost.username, @@ -610,13 +635,11 @@ document.addEventListener('DOMContentLoaded', () => { userPosts.unshift(storagePost); localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts)); - // 3. Save Image Data to IndexedDB (Large Capacity) - // Async operation try { - await saveImageToDB(id, imageSrc); + await saveMediaToDB(id, stagedMedia); } catch (e) { - console.error('Failed to save to DB:', e); - alert('Uploaded, but image data count not be saved permanently due to storage error.'); + console.error('Failed to save media array:', e); + alert('Uploaded, but media could not be saved permanently due to storage error.'); } resetPostForm(); @@ -628,10 +651,15 @@ document.addEventListener('DOMContentLoaded', () => { function resetPostForm() { if (imageInput) imageInput.value = ''; if (captionInput) captionInput.value = ''; - if (imagePreview) { - imagePreview.src = ''; - imagePreview.classList.add('hidden'); + 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'); } diff --git a/styles/main.css b/styles/main.css index baac6e6..9e0734b 100644 --- a/styles/main.css +++ b/styles/main.css @@ -783,4 +783,90 @@ p { outline: none; padding-top: 8px; /* Align with avatar */ -} \ No newline at end of file +} +/* --- CAROUSEL & VIDEO STYLES --- */ + +/* Carousel Container */ +.media-carousel { + width: 100%; + height: 100%; + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ + background: #000; +} + +.media-carousel::-webkit-scrollbar { + display: none; +} + +/* Individual Slide */ +.media-slide { + min-width: 100%; + height: 100%; + scroll-snap-align: center; + position: relative; + display: flex; + justify-content: center; + background: #000; +} + +.media-slide img, +.media-slide video { + width: 100%; + height: 100%; + object-fit: cover; /* or contain depending on pref */ + display: block; +} + +/* Pagination Dots */ +.carousel-dots { + position: absolute; + bottom: 15px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 6px; + z-index: 10; + pointer-events: none; +} + +.dot { + width: 6px; + height: 6px; + background: rgba(255, 255, 255, 0.4); + border-radius: 50%; + transition: 0.2s; +} + +.dot.active { + background: #fff; + transform: scale(1.2); +} + +/* Post Media Wrapper (replaces .post-image) */ +.post-media-wrapper { + position: relative; + width: 100%; + aspect-ratio: 4/5; + background: #111; + overflow: hidden; +} + +/* Update create post view to handle carousel preview */ +#create-post-view .image-upload-wrapper { + /* Keep aspect ratio but allow overflow for carousel */ + display: block; + overflow: hidden; +} + +#media-preview-container { + width: 100%; + height: 100%; + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; +} +