'use client' import { useState, useEffect, useRef } from 'react' export interface GalerieBild { id: string; src: string; alt: string } interface Slot { bild: GalerieBild; phase: 'idle' | 'out' | 'in' } const VISIBLE = 12 const INTERVAL_MS = 1500 const FLIP_MS = 360 const GAP = 8 const ROW_H = 165 function shuffle(arr: T[]): T[] { const a = [...arr] for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]] } return a } export function GalerieAnzeige({ bilder }: { bilder: GalerieBild[] }) { const n = Math.min(bilder.length, VISIBLE) // Kein shuffle im initializer → Server und Client rendern identisch → kein Hydration-Mismatch const [slots, setSlots] = useState(() => bilder.slice(0, n).map(b => ({ bild: b, phase: 'idle' as const })) ) const slotsRef = useRef(slots) useEffect(() => { slotsRef.current = slots }, [slots]) const flipping = useRef(new Set()) const poolRef = useRef([]) const poolIdxRef = useRef(0) useEffect(() => { // Läuft nur auf dem Client, nach der Hydration const shuffled = shuffle([...bilder]) poolRef.current = shuffled poolIdxRef.current = n setSlots(shuffled.slice(0, n).map(b => ({ bild: b, phase: 'idle' as const }))) if (bilder.length < 2) return const id = setInterval(() => { const cur = slotsRef.current const free = cur.map((_, i) => i).filter(i => !flipping.current.has(i)) if (free.length === 0) return const si = free[Math.floor(Math.random() * free.length)] if (bilder.length > VISIBLE) { const next = poolRef.current[poolIdxRef.current % poolRef.current.length] poolIdxRef.current++ flip(si, next) } else { const others = free.filter(i => i !== si) if (others.length === 0) return const oi = others[Math.floor(Math.random() * others.length)] flip(si, cur[oi].bild) flip(oi, cur[si].bild) } }, INTERVAL_MS) return () => clearInterval(id) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // bewusst leer: läuft genau einmal nach Mount function flip(i: number, newBild: GalerieBild) { flipping.current.add(i) setSlots(p => p.map((s, j) => j === i ? { ...s, phase: 'out' } : s)) setTimeout(() => { setSlots(p => p.map((s, j) => j === i ? { bild: newBild, phase: 'in' } : s)) setTimeout(() => { setSlots(p => p.map((s, j) => j === i ? { ...s, phase: 'idle' } : s)) flipping.current.delete(i) }, FLIP_MS) }, FLIP_MS) } const imgStyle = (phase: string): React.CSSProperties => ({ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', animation: phase === 'out' ? `galFlipOut ${FLIP_MS}ms ease-in forwards` : phase === 'in' ? `galFlipIn ${FLIP_MS}ms ease-out forwards` : 'none', }) const numGroups = Math.ceil(slots.length / 4) return ( <> {/* Mobile */}
{slots.map((slot, si) => (
{/* eslint-disable-next-line @next/next/no-img-element */} {slot.bild.alt}
))}
{/* Desktop */}
{Array.from({ length: numGroups }, (_, gi) => { const bigLeft = gi % 2 === 0 return (
{[0, 1, 2, 3].map(idx => { const si = gi * 4 + idx const slot = slots[si] if (!slot) return null const cellStyle: React.CSSProperties = bigLeft ? { gridColumn: idx === 0 ? '1' : '2', gridRow: idx === 0 ? '1/4' : `${idx}` } : { gridColumn: idx === 3 ? '2' : '1', gridRow: idx === 3 ? '1/4' : `${idx + 1}` } return (
{/* eslint-disable-next-line @next/next/no-img-element */} {slot.bild.alt}
) })}
) })}
) }