// ===== State ===== let currentSort = 'new'; let currentPosts = []; let currentPostId = null; let selectedFile = null; // ===== Initialize ===== document.addEventListener('DOMContentLoaded', () => { loadPosts(); setupDragDrop(); setupPasteUpload(); }); // ===== 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, index) => `
${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.getElementById('uploadPrompt').style.display = ''; document.getElementById('uploadProcessing').style.display = 'none'; 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', (e) => { // Don't trigger file picker if clicking the remove button if (e.target.closest('.btn-remove')) return; input.click(); }); zone.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); zone.classList.add('dragover'); }); zone.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); zone.classList.remove('dragover'); }); zone.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); 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.getElementById('uploadPrompt').style.display = 'none'; }; reader.readAsDataURL(file); } function removePreview() { selectedFile = null; document.getElementById('uploadPreview').style.display = 'none'; document.getElementById('uploadPrompt').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...'; // Show processing state in upload zone const processing = document.getElementById('uploadProcessing'); const prompt = document.getElementById('uploadPrompt'); const preview = document.getElementById('uploadPreview'); processing.style.display = 'block'; prompt.style.display = 'none'; preview.style.display = 'none'; try { if (selectedFile) { const formData = new FormData(); formData.append('image', selectedFile); formData.append('title', title || 'Untitled'); formData.append('tags', tags); formData.append('author', author); const newPost = await apiPost('/api/upload', formData); showToast('Post uploaded successfully! 🎉', 'success'); closeUploadModal(); await loadPosts(); // Scroll to and highlight the new post highlightNewPost(newPost.id); } } catch (err) { showError(err.message); showToast(err.message, 'error'); } finally { btn.disabled = false; btn.textContent = 'Upload Post'; processing.style.display = 'none'; if (selectedFile) { preview.style.display = 'block'; } else { prompt.style.display = ''; } } } // ===== Paste Upload ===== function setupPasteUpload() { document.addEventListener('paste', (e) => { // Ignore if a modal is open and user is typing in an input const activeModal = document.querySelector('.modal-overlay.active'); if (activeModal) { const activeElement = document.activeElement; if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) { return; } } const items = e.clipboardData && e.clipboardData.items; if (!items) return; let imageBlob = null; let imageType = ''; for (let i = 0; i < items.length; i++) { if (items[i].type.startsWith('image/')) { imageBlob = items[i].getAsFile(); imageType = items[i].type; break; } } if (!imageBlob) return; // Validate type const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!allowed.includes(imageType)) { showToast(`Unsupported paste type: ${imageType}`, 'error'); return; } // Validate size if (imageBlob.size > 20 * 1024 * 1024) { showToast('Pasted image too large. Maximum size is 20MB.', 'error'); return; } e.preventDefault(); handlePasteUpload(imageBlob); }); } async function handlePasteUpload(blob) { // Create a File from the blob with a generated name const ext = blob.type.split('/')[1] || 'png'; const fileName = `pasted-${Date.now()}.${ext}`; const file = new File([blob], fileName, { type: blob.type }); // Open the upload modal and set the file openUploadModal(); 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.getElementById('uploadPrompt').style.display = 'none'; }; reader.readAsDataURL(file); // Auto-submit after a brief delay so user sees the preview setTimeout(async () => { const btn = document.getElementById('submitBtn'); btn.disabled = true; btn.textContent = 'Uploading...'; const processing = document.getElementById('uploadProcessing'); const prompt = document.getElementById('uploadPrompt'); const preview = document.getElementById('uploadPreview'); processing.style.display = 'block'; prompt.style.display = 'none'; preview.style.display = 'none'; try { const formData = new FormData(); formData.append('image', file); formData.append('title', 'Untitled'); formData.append('tags', ''); formData.append('author', 'Anonymous'); const newPost = await apiPost('/api/upload', formData); showToast('Image pasted & uploaded! 🎉', 'success'); closeUploadModal(); await loadPosts(); highlightNewPost(newPost.id); } catch (err) { showToast(`Paste upload failed: ${err.message}`, 'error'); // Re-show preview so user can retry processing.style.display = 'none'; preview.style.display = 'block'; } finally { btn.disabled = false; btn.textContent = 'Upload Post'; processing.style.display = 'none'; } }, 600); } // ===== Highlight New Post ===== function highlightNewPost(postId) { // Wait a tick for the DOM to update requestAnimationFrame(() => { const cards = document.querySelectorAll('.post-card'); for (const card of cards) { if (card.getAttribute('onclick') && card.getAttribute('onclick').includes(postId)) { card.classList.add('highlight-new'); card.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => card.classList.remove('highlight-new'), 2500); break; } } }); } // ===== 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.style.transition = 'opacity 200ms ease, transform 200ms ease'; toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; setTimeout(() => toast.remove(), 200); }, 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(); } });