// ===== State ===== let currentSort = 'new'; let currentPosts = []; let currentPostId = null; let selectedFile = null; // ===== Initialize ===== document.addEventListener('DOMContentLoaded', () => { loadPosts(); setupDragDrop(); }); // ===== API Helpers ===== async function apiGet(endpoint) { const res = await fetch(endpoint); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } async function apiPost(endpoint, formData = null, body = null) { let res; if (formData) { res = await fetch(endpoint, { method: 'POST', body: formData }); } else { res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); } if (!res.ok) { const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` })); throw new Error(err.error || `HTTP ${res.status}`); } return res.json(); } // ===== Posts ===== async function loadPosts() { const loading = document.getElementById('loading'); loading.style.display = 'block'; try { currentPosts = await apiGet(`/api/posts?sort=${currentSort}`); renderGallery(); } catch (err) { showToast('Failed to load posts', 'error'); } finally { loading.style.display = 'none'; } } function renderGallery() { const gallery = document.getElementById('gallery'); const count = document.getElementById('postCount'); count.textContent = `${currentPosts.length} posts`; if (currentPosts.length === 0) { gallery.innerHTML = '
No posts found
'; return; } gallery.innerHTML = currentPosts.map(post => `
${escapeHtml(post.title)}

${escapeHtml(post.title)}

▲ ${formatNumber(post.upvotes)} 👁️ ${formatNumber(post.views)} 💬 ${post.commentCount || 0}
${post.tags && post.tags.length ? `
${post.tags.slice(0, 3).map(t => `${escapeHtml(t)}`).join('')}
` : ''}
`).join(''); } // ===== Sorting ===== function setSort(sort) { currentSort = sort; document.querySelectorAll('.filter-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.sort === sort); }); loadPosts(); } // ===== Search ===== function filterPosts() { const query = document.getElementById('searchInput').value.toLowerCase(); if (!query) { loadPosts(); return; } const filtered = currentPosts.filter(p => p.title.toLowerCase().includes(query) || (p.tags && p.tags.some(t => t.toLowerCase().includes(query))) ); const gallery = document.getElementById('gallery'); const count = document.getElementById('postCount'); count.textContent = `${filtered.length} posts matching "${query}"`; if (filtered.length === 0) { gallery.innerHTML = '
No posts match your search
'; return; } gallery.innerHTML = filtered.map(post => `
${escapeHtml(post.title)}

${escapeHtml(post.title)}

▲ ${formatNumber(post.upvotes)} 👁️ ${formatNumber(post.views)} 💬 ${post.commentCount || 0}
${post.tags && post.tags.length ? `
${post.tags.slice(0, 3).map(t => `${escapeHtml(t)}`).join('')}
` : ''}
`).join(''); } // ===== Upload Modal ===== function openUploadModal() { document.getElementById('uploadModal').classList.add('active'); selectedFile = null; document.getElementById('uploadPreview').style.display = 'none'; document.querySelector('.upload-prompt').style.display = ''; document.getElementById('postTitle').value = ''; document.getElementById('postTags').value = ''; document.getElementById('postAuthor').value = ''; hideError(); } function closeUploadModal() { document.getElementById('uploadModal').classList.remove('active'); } function setupDragDrop() { const zone = document.getElementById('uploadZone'); const input = document.getElementById('fileInput'); zone.addEventListener('click', () => input.click()); zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); }); zone.addEventListener('dragleave', () => { zone.classList.remove('dragover'); }); zone.addEventListener('drop', (e) => { e.preventDefault(); zone.classList.remove('dragover'); const file = e.dataTransfer.files[0]; if (file) handleFile(file); }); input.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) handleFile(file); }); } function handleFile(file) { // Validate const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!allowed.includes(file.type)) { showError(`Unsupported file type: ${file.type}. Please upload JPEG, PNG, GIF, or WebP.`); return; } if (file.size > 20 * 1024 * 1024) { showError('File too large. Maximum size is 20MB.'); return; } selectedFile = file; hideError(); // Show preview const reader = new FileReader(); reader.onload = (e) => { document.getElementById('previewImage').src = e.target.result; document.getElementById('uploadPreview').style.display = 'block'; document.querySelector('.upload-prompt').style.display = 'none'; }; reader.readAsDataURL(file); } function removePreview() { selectedFile = null; document.getElementById('uploadPreview').style.display = 'none'; document.querySelector('.upload-prompt').style.display = ''; document.getElementById('fileInput').value = ''; } async function submitPost() { const btn = document.getElementById('submitBtn'); const title = document.getElementById('postTitle').value.trim(); const tags = document.getElementById('postTags').value.trim(); const author = document.getElementById('postAuthor').value.trim() || 'Anonymous'; if (!selectedFile && !title) { showError('Please upload an image or enter a title.'); return; } btn.disabled = true; btn.textContent = 'Uploading...'; try { if (selectedFile) { const formData = new FormData(); formData.append('image', selectedFile); formData.append('title', title || 'Untitled'); formData.append('tags', tags); formData.append('author', author); await apiPost('/api/upload', formData); showToast('Post uploaded successfully! 🎉', 'success'); } closeUploadModal(); await loadPosts(); } catch (err) { showError(err.message); showToast(err.message, 'error'); } finally { btn.disabled = false; btn.textContent = 'Upload Post'; } } // ===== Post Detail Modal ===== async function openPostModal(postId) { currentPostId = postId; const modal = document.getElementById('postModal'); try { const post = await apiGet(`/api/posts/${postId}`); document.getElementById('postModalTitle').textContent = post.title; document.getElementById('postModalImage').src = post.url; document.getElementById('postModalImage').alt = post.title; document.getElementById('postModalVotes').textContent = formatNumber(post.upvotes); document.getElementById('postModalViews').textContent = formatNumber(post.views); document.getElementById('postModalComments').textContent = post.commentCount || 0; document.getElementById('postModalAuthor').innerHTML = `By ${escapeHtml(post.author)}`; document.getElementById('postModalDate').textContent = timeAgo(post.createdAt); // Tags const tagsEl = document.getElementById('postModalTags'); tagsEl.innerHTML = post.tags && post.tags.length ? post.tags.map(t => `${escapeHtml(t)}`).join('') : ''; // Load comments await loadComments(postId); modal.classList.add('active'); } catch (err) { showToast('Failed to load post details', 'error'); } } function closePostModal() { document.getElementById('postModal').classList.remove('active'); currentPostId = null; } // ===== Voting ===== async function votePost(direction) { if (!currentPostId) return; try { const result = await apiPost('/api/posts/' + currentPostId + '/vote', null, { direction, userId: 'session-' + Math.random().toString(36).slice(2, 8) }); document.getElementById('postModalVotes').textContent = formatNumber(result.upvotes); document.getElementById('postModalViews').textContent = formatNumber(result.views); // Update the gallery view too await loadPosts(); } catch (err) { showToast('Failed to vote', 'error'); } } // ===== Comments ===== async function loadComments(postId) { try { const postComments = await apiGet(`/api/comments/${postId}`); const list = document.getElementById('commentsList'); if (postComments.length === 0) { list.innerHTML = '

No comments yet. Be the first!

'; return; } const topLevel = postComments.filter(c => !c.parentId); const replies = postComments.filter(c => c.parentId); list.innerHTML = topLevel.map(c => { const childReplies = replies.filter(r => r.parentId === c.id); return `
${escapeHtml(c.author)}
${escapeHtml(c.text)}
${timeAgo(c.timestamp)}
${childReplies.map(r => `
${escapeHtml(r.author)}
${escapeHtml(r.text)}
${timeAgo(r.timestamp)}
`).join('')}
`; }).join(''); document.getElementById('postModalComments').textContent = postComments.length; } catch (err) { showToast('Failed to load comments', 'error'); } } async function postComment() { const input = document.getElementById('commentInput'); const text = input.value.trim(); if (!text || !currentPostId) return; try { await apiPost('/api/comments', null, { postId: currentPostId, text, author: 'Anonymous' }); input.value = ''; await loadComments(currentPostId); showToast('Comment posted! 💬', 'success'); } catch (err) { showToast('Failed to post comment', 'error'); } } // ===== Fullscreen ===== function openFullscreen() { const img = document.getElementById('postModalImage').src; document.getElementById('fullscreenImage').src = img; document.getElementById('fullscreenOverlay').classList.add('active'); } function closeFullscreen() { document.getElementById('fullscreenOverlay').classList.remove('active'); } // ===== Navigation ===== function navigateHome() { document.getElementById('searchInput').value = ''; loadPosts(); } // ===== Utilities ===== function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatNumber(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; return n.toString(); } function timeAgo(dateStr) { const now = new Date(); const date = new Date(dateStr); const diff = Math.floor((now - date) / 1000); if (diff < 60) return 'just now'; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; return date.toLocaleDateString(); } function showToast(message, type = 'success') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function showError(msg) { const el = document.getElementById('uploadError'); el.textContent = msg; el.style.display = 'block'; } function hideError() { document.getElementById('uploadError').style.display = 'none'; } // Close modals on overlay click document.getElementById('uploadModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) closeUploadModal(); }); document.getElementById('postModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) closePostModal(); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeFullscreen(); closePostModal(); closeUploadModal(); } });