import { useQuery } from "@tanstack/react-query"; import { useState, useEffect, useCallback, useRef } from "react"; import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react"; export interface GalleryImage { folder: string; fileName: string; thumb: string; large: string; mobile?: string; full?: string; artist?: string; } export type FocalPointMap = Record; const PAGE_SIZE = 24; export function getObjectPosition(fileName: string, focalPoints: FocalPointMap, fallback = "center 15%"): string { const fp = focalPoints[fileName]; if (fp) return `${fp.x}% ${fp.y}%`; return fallback; } function LazyImage({ src, alt, className, onClick, objectPosition }: { src: string; alt: string; className?: string; onClick?: () => void; objectPosition?: string }) { const ref = useRef(null); const [visible, setVisible] = useState(false); const [loaded, setLoaded] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setVisible(true); obs.disconnect(); } }, { rootMargin: "200px" } ); obs.observe(el); return () => obs.disconnect(); }, []); return (
{visible ? ( <> {!loaded &&
} {alt} setLoaded(true)} /> ) : (
)}
); } function Lightbox({ images, startIndex, onClose, focalPoints, }: { images: GalleryImage[]; startIndex: number; onClose: () => void; focalPoints: FocalPointMap; }) { const [index, setIndex] = useState(startIndex); const prev = useCallback(() => setIndex((i) => (i - 1 + images.length) % images.length), [images.length]); const next = useCallback(() => setIndex((i) => (i + 1) % images.length), [images.length]); useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); if (e.key === "ArrowLeft") prev(); if (e.key === "ArrowRight") next(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [onClose, prev, next]); const current = images[index]; return (
e.stopPropagation()}> {current.artist
{current.artist && ( {current.artist} )} {index + 1} / {images.length}
); } function SingleImageCarousel({ images, autoPlay = true, interval = 5000, onExpand, focalPoints, }: { images: GalleryImage[]; autoPlay?: boolean; interval?: number; onExpand?: (index: number) => void; focalPoints: FocalPointMap; }) { const [index, setIndex] = useState(0); const [paused, setPaused] = useState(false); const prev = useCallback(() => setIndex((i) => (i - 1 + images.length) % images.length), [images.length]); const next = useCallback(() => setIndex((i) => (i + 1) % images.length), [images.length]); useEffect(() => { if (!autoPlay || paused || images.length <= 1) return; const timer = setInterval(next, interval); return () => clearInterval(timer); }, [autoPlay, paused, next, interval, images.length]); if (images.length === 0) return null; const current = images[index]; return (
setPaused(true)} onMouseLeave={() => setPaused(false)} >
{current.artist
{current.artist && (
{current.artist}
)} {onExpand && ( )}
{images.slice(0, Math.min(images.length, 20)).map((_, i) => (
); } export function PhotoGalleryWidget({ reverseOrder = false }: { reverseOrder?: boolean } = {}) { const { data: rawImages, isLoading } = useQuery({ queryKey: ["/api/gallery", "widget"], queryFn: () => fetch("/api/gallery?limit=30").then((r) => r.json()), }); const { data: focalPoints } = useQuery({ queryKey: ["/api/gallery/focal-points"], }); const images = reverseOrder && rawImages ? [...rawImages].reverse() : rawImages; const [lightboxIndex, setLightboxIndex] = useState(null); if (isLoading) { return (

Fotogalerie

); } if (!images || images.length === 0) return null; return ( <>

Fotogalerie

setLightboxIndex(i)} focalPoints={focalPoints || {}} />
{lightboxIndex !== null && ( setLightboxIndex(null)} focalPoints={focalPoints || {}} /> )} ); } export default function GalleryPage() { const { data: images, isLoading } = useQuery({ queryKey: ["/api/gallery"], }); const { data: focalPoints } = useQuery({ queryKey: ["/api/gallery/focal-points"], }); const [lightboxIndex, setLightboxIndex] = useState(null); const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const sentinelRef = useRef(null); const fp = focalPoints || {}; const totalCount = images?.length || 0; const visibleImages = images?.slice(0, visibleCount) || []; const hasMore = visibleCount < totalCount; useEffect(() => { const el = sentinelRef.current; if (!el || !hasMore) return; const obs = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setVisibleCount((c) => Math.min(c + PAGE_SIZE, totalCount)); } }, { rootMargin: "400px" } ); obs.observe(el); return () => obs.disconnect(); }, [hasMore, totalCount]); return (
{isLoading ? (
{Array.from({ length: 12 }).map((_, i) => (
))}
) : images && images.length > 0 ? ( <>

{Math.min(visibleCount, totalCount)} von {totalCount} Fotos

{visibleImages.map((img, i) => ( ))}
{hasMore && (
Weitere Fotos werden geladen...
)} ) : (

Keine Fotos vorhanden

)} {lightboxIndex !== null && images && ( setLightboxIndex(null)} focalPoints={fp} /> )}
); }