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>
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user