Files
oma-memorial/src/components/PhotoSlideshow.tsx
T
denshooter a34d406375 feat: complete memorial website features
- Add user contribution system (memories, timeline entries)
- Add AI content moderation with Ollama (bad word detection + qwen3:4b)
- Add family photo/video upload with admin approval
- Add candle lighting feature
- Add timeline and recipe sections
- Add QR code page and OG image
- Add site authentication (password-protected access)
- Add proxy middleware for auth routing
- Add admin dashboard for content management
- Remove email fields, make name optional (default: Anonym)
- Add CI/CD pipeline for Gitea Actions
- Add Docker deployment configuration
- Optimize Ollama RAM usage (42GB → 2.9GB)
- Fix API routes accessibility through proxy middleware

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-18 12:20:33 +01:00

107 lines
3.4 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import type { MediaItem } from '@/lib/types'
export default function PhotoSlideshow({ photos }: { photos: MediaItem[] }) {
const [current, setCurrent] = useState(0)
const [paused, setPaused] = useState(false)
const next = useCallback(
() => setCurrent((c) => (c + 1) % photos.length),
[photos.length]
)
const prev = useCallback(
() => setCurrent((c) => (c - 1 + photos.length) % photos.length),
[photos.length]
)
useEffect(() => {
if (paused || photos.length <= 1) return
const id = setInterval(next, 5500)
return () => clearInterval(id)
}, [paused, photos.length, next])
if (photos.length === 0) return null
return (
<div
className="relative w-full overflow-hidden rounded-2xl shadow-2xl mb-10"
style={{ aspectRatio: '3/2' }}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
<AnimatePresence mode="wait">
<motion.div
key={photos[current].id}
className="absolute inset-0"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 1.4, ease: 'easeInOut' }}
>
<img
src={`/api/files/${photos[current].filename}`}
alt={photos[current].caption || 'Maria Malejka'}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent" />
{photos[current].caption && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="absolute bottom-0 inset-x-0 p-6 text-center"
>
<p className="text-white font-cormorant italic text-lg drop-shadow">
{photos[current].caption}
</p>
</motion.div>
)}
</motion.div>
</AnimatePresence>
{/* Navigation arrows */}
{photos.length > 1 && (
<>
<button
onClick={prev}
className="absolute left-3 top-1/2 -translate-y-1/2 bg-black/30 hover:bg-black/50 text-white rounded-full p-2 transition-colors z-10 backdrop-blur-sm"
aria-label="Vorheriges Bild"
>
<ChevronLeft size={20} />
</button>
<button
onClick={next}
className="absolute right-3 top-1/2 -translate-y-1/2 bg-black/30 hover:bg-black/50 text-white rounded-full p-2 transition-colors z-10 backdrop-blur-sm"
aria-label="Nächstes Bild"
>
<ChevronRight size={20} />
</button>
</>
)}
{/* Dots */}
{photos.length > 1 && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-10">
{photos.map((_, i) => (
<button
key={i}
onClick={() => setCurrent(i)}
className={`rounded-full transition-all duration-500 ${
i === current
? 'bg-amber-300 w-6 h-2'
: 'bg-white/50 w-2 h-2 hover:bg-white/80'
}`}
aria-label={`Bild ${i + 1}`}
/>
))}
</div>
)}
</div>
)
}