const PAGE_SIZE = 12; const MAX_FILE_BYTES = 5 * 1024 * 1024; const SAFE_TYPES = new Set(['image/png', 'image/jpeg']); const grid = document.querySelector('#meme-grid'); const feedLoader = document.querySelector('#feed-loader'); const feedLoaderLabel = document.querySelector('#feed-loader-label'); const uploadModal = document.querySelector('#upload-modal'); const uploadForm = document.querySelector('#upload-form'); const fileInput = document.querySelector('#meme-input'); const fileLabel = document.querySelector('#file-label'); const formStatus = document.querySelector('#form-status'); const submitUpload = document.querySelector('#submit-upload'); const lightbox = document.querySelector('#lightbox'); const lightboxImage = document.querySelector('#lightbox-image'); const lightboxDownload = document.querySelector('#lightbox-download'); const lightboxId = document.querySelector('#lightbox-id'); const scrollIndicator = document.querySelector('#scroll-indicator'); const statStatus = document.querySelector('#stat-status'); const statLatency = document.querySelector('#stat-latency'); const statNodes = document.querySelector('#stat-nodes'); const statMemes = document.querySelector('#stat-memes'); let currentPage = 0; let totalPages = 1; let isLoading = false; let latestValidationRun = 0; document.querySelector('#open-upload').addEventListener('click', () => { formStatus.textContent = ''; uploadModal.showModal(); }); document.querySelector('#close-upload').addEventListener('click', closeUpload); document.querySelector('#cancel-upload').addEventListener('click', closeUpload); document.querySelector('#close-lightbox').addEventListener('click', () => lightbox.close()); fileInput.addEventListener('change', async () => { const validationRun = ++latestValidationRun; const file = fileInput.files?.[0]; fileLabel.textContent = file ? file.name : 'SELECT PNG / JPEG'; formStatus.textContent = file ? 'INSPECTING_IMAGE...' : ''; const validationError = await validateClientFile(file); if (validationRun === latestValidationRun) { formStatus.textContent = validationError || ''; } }); uploadForm.addEventListener('submit', async (event) => { event.preventDefault(); const file = fileInput.files?.[0]; const validationError = await validateClientFile(file); if (validationError) { formStatus.textContent = validationError; return; } submitUpload.disabled = true; formStatus.textContent = 'UPLOADING...'; try { const formData = new FormData(); formData.append('meme', file); const response = await fetch('/api/memes', { method: 'POST', body: formData }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || 'Upload failed.'); if (payload.meme?.status === 'approved') { closeUpload(); await loadMemes(1, { reset: true }); } else { uploadForm.reset(); fileLabel.textContent = 'SELECT PNG / JPEG'; formStatus.textContent = `${payload.message || 'QUEUED_FOR_REVIEW'} MEME_CONSENSUS_SCORE: ${payload.meme?.moderationScore ?? '--'}`; } } catch (error) { formStatus.textContent = error.message.toUpperCase(); } finally { submitUpload.disabled = false; } }); grid.addEventListener('click', async (event) => { const viewButton = event.target.closest('[data-view-id]'); if (!viewButton) return; const card = viewButton.closest('.meme-card'); lightboxImage.src = viewButton.dataset.viewUrl; lightboxImage.alt = card.querySelector('img').alt; lightboxDownload.href = viewButton.dataset.downloadUrl; lightboxId.textContent = `ID: ${shortId(viewButton.dataset.viewId)}`; lightbox.showModal(); await recordView(viewButton.dataset.viewId); }); const observer = new IntersectionObserver((entries) => { if (entries.some((entry) => entry.isIntersecting)) { loadNextPage(); } }, { rootMargin: '420px 0px' }); connectLiveCounters(); updateScrollIndicator(); window.addEventListener('scroll', updateScrollIndicator, { passive: true }); window.addEventListener('resize', updateScrollIndicator); refreshStatus(); window.setInterval(refreshStatus, 5000); await loadMemes(1, { reset: true }); observer.observe(feedLoader); async function loadNextPage() { if (isLoading || currentPage >= totalPages) return; await loadMemes(currentPage + 1); } async function loadMemes(page, options = {}) { if (isLoading) return; isLoading = true; setLoader('FETCHING_NEXT_BLOCK...', true); try { const response = await fetch(`/api/memes?page=${page}&pageSize=${PAGE_SIZE}`); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || 'Feed error.'); currentPage = payload.page; totalPages = payload.totalPages; renderMemes(payload.memes, { append: !options.reset }); setLoader(currentPage < totalPages ? 'SCROLL_FOR_NEXT_BLOCK' : 'STREAM_SYNCHRONIZED', false); updateScrollIndicator(); } catch { if (options.reset) grid.innerHTML = `