a34d406375
- 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>
107 lines
3.4 KiB
TypeScript
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>
|
|
)
|
|
}
|