mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
75 lines
3.8 KiB
TypeScript
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>
|
|
)
|
|
}
|