Files
oma-memorial/src/components/TimelineContributionSection.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

252 lines
11 KiB
TypeScript

'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { Calendar, CheckCircle } from 'lucide-react'
export default function TimelineContributionSection() {
const [name, setName] = useState('')
const [story, setStory] = useState('')
const [addToTimeline, setAddToTimeline] = useState(false)
const [year, setYear] = useState('')
const [month, setMonth] = useState('')
const [day, setDay] = useState('')
const [title, setTitle] = useState('')
const [submitting, setSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// No required fields - allow empty submissions
setSubmitting(true)
setError('')
try {
const res = await fetch('/api/timeline-contributions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name.trim() || 'Anonym',
year: addToTimeline ? (year.trim() || null) : null,
month: addToTimeline ? (month.trim() || null) : null,
day: addToTimeline ? (day.trim() || null) : null,
title: addToTimeline ? (title.trim() || 'Erinnerung') : 'Erinnerung',
story: story.trim() || '',
}),
})
if (res.ok) {
setSuccess(true)
setName('')
setYear('')
setMonth('')
setDay('')
setTitle('')
setStory('')
setAddToTimeline(false)
setTimeout(() => setSuccess(false), 5000)
} else {
const data = await res.json()
setError(data.error || 'Fehler beim Senden')
}
} catch (err) {
setError('Netzwerkfehler. Bitte versuche es erneut.')
} finally {
setSubmitting(false)
}
}
return (
<section id="zeitstrahl-beitragen" className="py-16 sm:py-20 bg-cream">
<div className="max-w-2xl mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-10"
>
<h2 className="font-cormorant italic text-4xl sm:text-5xl text-warm-brown mb-3">
Teile deine Erinnerung
</h2>
<div className="flex items-center justify-center gap-4 mb-4">
<div className="h-px w-16 bg-warm-gold/40" />
<span className="text-warm-gold text-xl"></span>
<div className="h-px w-16 bg-warm-gold/40" />
</div>
<p className="font-lora text-warm-brown-light/60 text-sm leading-relaxed max-w-lg mx-auto">
Hast du eine besondere Erinnerung an Oma? Teile deine Geschichte mit uns.
Alle Beiträge werden von Dennis geprüft.
</p>
</motion.div>
{success && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6 bg-green-50 border border-green-200 rounded-xl p-4 flex items-start gap-3"
>
<CheckCircle className="text-green-600 flex-shrink-0 mt-0.5" size={20} />
<div>
<p className="font-lora text-green-800 font-semibold text-sm">
Vielen Dank für deine Erinnerung!
</p>
<p className="font-lora text-green-700 text-xs mt-0.5">
Dein Beitrag wird geprüft und erscheint bald auf der Seite.
</p>
</div>
</motion.div>
)}
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6 bg-red-50 border border-red-200 rounded-xl p-4"
>
<p className="font-lora text-red-800 text-sm">{error}</p>
</motion.div>
)}
<motion.form
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
onSubmit={handleSubmit}
className="bg-amber-50/50 border border-warm-border rounded-2xl p-6 sm:p-8 shadow-sm"
>
<div className="space-y-5">
{/* Name */}
<div>
<label className="block font-lora text-warm-brown text-sm mb-2">
Dein Name <span className="text-warm-brown-light/40 text-xs">(optional)</span>
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="z.B. Maria Schmidt"
className="w-full px-4 py-3 rounded-xl border border-warm-border bg-white text-warm-brown placeholder-warm-brown-light/30 focus:outline-none focus:ring-2 focus:ring-warm-gold/30 font-lora text-sm"
/>
</div>
{/* Story */}
<div>
<label className="block font-lora text-warm-brown text-sm mb-2">
Deine Erinnerung <span className="text-warm-brown-light/40 text-xs">(optional)</span>
</label>
<textarea
value={story}
onChange={(e) => setStory(e.target.value)}
placeholder="Erzähle uns deine Erinnerung an Oma..."
rows={6}
className="w-full px-4 py-3 rounded-xl border border-warm-border bg-white text-warm-brown placeholder-warm-brown-light/30 focus:outline-none focus:ring-2 focus:ring-warm-gold/30 font-lora text-sm resize-none leading-relaxed"
/>
</div>
{/* Add to timeline checkbox */}
<div className="bg-white/60 rounded-xl p-4 border border-warm-border/50">
<label className="flex items-start gap-3 cursor-pointer group">
<input
type="checkbox"
checked={addToTimeline}
onChange={(e) => setAddToTimeline(e.target.checked)}
className="mt-1 w-4 h-4 rounded border-warm-border text-warm-gold focus:ring-warm-gold/30"
/>
<div className="flex-1">
<span className="font-lora text-warm-brown text-sm font-medium group-hover:text-warm-gold transition-colors">
Als Ereignis im Zeitstrahl anzeigen
</span>
<p className="font-lora text-warm-brown-light/50 text-xs mt-0.5">
Nur ankreuzen, wenn es ein bestimmtes Ereignis war (z.B. "Omas 60. Geburtstag")
</p>
</div>
</label>
</div>
{/* Timeline fields - only shown if checkbox is checked */}
{addToTimeline && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="space-y-4 bg-amber-50 rounded-xl p-4 border border-warm-gold/20"
>
<p className="font-lora text-warm-brown text-xs font-medium">
Ergänze die Details für den Zeitstrahl:
</p>
{/* Date */}
<div>
<label className="block font-lora text-warm-brown text-sm mb-2">
Wann war das? <span className="text-warm-brown-light/40 text-xs">(optional)</span>
</label>
<div className="grid grid-cols-3 gap-3">
<input
type="text"
value={year}
onChange={(e) => setYear(e.target.value)}
placeholder="Jahr"
className="px-4 py-3 rounded-xl border border-warm-border bg-white text-warm-brown placeholder-warm-brown-light/30 focus:outline-none focus:ring-2 focus:ring-warm-gold/30 font-lora text-sm"
/>
<input
type="text"
value={month}
onChange={(e) => setMonth(e.target.value)}
placeholder="Monat"
className="px-4 py-3 rounded-xl border border-warm-border bg-white text-warm-brown placeholder-warm-brown-light/30 focus:outline-none focus:ring-2 focus:ring-warm-gold/30 font-lora text-sm"
/>
<input
type="text"
value={day}
onChange={(e) => setDay(e.target.value)}
placeholder="Tag"
className="px-4 py-3 rounded-xl border border-warm-border bg-white text-warm-brown placeholder-warm-brown-light/30 focus:outline-none focus:ring-2 focus:ring-warm-gold/30 font-lora text-sm"
/>
</div>
<p className="text-warm-brown-light/40 text-xs mt-1.5 font-lora">
Alle Felder optional
</p>
</div>
{/* Title */}
<div>
<label className="block font-lora text-warm-brown text-sm mb-2">
Ereignis-Titel <span className="text-warm-brown-light/40 text-xs">(optional)</span>
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="z.B. Omas 60. Geburtstag"
className="w-full px-4 py-3 rounded-xl border border-warm-border bg-white text-warm-brown placeholder-warm-brown-light/30 focus:outline-none focus:ring-2 focus:ring-warm-gold/30 font-lora text-sm"
/>
</div>
</motion.div>
)}
{/* Submit */}
<motion.button
type="submit"
disabled={submitting}
whileHover={{ scale: submitting ? 1 : 1.02 }}
whileTap={{ scale: submitting ? 1 : 0.98 }}
className="w-full py-4 rounded-xl bg-warm-gold hover:bg-warm-gold/90 disabled:bg-warm-brown-light/20 disabled:cursor-not-allowed text-white font-cormorant italic text-lg transition-colors shadow-sm disabled:shadow-none flex items-center justify-center gap-2"
>
<Calendar size={20} />
{submitting ? 'Wird gesendet...' : 'Erinnerung teilen'}
</motion.button>
<p className="text-center font-lora text-warm-brown-light/40 text-xs leading-relaxed">
Dein Beitrag wird von Dennis geprüft.
</p>
</div>
</motion.form>
</div>
</section>
)
}