(() => { 'use strict'; const { view, blog, query, myBlog } = window.MICHELANGELO; // ── State ────────────────────────────────────────────────── let offset = 0; let loading = false; let exhausted = false; let postType = 'photo'; let posts = []; // all loaded posts for lightbox navigation let lightboxIndex = -1; // ── Elements ─────────────────────────────────────────────── const grid = document.getElementById('grid'); const sentinel = document.getElementById('sentinel'); const loader = document.getElementById('loader'); const endMsg = document.getElementById('end-message'); const lightbox = document.getElementById('lightbox'); const lbMedia = lightbox.querySelector('.lightbox-media'); const lbClose = lightbox.querySelector('.lightbox-close'); const lbPrev = lightbox.querySelector('.lightbox-prev'); const lbNext = lightbox.querySelector('.lightbox-next'); const lbLike = lightbox.querySelector('.btn-like'); const lbReblog = lightbox.querySelector('.btn-reblog'); const lbSource = lightbox.querySelector('.btn-source'); // ── Type filter wiring ───────────────────────────────────── document.querySelectorAll('.type-filter a').forEach(a => { a.addEventListener('click', e => { e.preventDefault(); if (a.dataset.type === postType) return; postType = a.dataset.type; document.querySelectorAll('.type-filter a').forEach(x => x.classList.remove('active')); a.classList.add('active'); reset(); }); }); // ── API fetch ────────────────────────────────────────────── async function fetchPosts() { if (loading || exhausted) return; loading = true; loader.classList.remove('hidden'); let url; if (view === 'dashboard') { url = `/api/dashboard?offset=${offset}&type=${postType}`; } else if (view === 'blog') { url = `/api/blog/${encodeURIComponent(blog)}?offset=${offset}&type=${postType}`; } else if (view === 'search') { url = `/api/search?q=${encodeURIComponent(query)}`; exhausted = true; // tagged endpoint returns one batch } try { const resp = await fetch(url); if (!resp.ok) throw new Error(await resp.text()); const data = await resp.json(); const newPosts = Array.isArray(data) ? data : (data || []); if (!newPosts.length) { exhausted = true; endMsg.classList.remove('hidden'); } else { renderPosts(newPosts); offset += newPosts.length; if (newPosts.length < 20) { exhausted = true; endMsg.classList.remove('hidden'); } } } catch (err) { console.error('Fetch error:', err); } finally { loading = false; loader.classList.add('hidden'); } } // ── Render ───────────────────────────────────────────────── function renderPosts(newPosts) { const startIndex = posts.length; posts = posts.concat(newPosts); newPosts.forEach((post, i) => { const idx = startIndex + i; const card = buildCard(post, idx); if (card) grid.appendChild(card); }); } function buildCard(post, idx) { let mediaSrc = null; let isVideo = false; let thumb = null; if (post.type === 'photo' && post.photos && post.photos.length) { mediaSrc = post.photos[0].original_size.url; } else if (post.type === 'video' && post.video_url) { mediaSrc = post.video_url; thumb = post.thumbnail_url; isVideo = true; } if (!mediaSrc) return null; const card = document.createElement('div'); card.className = 'post-card'; card.dataset.index = idx; if (isVideo) { const vid = document.createElement('video'); vid.src = mediaSrc; if (thumb) vid.poster = thumb; vid.muted = true; vid.loop = true; vid.playsInline = true; vid.addEventListener('mouseenter', () => vid.play()); vid.addEventListener('mouseleave', () => { vid.pause(); vid.currentTime = 0; }); card.appendChild(vid); } else { const img = document.createElement('img'); img.src = mediaSrc; img.loading = 'lazy'; img.alt = ''; card.appendChild(img); } // Hover meta bar const meta = document.createElement('div'); meta.className = 'post-meta'; meta.innerHTML = ` ${esc(post.blog_name)} ${fmtNotes(post.note_count)} `; card.appendChild(meta); card.addEventListener('click', () => openLightbox(idx)); return card; } // ── Lightbox ─────────────────────────────────────────────── function openLightbox(idx) { lightboxIndex = idx; renderLightbox(); lightbox.classList.remove('hidden'); document.body.style.overflow = 'hidden'; } function closeLightbox() { lightbox.classList.add('hidden'); document.body.style.overflow = ''; lbMedia.innerHTML = ''; } function renderLightbox() { const post = posts[lightboxIndex]; if (!post) return; lbMedia.innerHTML = ''; lbPrev.style.visibility = lightboxIndex > 0 ? 'visible' : 'hidden'; lbNext.style.visibility = lightboxIndex < posts.length - 1 ? 'visible' : 'hidden'; if (post.type === 'photo' && post.photos && post.photos.length) { const img = document.createElement('img'); img.src = post.photos[0].original_size.url; img.alt = ''; lbMedia.appendChild(img); } else if (post.type === 'video' && post.video_url) { const vid = document.createElement('video'); vid.src = post.video_url; if (post.thumbnail_url) vid.poster = post.thumbnail_url; vid.controls = true; vid.autoplay = true; lbMedia.appendChild(vid); } // Actions lbLike.dataset.id = post.id_string || post.id; lbLike.dataset.key = post.reblog_key; lbLike.dataset.liked = post.liked ? 'true' : 'false'; lbLike.classList.toggle('liked', !!post.liked); lbLike.textContent = post.liked ? '♥ Liked' : '♡ Like'; lbReblog.dataset.id = post.id_string || post.id; lbReblog.dataset.key = post.reblog_key; lbReblog.dataset.blog = post.blog_name; lbSource.href = post.post_url || '#'; // Prefetch next batch if near end if (lightboxIndex >= posts.length - 5) { fetchPosts(); } } lbClose.addEventListener('click', closeLightbox); lightbox.addEventListener('click', e => { if (e.target === lightbox) closeLightbox(); }); lbPrev.addEventListener('click', () => { if (lightboxIndex > 0) { lightboxIndex--; renderLightbox(); } }); lbNext.addEventListener('click', () => { if (lightboxIndex < posts.length - 1) { lightboxIndex++; renderLightbox(); } }); document.addEventListener('keydown', e => { if (lightbox.classList.contains('hidden')) return; if (e.key === 'Escape') closeLightbox(); if (e.key === 'ArrowLeft') lbPrev.click(); if (e.key === 'ArrowRight') lbNext.click(); }); // ── Like ─────────────────────────────────────────────────── lbLike.addEventListener('click', async () => { const id = lbLike.dataset.id; const key = lbLike.dataset.key; const liked = lbLike.dataset.liked === 'true'; const url = `/api/like?id=${encodeURIComponent(id)}&key=${encodeURIComponent(key)}${liked ? '&unlike=1' : ''}`; try { const resp = await fetch(url, { method: 'GET' }); if (!resp.ok) throw new Error(await resp.text()); const post = posts[lightboxIndex]; post.liked = !liked; lbLike.dataset.liked = post.liked ? 'true' : 'false'; lbLike.classList.toggle('liked', post.liked); lbLike.textContent = post.liked ? '♥ Liked' : '♡ Like'; } catch (err) { console.error('Like error:', err); } }); // ── Reblog ───────────────────────────────────────────────── lbReblog.addEventListener('click', async () => { if (!myBlog) return; const body = new URLSearchParams({ id: lbReblog.dataset.id, reblog_key: lbReblog.dataset.key, blog_name: lbReblog.dataset.blog, native_blog: myBlog, }); try { const resp = await fetch('/api/reblog', { method: 'POST', body }); if (!resp.ok) throw new Error(await resp.text()); lbReblog.textContent = '✓ Reblogged'; setTimeout(() => { lbReblog.textContent = '⇄ Reblog'; }, 2000); } catch (err) { console.error('Reblog error:', err); } }); // ── Infinite scroll via IntersectionObserver ─────────────── const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) fetchPosts(); }, { rootMargin: '400px' }); observer.observe(sentinel); // ── Reset (type filter change) ───────────────────────────── function reset() { offset = 0; loading = false; exhausted = false; posts = []; lightboxIndex = -1; grid.innerHTML = ''; endMsg.classList.add('hidden'); fetchPosts(); } // ── Blog link interception ───────────────────────────────── // Clicking a blog name in the meta overlay navigates to /blog/ grid.addEventListener('click', e => { const blogLink = e.target.closest('.blog-name'); if (blogLink && blogLink.dataset.name) { e.stopPropagation(); window.location.href = `/blog/${encodeURIComponent(blogLink.dataset.name)}`; } }); // ── Helpers ──────────────────────────────────────────────── function esc(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function fmtNotes(n) { if (!n) return ''; if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; return String(n); } // ── Init ─────────────────────────────────────────────────── fetchPosts(); })();