Unify portfolio and animal studies galleries
This commit is contained in:
131
src/components/portfolio/PortfolioGallery.tsx
Normal file
131
src/components/portfolio/PortfolioGallery.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user