"use client"; /** * VirtualPostGrid — window-virtualised, infinite-scroll 2-up thumbnail grid for * the profile Posts tabs (/u/me and /u/[handle]). * * Mirrors HomeFeed's window virtualization, but the unit of virtualization is a * GRID ROW (two square cards) rather than a single card. Data (keyset paging + * the bottom sentinel) comes from usePostScrollFeed; this component only owns * rendering + windowing, so a profile with hundreds of posts stays cheap. */ import { useEffect, useMemo, useRef, useState } from "react"; import { useWindowVirtualizer } from "@tanstack/react-virtual"; import { Loader2 } from "lucide-react"; import type { Post } from "@/lib/social"; import { PostGridCard } from "./post-grid-card"; const COLS = 2; export function VirtualPostGrid({ posts, loading, hasMore, sentinelRef, emptyMessage, hrefFor, onDelete, onUpdated, }: { posts: Post[]; loading: boolean; hasMore: boolean; /** Bottom sentinel observed by usePostScrollFeed to load the next page. */ sentinelRef: React.RefObject; emptyMessage?: string; hrefFor?: (post: Post) => string; onDelete?: (id: string) => void; onUpdated?: (updated: Post) => void; }) { // Chunk the flat post list into rows of COLS for row-level virtualization. const rows = useMemo(() => { const out: Post[][] = []; for (let i = 0; i < posts.length; i += COLS) out.push(posts.slice(i, i + COLS)); return out; }, [posts]); // Document-absolute top of the grid, so the window virtualizer can map window // scroll → visible rows (the profile header/tabs sit above us). Re-measured // on resize and any body reflow. Same approach as HomeFeed. const parentRef = useRef(null); const [offsetTop, setOffsetTop] = useState(0); useEffect(() => { const measure = () => { const el = parentRef.current; if (el) setOffsetTop(el.getBoundingClientRect().top + window.scrollY); }; measure(); window.addEventListener("resize", measure); const ro = new ResizeObserver(measure); ro.observe(document.body); return () => { window.removeEventListener("resize", measure); ro.disconnect(); }; }, []); const virtualizer = useWindowVirtualizer({ count: rows.length, estimateSize: () => 240, // square card + gap; measureElement corrects it overscan: 6, scrollMargin: offsetTop, }); if (loading && posts.length === 0) { return (
); } if (!loading && posts.length === 0) { return (

{emptyMessage ?? "Nothing here yet."}

); } const virtualRows = virtualizer.getVirtualItems(); return (
{virtualRows.map((vr) => { const row = rows[vr.index]; return (
{row.map((p) => ( ))}
); })}
{hasMore ? (
) : null}
); }