Unify portfolio and animal studies galleries

This commit is contained in:
2026-01-31 01:18:46 +01:00
parent 0de3eed5f1
commit 96efd4c942
17 changed files with 800 additions and 528 deletions

View File

@ -0,0 +1,131 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type {
Cursor,
PortfolioArtworkItem,
PortfolioFilters,
} from "@/actions/portfolio/getPortfolioArtworksPage";
import { getPortfolioArtworksPage } from "@/actions/portfolio/getPortfolioArtworksPage";
import JustifiedGallery, {
type JustifiedGalleryItem,
} from "@/components/gallery/JustifiedGallery";
export default function PortfolioGallery({
filters,
}: {
filters: PortfolioFilters;
}) {
const { year, albumId, q } = filters;
const queryFilters = useMemo<PortfolioFilters>(
() => ({ year, albumId, q }),
[year, albumId, q]
);
const resetKey = useMemo(
() => `${year ?? ""}|${albumId ?? ""}|${q ?? ""}`,
[year, albumId, q]
);
const [items, setItems] = useState<PortfolioArtworkItem[]>([]);
const [done, setDone] = useState(false);
const [loading, setLoading] = useState(false);
const inFlight = useRef(false);
const doneRef = useRef(false);
doneRef.current = done;
const cursorRef = useRef<Cursor>(null);
useEffect(() => {
if (resetKey == null) return;
setItems([]);
setDone(false);
doneRef.current = false;
inFlight.current = false;
cursorRef.current = null;
}, [resetKey]);
const loadMore = useCallback(async () => {
if (inFlight.current || doneRef.current) return 0;
inFlight.current = true;
setLoading(true);
try {
const data = await getPortfolioArtworksPage({
take: 60,
cursor: cursorRef.current,
filters: queryFilters,
onlyPublished: true,
});
// Defensive dedupe
setItems((prev) => {
const seen = new Set(prev.map((x) => x.id));
const next = data.items.filter((x) => !seen.has(x.id));
return prev.concat(next);
});
cursorRef.current = data.nextCursor;
if (!data.nextCursor) setDone(true);
return data.items.length;
} finally {
setLoading(false);
inFlight.current = false;
}
}, [queryFilters]);
useEffect(() => {
void loadMore();
}, [loadMore]);
const galleryItems: JustifiedGalleryItem[] = items.map((it) => ({
id: it.id,
name: it.name,
altText: it.altText,
fileKey: it.fileKey,
width: it.thumbW,
height: it.thumbH,
dominantHex: it.dominantHex,
}));
useEffect(() => {
if (items.length === 0) return;
// Debug: inspect dominantHex values coming from the server.
console.log(
"[PortfolioGallery] dominantHex sample",
items.slice(0, 5).map((it) => ({
id: it.id,
dominantHex: it.dominantHex,
}))
);
}, [items]);
if (!loading && done && galleryItems.length === 0) {
return (
<p className="text-muted-foreground text-center py-20">
No artworks to display
</p>
);
}
return (
<div className="w-full">
<JustifiedGallery
items={galleryItems}
hrefFrom="portfolio"
showCaption={false}
targetRowHeight={160}
targetRowHeightMobile={160}
maxRowHeight={300}
maxRowItems={5}
maxRowItemsMobile={1}
gap={12}
onLoadMore={done ? undefined : () => void loadMore()}
hasMore={!done}
isLoadingMore={loading}
/>
</div>
);
}