import { useEffect, useRef, useState } from 'react'; import { deletePost } from '../../lib/api'; const PAGE_SIZE = 9; interface CoverImage { url: string; alt: string; } interface Post { slug: string; date: string; title?: string; excerpt?: string; summary?: string; tags: string[]; draft: boolean; reading_time: number; cover_image?: CoverImage; image_count: number; } interface Props { posts: Post[]; isAdmin?: boolean; } function formatSlug(slug: string) { if (!slug) return ''; return slug .split('-') .map(w => w.charAt(0).toUpperCase() + w.slice(1)) .join(' '); } function formatYear(date: string) { return new Date(date).getFullYear(); } function formatMonth(date: string) { return new Date(date).toLocaleDateString('en-US', { month: 'short' }).toUpperCase(); } // Deterministic salon-hang layout. Each tile gets a col-span (out of 12) and an aspect ratio. // The cycle is chosen so the room reads asymmetric but balanced. const LAYOUT_CYCLE: Array<{ col: number; aspect: string; tilt: number }> = [ { col: 7, aspect: '4 / 3', tilt: -0.4 }, { col: 5, aspect: '3 / 4', tilt: 0.3 }, { col: 4, aspect: '4 / 5', tilt: -0.2 }, { col: 4, aspect: '1 / 1', tilt: 0.5 }, { col: 4, aspect: '4 / 5', tilt: -0.6 }, { col: 5, aspect: '1 / 1', tilt: 0.2 }, { col: 7, aspect: '5 / 4', tilt: -0.3 }, { col: 8, aspect: '16 / 10', tilt: 0.4 }, { col: 4, aspect: '3 / 4', tilt: -0.5 }, ]; export default function PostList({ posts: initialPosts, isAdmin = false }: Props) { const [posts, setPosts] = useState(initialPosts); const [deleting, setDeleting] = useState(null); const [visible, setVisible] = useState(() => Math.min(PAGE_SIZE, initialPosts.length)); const sentinelRef = useRef(null); async function handleDelete(slug: string, title: string) { if (deleting) return; if (!window.confirm(`Take "${title}" off the wall? This cannot be undone.`)) return; setDeleting(slug); try { await deletePost(slug); setPosts(p => p.filter(x => x.slug !== slug)); } catch (e) { window.alert(`Failed to remove: ${e instanceof Error ? e.message : 'unknown error'}`); } finally { setDeleting(null); } } useEffect(() => { setVisible(v => Math.min(Math.max(v, PAGE_SIZE), posts.length)); }, [posts.length]); useEffect(() => { if (visible >= posts.length) return; const el = sentinelRef.current; if (!el) return; const io = new IntersectionObserver( entries => { if (entries.some(e => e.isIntersecting)) { setVisible(v => Math.min(v + PAGE_SIZE, posts.length)); } }, { rootMargin: '600px 0px' }, ); io.observe(el); return () => io.disconnect(); }, [visible, posts.length]); if (posts.length === 0) { return null; } const shown = posts.slice(0, visible); const hasMore = visible < posts.length; return ( <>
{shown.map((post, idx) => { const displayTitle = post.title || formatSlug(post.slug); const isDeleting = deleting === post.slug; const layout = LAYOUT_CYCLE[idx % LAYOUT_CYCLE.length]; const hasCover = !!post.cover_image?.url; return (
{hasCover ? ( {post.cover_image!.alt ) : (
untitled
)} {post.image_count > 1 && ( {post.image_count} plates )} {post.draft && ( Sketch )}
{displayTitle}
{post.summary && (
{post.summary}
)}
{formatMonth(post.date)} {formatYear(post.date)}
{post.tags && post.tags.length > 0 && (
{post.tags.slice(0, 4).map(tag => ( {tag} ))}
)} {isAdmin && (
e.stopPropagation()} title="Edit" aria-label={`Edit ${displayTitle}`} className="p-1.5 bg-[var(--mantle)] text-[var(--rosewater)] hover:bg-[var(--blue)] border border-[var(--surface2)] transition-colors" style={{ borderRadius: 1 }} >
)}
); })}
{hasMore && ( )} ); }