MBO-Tech-IT-Webseite/modules/06-website-cms/files/components/GalerieAnzeige.tsx

156 lines
5.4 KiB
TypeScript

'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<T>(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<Slot[]>(() =>
bilder.slice(0, n).map(b => ({ bild: b, phase: 'idle' as const }))
)
const slotsRef = useRef<Slot[]>(slots)
useEffect(() => { slotsRef.current = slots }, [slots])
const flipping = useRef(new Set<number>())
const poolRef = useRef<GalerieBild[]>([])
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 (
<>
<style>{`
@keyframes galFlipOut {
from { transform: perspective(700px) rotateY(0deg); }
to { transform: perspective(700px) rotateY(90deg); }
}
@keyframes galFlipIn {
from { transform: perspective(700px) rotateY(-90deg); }
to { transform: perspective(700px) rotateY(0deg); }
}
.galerie-cell:hover .galerie-hl { opacity: 1 !important; }
`}</style>
{/* Mobile */}
<div className="grid grid-cols-2 gap-2 md:hidden">
{slots.map((slot, si) => (
<div key={si} className="galerie-cell relative aspect-[4/3] overflow-hidden rounded-lg">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={slot.bild.src} alt={slot.bild.alt} style={imgStyle(slot.phase)} />
<div className="galerie-hl absolute inset-0 transition-opacity duration-300"
style={{ background: 'rgba(76,175,80,0.15)', opacity: 0 }} />
</div>
))}
</div>
{/* Desktop */}
<div className="hidden md:flex flex-col" style={{ gap: GAP }}>
{Array.from({ length: numGroups }, (_, gi) => {
const bigLeft = gi % 2 === 0
return (
<div key={gi} style={{
display: 'grid',
gridTemplateColumns: bigLeft ? '2fr 1fr' : '1fr 2fr',
gridTemplateRows: `repeat(3, ${ROW_H}px)`,
gap: GAP,
}}>
{[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 (
<div key={si} className="galerie-cell relative overflow-hidden rounded-lg" style={cellStyle}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={slot.bild.src} alt={slot.bild.alt} style={imgStyle(slot.phase)} />
<div className="galerie-hl absolute inset-0 transition-opacity duration-300"
style={{ background: 'rgba(76,175,80,0.15)', opacity: 0 }} />
</div>
)
})}
</div>
)
})}
</div>
</>
)
}