161 lines
4.1 KiB
TypeScript
161 lines
4.1 KiB
TypeScript
"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 [screen, setScreen] = useState<{
|
||
width: number;
|
||
height: number;
|
||
dpr: number;
|
||
} | null>(null);
|
||
|
||
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]);
|
||
|
||
useEffect(() => {
|
||
const update = () => {
|
||
setScreen({
|
||
width: window.innerWidth,
|
||
height: window.innerHeight,
|
||
dpr: window.devicePixelRatio || 1,
|
||
});
|
||
};
|
||
|
||
update();
|
||
window.addEventListener("resize", update);
|
||
window.addEventListener("orientationchange", update);
|
||
return () => {
|
||
window.removeEventListener("resize", update);
|
||
window.removeEventListener("orientationchange", update);
|
||
};
|
||
}, []);
|
||
|
||
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="relative w-full">
|
||
{screen ? (
|
||
<div className="pointer-events-none absolute right-2 top-2 z-10 rounded border border-border/60 bg-background/80 px-2 py-1 text-[11px] font-mono text-muted-foreground shadow-sm">
|
||
Screen {screen.width} × {screen.height} · {screen.dpr}x
|
||
</div>
|
||
) : null}
|
||
<JustifiedGallery
|
||
items={galleryItems}
|
||
hrefFrom="portfolio"
|
||
showCaption={false}
|
||
debug={false}
|
||
targetRowHeight={160}
|
||
targetRowHeightMobile={160}
|
||
maxRowHeight={300}
|
||
maxRowItems={5}
|
||
maxRowItemsMobile={1}
|
||
gap={12}
|
||
onLoadMore={done ? undefined : () => void loadMore()}
|
||
hasMore={!done}
|
||
isLoadingMore={loading}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|