since3-portal/modules/templates/catalog/shared/lightbox.tsx
2026-10-10 02:38:50 +02:00

75 lines
3.8 KiB
TypeScript

"use client"
import { AnimatePresence, motion } from "motion/react"
import { useEffect, useEffectEvent } from "react"
import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react"
import { TImage } from "./media"
// Vollbild-Ansicht für Galerien: Pfeiltasten, Escape, Klick daneben schließt
export function Lightbox({ images, index, onIndex }: {
images: { src: string; caption?: string }[]
index: number | null
onIndex: (index: number | null) => void
}) {
const count = images.length
const step = (offset: number) => index !== null && onIndex((index + offset + count) % count)
const onKey = useEffectEvent((event: KeyboardEvent) => {
if (event.key === "Escape") onIndex(null)
if (event.key === "ArrowRight") step(1)
if (event.key === "ArrowLeft") step(-1)
})
useEffect(() => {
if (index === null) return
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [index])
const image = index !== null ? images[index] : undefined
return (
<AnimatePresence>
{image && (
<motion.div
role="dialog"
aria-label="Bildansicht"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => onIndex(null)}
className="fixed inset-0 z-[90] flex items-center justify-center bg-black/90 p-4 md:p-12"
>
<AnimatePresence mode="wait">
<motion.figure
key={index}
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.3 }}
onClick={(event) => event.stopPropagation()}
className="relative h-full max-h-[85vh] w-full max-w-6xl"
>
<TImage src={image.src} alt={image.caption} sizes="100vw" className="object-contain" />
{image.caption && <figcaption className="absolute inset-x-0 -bottom-8 text-center text-sm text-white/70">{image.caption}</figcaption>}
</motion.figure>
</AnimatePresence>
<button type="button" aria-label="Schließen" onClick={() => onIndex(null)} className="absolute top-4 right-4 grid size-11 cursor-pointer place-items-center rounded-full bg-white/10 text-white hover:bg-white/20">
<XIcon className="size-5" />
</button>
{count > 1 && (
<>
<button type="button" aria-label="Vorheriges Bild" onClick={(e) => { e.stopPropagation(); step(-1) }} className="absolute left-4 grid size-12 cursor-pointer place-items-center rounded-full bg-white/10 text-white hover:bg-white/20">
<ChevronLeftIcon className="size-6" />
</button>
<button type="button" aria-label="Nächstes Bild" onClick={(e) => { e.stopPropagation(); step(1) }} className="absolute right-4 grid size-12 cursor-pointer place-items-center rounded-full bg-white/10 text-white hover:bg-white/20">
<ChevronRightIcon className="size-6" />
</button>
<p className="absolute top-6 left-6 font-mono text-sm text-white/60">{(index ?? 0) + 1} / {count}</p>
</>
)}
</motion.div>
)}
</AnimatePresence>
)
}