MultipleImagesPost

This commit is contained in:
2026-01-29 12:30:38 +11:00
parent 4b64a9e13b
commit 6774ff272d
3 changed files with 269 additions and 154 deletions

View File

@@ -237,16 +237,17 @@
<div class="post-creation-area"> <div class="post-creation-area">
<div class="image-upload-wrapper" id="upload-trigger"> <div class="image-upload-wrapper" id="upload-trigger">
<input type="file" id="post-image-input" accept="image/*" hidden> <input type="file" id="post-image-input" accept="image/*,video/*" multiple hidden>
<div class="upload-placeholder"> <div class="upload-placeholder">
<svg viewBox="0 0 24 24" width="48" height="48" stroke="white" fill="none" stroke-width="1"> <svg viewBox="0 0 24 24" width="48" height="48" stroke="white" fill="none" stroke-width="1">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle> <circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline> <polyline points="21 15 16 10 5 21"></polyline>
</svg> </svg>
<span>Tap to Select Photo</span> <span>Tap to Select Photos or Videos</span>
</div> </div>
<img id="image-preview" class="hidden" src="" alt="Preview"> <!-- Preview Container for Carousel -->
<div id="media-preview-container" class="media-carousel hidden"></div>
</div> </div>
<div class="caption-wrapper"> <div class="caption-wrapper">

328
script.js
View File

@@ -1,5 +1,5 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// --- INDEXED DB HELPERS (For >5MB Storage) --- // --- INDEXED DB HELPERS (Multi-Media Support) ---
const DB_NAME = 'SocialAppDB'; const DB_NAME = 'SocialAppDB';
const DB_VERSION = 1; 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 { try {
const db = await openDB(); const db = await openDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction('images', 'readwrite'); const tx = db.transaction('images', 'readwrite');
const store = tx.objectStore('images'); const store = tx.objectStore('images');
store.put({ id, data: dataUrl }); store.put({ id, media: mediaArray });
tx.oncomplete = () => resolve(); tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error); tx.onerror = () => reject(tx.error);
}); });
@@ -33,14 +34,15 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
async function getImageFromDB(id) { // Get Media Array
async function getMediaFromDB(id) {
try { try {
const db = await openDB(); const db = await openDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction('images', 'readonly'); const tx = db.transaction('images', 'readonly');
const store = tx.objectStore('images'); const store = tx.objectStore('images');
const request = store.get(id); 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); request.onerror = () => reject(request.error);
}); });
} catch (e) { } catch (e) {
@@ -60,7 +62,7 @@ document.addEventListener('DOMContentLoaded', () => {
tx.onerror = () => reject(tx.error); tx.onerror = () => reject(tx.error);
}); });
} catch (e) { } 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 sharePostBtn = document.getElementById('share-post-btn');
const uploadTrigger = document.getElementById('upload-trigger'); const uploadTrigger = document.getElementById('upload-trigger');
const imageInput = document.getElementById('post-image-input'); 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 captionInput = document.getElementById('post-caption-input');
const navPostBtn = document.querySelector('.nav-btn:nth-child(2)'); const navPostBtn = document.querySelector('.nav-btn:nth-child(2)');
const navHomeBtn = document.querySelector('.nav-btn:first-child'); const navHomeBtn = document.querySelector('.nav-btn:first-child');
@@ -102,7 +104,6 @@ document.addEventListener('DOMContentLoaded', () => {
function updateUI() { function updateUI() {
if (!title || !subtitle || !btnText || !confirmPasswordGroup) return; if (!title || !subtitle || !btnText || !confirmPasswordGroup) return;
if (isLoginMode) { if (isLoginMode) {
title.innerText = 'Welcome Back'; title.innerText = 'Welcome Back';
subtitle.innerText = 'Enter your details below'; subtitle.innerText = 'Enter your details below';
@@ -120,7 +121,6 @@ document.addEventListener('DOMContentLoaded', () => {
confirmPasswordGroup.classList.remove('hidden'); confirmPasswordGroup.classList.remove('hidden');
confirmPasswordInput.required = true; confirmPasswordInput.required = true;
} }
const newLink = document.querySelector('.signup-link a'); const newLink = document.querySelector('.signup-link a');
if (newLink) { if (newLink) {
newLink.addEventListener('click', (e) => { newLink.addEventListener('click', (e) => {
@@ -129,7 +129,6 @@ document.addEventListener('DOMContentLoaded', () => {
updateUI(); updateUI();
}); });
} }
if (loginForm) loginForm.reset(); if (loginForm) loginForm.reset();
resetButton(); resetButton();
} }
@@ -148,19 +147,8 @@ document.addEventListener('DOMContentLoaded', () => {
submitBtn.style.background = 'linear-gradient(135deg, #10B981 0%, #059669 100%)'; submitBtn.style.background = 'linear-gradient(135deg, #10B981 0%, #059669 100%)';
} else { } else {
submitBtn.style.background = 'linear-gradient(135deg, #EF4444 0%, #B91C1C 100%)'; 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() { function navigateToFeed() {
@@ -169,16 +157,11 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => { setTimeout(() => {
loginContainer.classList.add('hidden'); loginContainer.classList.add('hidden');
loginContainer.style.display = 'none'; loginContainer.style.display = 'none';
feedView.classList.remove('hidden'); feedView.classList.remove('hidden');
void feedView.offsetWidth; // Reflow void feedView.offsetWidth;
feedView.classList.add('fade-in'); feedView.classList.add('fade-in');
initFeed();
initFeed(); // Load data (Async) setTimeout(() => feedView.classList.remove('fade-in'), 600);
setTimeout(() => {
feedView.classList.remove('fade-in');
}, 600);
}, 600); }, 600);
} }
} }
@@ -213,60 +196,45 @@ document.addEventListener('DOMContentLoaded', () => {
existingUsers.push(newUser); existingUsers.push(newUser);
localStorage.setItem('socialAppUsers', JSON.stringify(existingUsers)); localStorage.setItem('socialAppUsers', JSON.stringify(existingUsers));
showFeedback(true, 'Account Created!'); showFeedback(true, 'Account Created!');
setTimeout(() => { setTimeout(() => { isLoginMode = true; updateUI(); setTimeout(navigateToFeed, 1000); }, 1500);
isLoginMode = true;
updateUI();
setTimeout(navigateToFeed, 1000);
}, 1500);
} }
}); });
} }
// --- DATA HELPERS --- // --- DATA HELPERS ---
function getPostState() { function getPostState() { return JSON.parse(localStorage.getItem('socialAppPostState') || '{}'); }
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 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() { async function initFeed() {
// 1. Render User Posts (Fetch images from DB)
const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); const savedUserPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]');
// Use a loop to handle async properly
const postsToRender = [...savedUserPosts].reverse(); const postsToRender = [...savedUserPosts].reverse();
for (const post of postsToRender) { for (const post of postsToRender) {
if (!isPostDeleted(post.id) && !document.querySelector(`[data-post-id="${post.id}"]`)) { if (!isPostDeleted(post.id) && !document.querySelector(`[data-post-id="${post.id}"]`)) {
// Try to get image from DB, fallback to post.imageSrc (legacy support) let mediaItems = post.media;
let imgSrc = post.imageSrc; // Backwards Compatibility
if (!imgSrc || imgSrc.length < 100) { // If it's a placeholder (null) if (!mediaItems && post.imageSrc) {
const dbImg = await getImageFromDB(post.id); mediaItems = [{ type: 'image', src: post.imageSrc }];
if (dbImg) imgSrc = dbImg; }
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) { if (mediaItems && mediaItems.length > 0) {
const postWithImg = { ...post, imageSrc: imgSrc }; const postWithMedia = { ...post, media: mediaItems };
renderPost(postWithImg, true); renderPost(postWithMedia, true);
} }
} }
} }
// 2. Restore Interaction State (Likes/Comments) // Restore State
// Give a small delay to ensure rendering catches up or run immediately
const state = getPostState(); const state = getPostState();
document.querySelectorAll('.post-card').forEach(card => { document.querySelectorAll('.post-card').forEach(card => {
const id = card.getAttribute('data-post-id'); const id = card.getAttribute('data-post-id');
if (id && isPostDeleted(id)) { if (id && isPostDeleted(id)) { card.remove(); return; }
card.remove();
return;
}
if (id && state[id]) { if (id && state[id]) {
const data = state[id]; const data = state[id];
const likeIcon = card.querySelector('.heart-icon'); const likeIcon = card.querySelector('.heart-icon');
@@ -297,7 +265,6 @@ document.addEventListener('DOMContentLoaded', () => {
// --- RENDER POST --- // --- RENDER POST ---
function renderPost(post, prepend = false) { function renderPost(post, prepend = false) {
// Avoid duplicates
if (document.querySelector(`[data-post-id="${post.id}"]`)) return; if (document.querySelector(`[data-post-id="${post.id}"]`)) return;
const feedContainer = document.querySelector('.feed-container'); const feedContainer = document.querySelector('.feed-container');
@@ -318,6 +285,31 @@ document.addEventListener('DOMContentLoaded', () => {
</div> </div>
` : `<button class="icon-btn-sm">...</button>`; ` : `<button class="icon-btn-sm">...</button>`;
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">
<video src="${item.src}" controls playsinline loop></video>
</div>`;
} else {
slidesHTML += `
<div class="media-slide">
<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>` : '';
article.innerHTML = ` article.innerHTML = `
<div class="post-header"> <div class="post-header">
<div class="post-user"> <div class="post-user">
@@ -326,8 +318,11 @@ document.addEventListener('DOMContentLoaded', () => {
</div> </div>
${optionsHTML} ${optionsHTML}
</div> </div>
<div class="post-image"> <div class="post-image-wrapper post-media-wrapper">
<img src="${post.imageSrc}" alt="Post"> <div class="media-carousel" onscroll="updateDots(this)">
${slidesHTML}
</div>
${paginationHTML}
</div> </div>
<div class="post-actions"> <div class="post-actions">
<div class="action-left"> <div class="action-left">
@@ -350,57 +345,82 @@ document.addEventListener('DOMContentLoaded', () => {
`; `;
if (prepend) { if (prepend) {
// Check again for safety inside async flows
if (!feedContainer.querySelector(`[data-post-id="${post.id}"]`)) { if (!feedContainer.querySelector(`[data-post-id="${post.id}"]`)) {
feedContainer.insertBefore(article, feedContainer.firstChild); feedContainer.insertBefore(article, feedContainer.firstChild);
} }
} }
else feedContainer.appendChild(article); 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 --- // --- FEED LISTENERS ---
if (feedView) { if (feedView) {
initFeed(); // Initial Load logic initFeed();
feedView.addEventListener('click', (e) => { feedView.addEventListener('click', (e) => {
const target = e.target; const target = e.target;
// 1. OPTIONS MENU
const trigger = target.closest('.options-trigger'); const trigger = target.closest('.options-trigger');
if (trigger) { if (trigger) {
const header = trigger.closest('.post-header'); const header = trigger.closest('.post-header');
const menu = header.querySelector('.options-menu'); const menu = header.querySelector('.options-menu');
if (menu) { if (menu) {
document.querySelectorAll('.options-menu.active').forEach(m => { document.querySelectorAll('.options-menu.active').forEach(m => m.classList.remove('active'));
if (m !== menu) m.classList.remove('active');
});
menu.classList.toggle('active'); menu.classList.toggle('active');
e.stopPropagation(); e.stopPropagation();
} }
return; return;
} }
// 2. DELETE ACTION
const deleteBtn = target.closest('.delete'); const deleteBtn = target.closest('.delete');
if (deleteBtn) { if (deleteBtn) {
const card = deleteBtn.closest('.post-card'); const card = deleteBtn.closest('.post-card');
const id = card.getAttribute('data-post-id'); const id = card.getAttribute('data-post-id');
if (id && id.startsWith('post_')) {
if (id && id.startsWith('post_')) { // Double check ownership
if (confirm('Delete this post?')) { if (confirm('Delete this post?')) {
card.style.transition = 'opacity 0.3s, transform 0.3s'; card.style.transition = 'opacity 0.3s, transform 0.3s';
card.style.opacity = '0'; card.style.opacity = '0';
card.style.transform = 'scale(0.9)'; card.style.transform = 'scale(0.9)';
setTimeout(() => card.remove(), 300); setTimeout(() => card.remove(), 300);
let userPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]'); let userPosts = JSON.parse(localStorage.getItem('socialAppUserPosts') || '[]');
userPosts = userPosts.filter(p => p.id !== id); userPosts = userPosts.filter(p => p.id !== id);
localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts)); localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts));
// Delete from DB too
deleteImageFromDB(id); deleteImageFromDB(id);
// Also ban ID to be safe
const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]'); const deletedPosts = JSON.parse(localStorage.getItem('socialAppDeletedPosts') || '[]');
if (!deletedPosts.includes(id)) { if (!deletedPosts.includes(id)) {
deletedPosts.push(id); deletedPosts.push(id);
@@ -410,19 +430,15 @@ document.addEventListener('DOMContentLoaded', () => {
} }
return; return;
} }
// 3. LIKE ACTION
const likeBtn = target.closest('.heart-icon')?.closest('.icon-btn'); const likeBtn = target.closest('.heart-icon')?.closest('.icon-btn');
if (likeBtn) { if (likeBtn) {
const icon = likeBtn.querySelector('.heart-icon'); const icon = likeBtn.querySelector('.heart-icon');
const postCard = likeBtn.closest('.post-card'); const postCard = likeBtn.closest('.post-card');
const likesText = postCard.querySelector('.likes'); const likesText = postCard.querySelector('.likes');
const postId = postCard.getAttribute('data-post-id'); const postId = postCard.getAttribute('data-post-id');
icon.classList.toggle('liked'); icon.classList.toggle('liked');
let count = parseInt(likesText.innerText); let count = parseInt(likesText.innerText);
let isLiked = false; let isLiked = false;
if (icon.classList.contains('liked')) { if (icon.classList.contains('liked')) {
icon.style.fill = '#EF4444'; icon.style.fill = '#EF4444';
icon.style.stroke = '#EF4444'; icon.style.stroke = '#EF4444';
@@ -435,7 +451,6 @@ document.addEventListener('DOMContentLoaded', () => {
isLiked = false; isLiked = false;
} }
likesText.innerText = count + ' likes'; likesText.innerText = count + ' likes';
if (postId) { if (postId) {
const state = getPostState(); const state = getPostState();
if (!state[postId]) state[postId] = { comments: [] }; if (!state[postId]) state[postId] = { comments: [] };
@@ -446,13 +461,10 @@ document.addEventListener('DOMContentLoaded', () => {
} }
return; 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'); const commentBtn = target.closest('.icon-btn');
if (commentBtn) { if (commentBtn) {
const svgs = commentBtn.innerHTML; 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 postCard = commentBtn.closest('.post-card');
const commentSection = postCard.querySelector('.comment-section'); const commentSection = postCard.querySelector('.comment-section');
commentSection.classList.toggle('active'); commentSection.classList.toggle('active');
@@ -463,15 +475,12 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
} }
// 5. POST COMMENT
if (target.classList.contains('post-btn')) { if (target.classList.contains('post-btn')) {
const wrapper = target.closest('.comment-input-wrapper'); const wrapper = target.closest('.comment-input-wrapper');
const input = wrapper.querySelector('.comment-input'); const input = wrapper.querySelector('.comment-input');
const text = input.value.trim(); const text = input.value.trim();
const postCard = wrapper.closest('.post-card'); const postCard = wrapper.closest('.post-card');
const postId = postCard.getAttribute('data-post-id'); const postId = postCard.getAttribute('data-post-id');
if (text) { if (text) {
addComment(wrapper.closest('.comment-section'), text); addComment(wrapper.closest('.comment-section'), text);
if (postId) { if (postId) {
@@ -485,15 +494,12 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
}); });
// Enter key for comments
feedView.addEventListener('keydown', (e) => { feedView.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.target.classList.contains('comment-input')) { if (e.key === 'Enter' && e.target.classList.contains('comment-input')) {
const text = e.target.value.trim(); const text = e.target.value.trim();
const wrapper = e.target.closest('.comment-input-wrapper'); const wrapper = e.target.closest('.comment-input-wrapper');
const postCard = wrapper.closest('.post-card'); const postCard = wrapper.closest('.post-card');
const postId = postCard.getAttribute('data-post-id'); const postId = postCard.getAttribute('data-post-id');
if (text) { if (text) {
addComment(e.target.closest('.comment-section'), text); addComment(e.target.closest('.comment-section'), text);
if (postId) { if (postId) {
@@ -507,8 +513,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
}); });
// Close menus
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (!e.target.closest('.options-menu') && !e.target.closest('.options-trigger')) { if (!e.target.closest('.options-menu') && !e.target.closest('.options-trigger')) {
document.querySelectorAll('.options-menu.active').forEach(m => m.classList.remove('active')); document.querySelectorAll('.options-menu.active').forEach(m => m.classList.remove('active'));
@@ -525,69 +529,92 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// --- CREATE POST LOGIC --- // --- CREATE POST LOGIC ---
// Navigation
if (navPostBtn) { if (navPostBtn) {
navPostBtn.addEventListener('click', () => { navPostBtn.addEventListener('click', () => { if (feedView && createPostView) { feedView.classList.add('hidden'); createPostView.classList.remove('hidden'); createPostView.classList.add('fade-in'); } });
if (feedView && createPostView) {
feedView.classList.add('hidden');
createPostView.classList.remove('hidden');
createPostView.classList.add('fade-in');
}
});
} }
// Cancel
if (cancelPostBtn) { if (cancelPostBtn) {
cancelPostBtn.addEventListener('click', () => { cancelPostBtn.addEventListener('click', () => { resetPostForm(); createPostView.classList.add('hidden'); feedView.classList.remove('hidden'); });
resetPostForm();
createPostView.classList.add('hidden');
feedView.classList.remove('hidden');
});
} }
// Home Nav
if (navHomeBtn) { if (navHomeBtn) {
navHomeBtn.addEventListener('click', () => { navHomeBtn.addEventListener('click', () => { if (createPostView && !createPostView.classList.contains('hidden')) { resetPostForm(); createPostView.classList.add('hidden'); feedView.classList.remove('hidden'); } });
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) { if (uploadTrigger && imageInput) {
uploadTrigger.addEventListener('click', () => imageInput.click()); uploadTrigger.addEventListener('click', () => imageInput.click());
imageInput.addEventListener('change', (e) => { imageInput.addEventListener('change', async (e) => {
const file = e.target.files[0]; const files = Array.from(e.target.files);
if (file) { if (files.length > 0) {
const reader = new FileReader(); stagedMedia = [];
reader.onload = (e) => { mediaPreviewContainer.innerHTML = '';
imagePreview.src = e.target.result; mediaPreviewContainer.classList.remove('hidden');
imagePreview.classList.remove('hidden'); document.querySelector('.upload-placeholder').classList.add('hidden');
document.querySelector('.upload-placeholder').classList.add('hidden');
}; for (const file of files) {
reader.readAsDataURL(file); 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.updateDots(mediaPreviewContainer);
enableDragScroll(mediaPreviewContainer); // Desktop Swipe Support
}
} }
}); });
} }
// Share (Logic Updated for IndexedDB) // Share Handler
if (sharePostBtn) { if (sharePostBtn) {
sharePostBtn.addEventListener('click', async () => { sharePostBtn.addEventListener('click', async () => {
if (!imagePreview.src || imagePreview.classList.contains('hidden')) { if (stagedMedia.length === 0) {
alert('Please select an image first.'); alert('Please select at least one photo or video.');
return; return;
} }
const caption = captionInput.value.trim(); const caption = captionInput.value.trim();
const imageSrc = imagePreview.src;
const id = 'post_' + Date.now(); const id = 'post_' + Date.now();
// 1. Render Optimistically (Immediate Feedback)
const displayPost = { const displayPost = {
id, id,
imageSrc, media: stagedMedia,
caption, caption,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
username: 'you', username: 'you',
@@ -595,11 +622,9 @@ document.addEventListener('DOMContentLoaded', () => {
}; };
renderPost(displayPost, true); renderPost(displayPost, true);
// 2. Save Metadata to LocalStorage (Small footprint)
// IMPORTANT: We do NOT save the imageSrc string here to save space
const storagePost = { const storagePost = {
id, id,
imageSrc: null, // Placeholder, will look up in DB media: null,
caption, caption,
timestamp: displayPost.timestamp, timestamp: displayPost.timestamp,
username: displayPost.username, username: displayPost.username,
@@ -610,13 +635,11 @@ document.addEventListener('DOMContentLoaded', () => {
userPosts.unshift(storagePost); userPosts.unshift(storagePost);
localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts)); localStorage.setItem('socialAppUserPosts', JSON.stringify(userPosts));
// 3. Save Image Data to IndexedDB (Large Capacity)
// Async operation
try { try {
await saveImageToDB(id, imageSrc); await saveMediaToDB(id, stagedMedia);
} catch (e) { } catch (e) {
console.error('Failed to save to DB:', e); console.error('Failed to save media array:', e);
alert('Uploaded, but image data count not be saved permanently due to storage error.'); alert('Uploaded, but media could not be saved permanently due to storage error.');
} }
resetPostForm(); resetPostForm();
@@ -628,10 +651,15 @@ document.addEventListener('DOMContentLoaded', () => {
function resetPostForm() { function resetPostForm() {
if (imageInput) imageInput.value = ''; if (imageInput) imageInput.value = '';
if (captionInput) captionInput.value = ''; if (captionInput) captionInput.value = '';
if (imagePreview) { stagedMedia = [];
imagePreview.src = ''; if (mediaPreviewContainer) {
imagePreview.classList.add('hidden'); 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'); const ph = document.querySelector('.upload-placeholder');
if (ph) ph.classList.remove('hidden'); if (ph) ph.classList.remove('hidden');
} }

View File

@@ -784,3 +784,89 @@ p {
padding-top: 8px; padding-top: 8px;
/* Align with avatar */ /* Align with avatar */
} }
/* --- 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;
}