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>
252 lines
11 KiB
TypeScript
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>
|
|
)
|
|
}
|