"use client"; import { useCallback, useEffect, useState } from "react"; /** * Client-side photo gallery for the public Photos page. Renders the same * card grid as before, but each tile is now a button that opens the photo in * a full-screen lightbox overlay. The overlay is a fixed, full-viewport layer * toggled by state โ€” closes on backdrop click, on the โœ• button, or with Esc. * * Photos are pre-shaped on the server (caption strings already translated) so * this component carries no prisma/i18n dependency. */ export type LightboxPhoto = { id: string; url: string; alt: string; date: string; user: string; }; export function PhotoLightbox({ photos }: { photos: LightboxPhoto[] }) { // Index of the photo shown in the lightbox, or null when closed. const [openIndex, setOpenIndex] = useState(null); // eslint-disable-next-line security/detect-object-injection -- openIndex is numeric array index, guarded by null check const active = openIndex !== null ? photos[openIndex] : null; const close = useCallback(() => setOpenIndex(null), []); // Esc-to-close + lock background scroll while the overlay is open. useEffect(() => { if (active == null) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") close(); } document.addEventListener("keydown", onKey); const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = prevOverflow; }; }, [active, close]); return ( <>
{photos.map((p, i) => ( ))}
{active ? (
{/* Stop propagation so clicking the image itself doesn't close. */}
e.stopPropagation()} style={{ margin: 0, maxWidth: "min(92vw, 1100px)", maxHeight: "90vh", display: "flex", flexDirection: "column", alignItems: "center", gap: "0.75rem", cursor: "default", }} > {/* eslint-disable-next-line @next/next/no-img-element */} {active.alt}
{active.date} ยท {active.user}
) : null} ); }