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 = `
FEED_ERROR
`; setLoader('FEED_ERROR', false); } finally { grid.ariaBusy = 'false'; isLoading = false; } } function renderMemes(memes, options = {}) { if (!options.append && memes.length === 0) { grid.innerHTML = `
NO_MEMES_IN_STREAM
`; return; } const cards = memes.map((meme, index) => { const article = document.createElement('article'); article.className = 'meme-card'; article.dataset.memeId = meme.id; article.innerHTML = `
ID: ${shortId(meme.id)}
${relativeAge(meme.createdAt)}
Uploaded meme ${shortId(meme.id)}
${formatCount(meme.viewCount)} ${formatCount(meme.downloadCount)}
`; if (options.append && index === 0) article.tabIndex = -1; return article; }); if (options.append) { grid.append(...cards); } else { grid.replaceChildren(...cards); } } async function validateClientFile(file) { if (!file) return 'SELECT A FILE.'; if (!SAFE_TYPES.has(file.type)) return 'ONLY PNG AND JPEG ARE ACCEPTED.'; if (file.size > MAX_FILE_BYTES) return 'IMAGE EXCEEDS 5MB.'; try { const dimensions = await readImageDimensions(file); if (dimensions.width !== dimensions.height) return 'IMAGE MUST BE SQUARE.'; if (dimensions.width > 6000 || dimensions.height > 6000) return 'IMAGE EXCEEDS 6000x6000.'; } catch { return 'IMAGE COULD NOT BE INSPECTED.'; } return ''; } function readImageDimensions(file) { return new Promise((resolve, reject) => { const image = new Image(); const url = URL.createObjectURL(file); image.onload = () => { URL.revokeObjectURL(url); resolve({ width: image.naturalWidth, height: image.naturalHeight }); }; image.onerror = () => { URL.revokeObjectURL(url); reject(new Error('invalid image')); }; image.src = url; }); } function closeUpload() { uploadForm.reset(); fileLabel.textContent = 'SELECT PNG / JPEG'; formStatus.textContent = ''; uploadModal.close(); } async function recordView(id) { try { const response = await fetch(`/api/memes/${id}/view`, { method: 'POST' }); const payload = await response.json(); if (response.ok) updateCounters(payload.meme); } catch { // Live counter updates are best-effort; the lightbox should still open. } } function connectLiveCounters() { if (!('EventSource' in window)) return; const source = new EventSource('/api/events'); source.addEventListener('metric', (event) => { updateCounters(JSON.parse(event.data)); }); } function updateCounters(meme) { for (const element of document.querySelectorAll(`[data-counter-id="${meme.id}"]`)) { if (element.dataset.counterKind === 'view') { element.lastChild.textContent = formatCount(meme.viewCount); } if (element.dataset.counterKind === 'download') { element.lastChild.textContent = formatCount(meme.downloadCount); } } } function setLoader(label, spinning) { feedLoaderLabel.textContent = label; feedLoader.classList.toggle('is-spinning', spinning); } function shortId(id) { return `0x${id.slice(0, 4).toUpperCase()}...${id.slice(-4).toUpperCase()}`; } function relativeAge(value) { const elapsed = Math.max(1, Date.now() - new Date(value).getTime()); const minutes = Math.floor(elapsed / 60000); if (minutes < 60) return `${minutes}M AGO`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}H AGO`; return `${Math.floor(hours / 24)}D AGO`; } function formatCount(value) { const count = Number.isFinite(value) ? value : 0; if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`; if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`; return String(count); } function scoreBucket(value) { const score = Number.isFinite(value) ? value : 50; if (score >= 90) return 5; if (score >= 75) return 4; if (score >= 60) return 3; if (score >= 40) return 2; if (score >= 20) return 1; return 0; } async function refreshStatus() { const started = performance.now(); try { const response = await fetch('/api/status', { cache: 'no-store' }); const status = await response.json(); if (!response.ok) throw new Error('status failed'); statStatus.textContent = status.ok ? 'ONLINE' : 'DEGRADED'; statLatency.textContent = `${Math.max(1, Math.round(performance.now() - started))}MS`; statNodes.textContent = formatCount(status.liveClients); statMemes.textContent = formatCount(status.memeCount); } catch { statStatus.textContent = 'OFFLINE'; statLatency.textContent = '--MS'; statNodes.textContent = '--'; statMemes.textContent = '--'; } } function updateScrollIndicator() { const segments = [...scrollIndicator.querySelectorAll('span')]; const maxScroll = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); const progress = Math.min(1, Math.max(0, window.scrollY / maxScroll)); const activeIndex = Math.min(segments.length - 1, Math.round(progress * (segments.length - 1))); segments.forEach((segment, index) => { segment.classList.toggle('active', index === activeIndex); }); }