Files

169 lines
6.3 KiB
TypeScript
Raw Permalink 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 Image from "next/image";
import { useEffect, useRef, useState } from "react";
export function Gallery({ images, alt }: { images: string[]; alt: string }) {
const [open, setOpen] = useState<number | null>(null);
const touchStartX = useRef<number | null>(null);
const thumbsRef = useRef<HTMLDivElement>(null);
// Keyboard + scroll lock
useEffect(() => {
if (open === null) return;
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(null);
if (e.key === "ArrowRight") setOpen((i) => (i === null ? null : (i + 1) % images.length));
if (e.key === "ArrowLeft") setOpen((i) => (i === null ? null : (i - 1 + images.length) % images.length));
};
window.addEventListener("keydown", onKey);
return () => {
window.removeEventListener("keydown", onKey);
document.body.style.overflow = prev;
};
}, [open, images.length]);
// Scroll active thumbnail into view
useEffect(() => {
if (open === null || !thumbsRef.current) return;
const btn = thumbsRef.current.children[open] as HTMLElement | undefined;
btn?.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" });
}, [open]);
if (images.length === 0) return null;
const [hero, ...rest] = images;
const go = (dir: 1 | -1) =>
setOpen((i) => (i === null ? null : (i + dir + images.length) % images.length));
return (
<>
{/* Grid */}
<div className="grid md:grid-cols-5 gap-2 md:gap-3">
<button
onClick={() => setOpen(0)}
className="md:col-span-3 relative aspect-[4/3] overflow-hidden rounded-sm group"
>
<Image
src={hero}
alt={`${alt} — Ansicht 1`}
fill
priority
sizes="(max-width: 768px) 100vw, 60vw"
className="object-cover transition-transform duration-700 group-hover:scale-[1.02]"
/>
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<span className="bg-ink/70 text-parchment text-xs px-3 py-1.5 rounded-full backdrop-blur-sm">
Alle {images.length} Fotos
</span>
</div>
</button>
<div className="md:col-span-2 grid grid-cols-2 gap-2 md:gap-3">
{rest.slice(0, 4).map((src, idx) => (
<button
key={src}
onClick={() => setOpen(idx + 1)}
className="relative aspect-[4/3] overflow-hidden rounded-sm group"
>
<Image
src={src}
alt={`${alt} — Ansicht ${idx + 2}`}
fill
sizes="(max-width: 768px) 50vw, 25vw"
className="object-cover transition-transform duration-700 group-hover:scale-[1.03]"
/>
{idx === 3 && images.length > 5 && (
<div className="absolute inset-0 bg-ink/55 text-parchment flex items-center justify-center text-sm font-medium">
+{images.length - 5}
</div>
)}
</button>
))}
</div>
</div>
{/* Lightbox */}
{open !== null && (
<div
className="fixed inset-0 z-50 bg-ink/95 flex flex-col"
onClick={() => setOpen(null)}
role="dialog"
aria-modal="true"
onTouchStart={(e) => { touchStartX.current = e.touches[0].clientX; }}
onTouchEnd={(e) => {
if (touchStartX.current === null) return;
const dx = e.changedTouches[0].clientX - (touchStartX.current as number);
if (Math.abs(dx) > 50) go(dx < 0 ? 1 : -1);
touchStartX.current = null;
}}
>
{/* Top bar */}
<div className="shrink-0 flex items-center justify-between px-5 py-4" onClick={(e) => e.stopPropagation()}>
<span className="text-parchment/50 text-sm tabular-nums">{open + 1} / {images.length}</span>
<button
onClick={() => setOpen(null)}
className="text-parchment/70 hover:text-parchment transition-colors text-xl leading-none px-2 py-1"
aria-label="Galerie schließen"
>
</button>
</div>
{/* Image */}
<div
className="flex-1 flex items-center justify-center relative px-14 md:px-20 min-h-0"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={(e) => { e.stopPropagation(); go(-1); }}
className="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 text-parchment/60 hover:text-parchment text-5xl px-3 py-4 transition-colors select-none z-10"
aria-label="Vorheriges Bild"
></button>
<div className="relative w-full max-w-5xl aspect-[4/3]">
<Image
key={open}
src={images[open]}
alt={`${alt} — Ansicht ${open + 1}`}
fill
sizes="90vw"
className="object-contain"
/>
</div>
<button
onClick={(e) => { e.stopPropagation(); go(1); }}
className="absolute right-2 md:right-4 top-1/2 -translate-y-1/2 text-parchment/60 hover:text-parchment text-5xl px-3 py-4 transition-colors select-none z-10"
aria-label="Nächstes Bild"
></button>
</div>
{/* Thumbnail strip */}
<div
className="shrink-0 py-3 px-4"
onClick={(e) => e.stopPropagation()}
>
<div ref={thumbsRef} className="flex gap-2 overflow-x-auto" style={{ scrollbarWidth: "none" }}>
{images.map((src, idx) => (
<button
key={src}
onClick={() => setOpen(idx)}
className={`relative shrink-0 w-14 h-10 rounded overflow-hidden transition-all duration-200 ${
idx === open ? "ring-2 ring-parchment opacity-100" : "opacity-35 hover:opacity-65"
}`}
>
<Image src={src} alt="" fill sizes="56px" className="object-cover" />
</button>
))}
</div>
</div>
</div>
)}
</>
);
}