From 641c85ed563d64eca9417081d48be7b95a0713fc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 Mar 2026 14:21:43 -0500 Subject: [PATCH] Hide orphan trailing row when more pages are loading, show skeleton instead When hasNextPage is true and the last row is incomplete (not fully packed), hide it to prevent oversized straggler thumbnails. Show 2 skeleton rows as a loading placeholder. Once the next page loads, those items get properly packed with new neighbors. --- src/components/MediaGrid.tsx | 41 +++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/components/MediaGrid.tsx b/src/components/MediaGrid.tsx index a4230bbc..f3f05321 100644 --- a/src/components/MediaGrid.tsx +++ b/src/components/MediaGrid.tsx @@ -56,6 +56,12 @@ interface JustifiedRow { heightFraction: number; } +interface JustifiedLayoutResult { + rows: JustifiedRow[]; + /** True when the last row was not fully packed (trailing/orphan row). */ + lastRowIncomplete: boolean; +} + /** * Compute a justified (Google Photos–style) row layout. * Packs items into rows so each row fills the container width. @@ -71,8 +77,8 @@ function justifiedLayout( getAspectRatio: (item: T) => number, targetRowHeight: number = 0.3, maxRowItems: number = 5, -): JustifiedRow[] { - if (items.length === 0) return []; +): JustifiedLayoutResult { + if (items.length === 0) return { rows: [], lastRowIncomplete: false }; const rows: JustifiedRow[] = []; let currentRow: T[] = []; @@ -102,9 +108,10 @@ function justifiedLayout( items: currentRow, heightFraction: Math.min(rowHeightFraction, targetRowHeight), }); + return { rows, lastRowIncomplete: true }; } - return rows; + return { rows, lastRowIncomplete: false }; } // ── Media extraction ────────────────────────────────────────────────────────── @@ -391,7 +398,7 @@ export function MediaGrid({ events, className, initialOpenUrl, onInitialOpenCons }, [items]); // Compute justified row layout — fewer items per row on mobile for larger thumbnails - const rows = useMemo( + const { rows, lastRowIncomplete } = useMemo( () => justifiedLayout( items.map((item, i) => ({ item, index: i })), ({ item }) => parseDimToAspectRatio(item.dim), @@ -401,6 +408,10 @@ export function MediaGrid({ events, className, initialOpenUrl, onInitialOpenCons [items, isMobile], ); + // When more pages are coming, hide the trailing incomplete row to avoid + // oversized orphan thumbnails. Show a skeleton placeholder instead. + const visibleRows = hasNextPage && lastRowIncomplete ? rows.slice(0, -1) : rows; + // Open at initialOpenUrl if provided const initialIndex = useMemo(() => { if (!initialOpenUrl) return null; @@ -463,7 +474,7 @@ export function MediaGrid({ events, className, initialOpenUrl, onInitialOpenCons return ( <>
- {rows.map((row, rowIdx) => { + {visibleRows.map((row, rowIdx) => { // The row's aspect ratio is the sum of all item aspect ratios // (at equal height, total width = sum of ARs * height) const rowAR = row.items.reduce((s, { item }) => s + parseDimToAspectRatio(item.dim), 0); @@ -494,6 +505,26 @@ export function MediaGrid({ events, className, initialOpenUrl, onInitialOpenCons
); })} + + {/* Skeleton placeholder while next page loads */} + {hasNextPage && ( + <> + {(isMobile ? SKELETON_ROWS_MOBILE : SKELETON_ROWS_DESKTOP).slice(0, 2).map((ratios, i) => { + const rowAR = ratios.reduce((s, r) => s + r, 0); + return ( +
+ {ratios.map((ar, j) => ( + + ))} +
+ ); + })} + + )} {flatIndex !== null && (