Files
v2.app.gaertan.art/src/components/portfolio/PortfolioGallery.tsx
2026-02-02 10:40:12 +01:00

161 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}