"use client"; import { AnimatePresence, motion } from "motion/react"; 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 db/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 ? ( { if (e.target === e.currentTarget) close(); }} role="dialog" aria-modal="true" aria-label={active.alt} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} style={{ position: "fixed", inset: 0, zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center", padding: "1.5rem", background: "rgba(0, 0, 0, 0.82)", backdropFilter: "blur(8px)", cursor: "zoom-out", }} > {/* Clicking the dialog backdrop (not the figure) closes the lightbox. */}
{/* eslint-disable-next-line @next/next/no-img-element */} {active.alt}
{active.date} ยท {active.user}
) : null}
); }