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
-
![Preview]()
+
+
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 += `
+
`;
+ }
+ if (media.length > 1) {
+ dotsHTML += `
`;
+ }
+ });
+ }
+
+ const paginationHTML = media.length > 1 ? `
${dotsHTML}
` : '';
+
article.innerHTML = `
-
-

+
+
+ ${slidesHTML}
+
+ ${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;
+}
+