'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { Upload, Plus, Edit2, Trash2, Save, X, LogOut, Image as ImageIcon, Film, Music, FileText, Eye, Loader2, Flame, User, Heart, } from 'lucide-react' type Memory = { id: number title: string content: string created_at: string updated_at: string } type Candle = { id: number name: string message: string | null created_at: string } type MediaItem = { id: number filename: string original_name: string | null type: 'photo' | 'video' | 'music' caption: string | null sort_order: number created_at: string } type TimelineEntry = { id: number year: string month: string | null day: string | null title: string description: string | null location: string | null media_filenames: string | null sort_order: number } type Recipe = { id: number title: string description: string | null ingredients: string | null instructions: string | null sort_order: number } type TimelineContribution = { id: number name: string email: string | null type: 'memory' | 'timeline' | 'media' | 'recipe' year: string | null month: string | null day: string | null title: string | null content: string | null location: string | null media_filenames: string | null status: 'pending' | 'approved' | 'rejected' | 'flagged' moderation_reason: string | null created_at: string } type FamilyUpload = { id: number filename: string original_name: string | null type: 'photo' | 'video' caption: string | null status: string created_at: string } export default function AdminPage() { const [authed, setAuthed] = useState(null) const [password, setPassword] = useState('') const [loginError, setLoginError] = useState('') const [loginLoading, setLoginLoading] = useState(false) const [memories, setMemories] = useState([]) const [candles, setCandles] = useState([]) const [photos, setPhotos] = useState([]) const [videos, setVideos] = useState([]) const [music, setMusic] = useState([]) const [timeline, setTimeline] = useState([]) const [recipes, setRecipes] = useState([]) const [familyUploads, setFamilyUploads] = useState([]) const [timelineContributions, setTimelineContributions] = useState([]) const [uploading, setUploading] = useState(false) const [uploadCaption, setUploadCaption] = useState('') const [uploadStatus, setUploadStatus] = useState('') const [dragOver, setDragOver] = useState(false) const [editingMemory, setEditingMemory] = useState(null) const [newMemory, setNewMemory] = useState({ title: '', content: '' }) const [showNewMemory, setShowNewMemory] = useState(false) const [savingMemory, setSavingMemory] = useState(false) const [editingCandle, setEditingCandle] = useState(null) const [contributionFilter, setContributionFilter] = useState<'all' | 'review' | 'approved' | 'rejected'>('review') const [editingContribution, setEditingContribution] = useState(null) const [editingTimelineEntry, setEditingTimelineEntry] = useState(null) const [editingRecipe, setEditingRecipe] = useState(null) const fileInputRef = useRef(null) const loadData = useCallback(async () => { const [memoriesRes, candlesRes, mediaRes, timelineRes, recipesRes, uploadsRes, contributionsRes] = await Promise.all([ fetch('/api/memories'), fetch('/api/candles'), fetch('/api/media'), fetch('/api/timeline'), fetch('/api/recipes'), fetch('/api/family-upload'), fetch('/api/contributions'), // New unified endpoint ]) const memoriesData = memoriesRes.ok ? await memoriesRes.json() : [] const candlesData = candlesRes.ok ? await candlesRes.json() : [] const mediaData = mediaRes.ok ? await mediaRes.json() : [] const timelineData = timelineRes.ok ? await timelineRes.json() : [] const recipesData = recipesRes.ok ? await recipesRes.json() : [] const uploadsData = uploadsRes.ok ? await uploadsRes.json() : [] const contributionsData = contributionsRes.ok ? await contributionsRes.json() : [] setMemories(Array.isArray(memoriesData) ? memoriesData : []) setCandles(Array.isArray(candlesData) ? candlesData : []) const items: MediaItem[] = Array.isArray(mediaData) ? mediaData : [] setPhotos(items.filter((m) => m.type === 'photo')) setVideos(items.filter((m) => m.type === 'video')) setMusic(items.filter((m) => m.type === 'music')) setTimeline(Array.isArray(timelineData) ? timelineData : []) setRecipes(Array.isArray(recipesData) ? recipesData : []) setFamilyUploads(Array.isArray(uploadsData) ? uploadsData : []) setTimelineContributions(Array.isArray(contributionsData) ? contributionsData : []) }, []) useEffect(() => { fetch('/api/auth') .then((r) => r.json()) .then((d) => { setAuthed(d.authed) if (d.authed) loadData() }) .catch(() => setAuthed(false)) }, [loadData]) const login = async (e: React.FormEvent) => { e.preventDefault() setLoginLoading(true) setLoginError('') try { const res = await fetch('/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }), }) if (res.ok) { setAuthed(true) loadData() } else { setLoginError('Falsches Passwort') } } finally { setLoginLoading(false) } } const logout = async () => { await fetch('/api/auth', { method: 'DELETE' }) setAuthed(false) setPassword('') } const uploadFile = async (file: File) => { const formData = new FormData() formData.append('file', file) if (uploadCaption) formData.append('caption', uploadCaption) const res = await fetch('/api/upload', { method: 'POST', body: formData }) if (!res.ok) { const err = await res.json().catch(() => ({})) setUploadStatus(`Fehler: ${err.error || 'Unbekannter Fehler'}`) return false } return true } const handleFiles = async (files: File[]) => { if (files.length === 0) return setUploading(true) setUploadStatus(`Lade ${files.length} Datei(en) hoch...`) let success = 0 for (const file of files) { const ok = await uploadFile(file) if (ok) success++ } setUploadStatus(`${success} von ${files.length} hochgeladen.`) setUploadCaption('') await loadData() setUploading(false) if (fileInputRef.current) fileInputRef.current.value = '' setTimeout(() => setUploadStatus(''), 3000) } const handleFileChange = (e: React.ChangeEvent) => { handleFiles(Array.from(e.target.files || [])) } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setDragOver(false) handleFiles(Array.from(e.dataTransfer.files)) } const deleteMedia = async (id: number) => { if (!confirm('Datei wirklich löschen?')) return await fetch(`/api/media/${id}`, { method: 'DELETE' }) loadData() } const saveMemory = async (e: React.FormEvent) => { e.preventDefault() setSavingMemory(true) try { if (editingMemory) { await fetch(`/api/memories/${editingMemory.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: editingMemory.title, content: editingMemory.content, }), }) setEditingMemory(null) } else { await fetch('/api/memories', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newMemory), }) setNewMemory({ title: '', content: '' }) setShowNewMemory(false) } loadData() } finally { setSavingMemory(false) } } const deleteMemory = async (id: number) => { if (!confirm('Erinnerung wirklich löschen?')) return await fetch(`/api/memories/${id}`, { method: 'DELETE' }) loadData() } // ── Loading state ────────────────────────────────────────────────────────── if (authed === null) { return (
) } // ── Login ───────────────────────────────────────────────────────────────── if (!authed) { return (

Verwaltung

Maria Malejka · Gedenkseite

setPassword(e.target.value)} placeholder="Passwort" className="w-full px-4 py-3 rounded-xl border border-warm-border bg-white text-warm-brown focus:outline-none focus:ring-2 focus:ring-amber-400 font-lora" autoFocus /> {loginError && (

{loginError}

)}
← Zur Gedenkseite
) } // ── Admin UI ─────────────────────────────────────────────────────────────── return (
{/* Header */}
Seite ansehen

Verwaltung

{/* ── Upload Section ───────────────────────────────────────────────── */}

Dateien hochladen

setUploadCaption(e.target.value)} placeholder="Bildunterschrift / Beschreibung (optional)" className="w-full px-4 py-2.5 rounded-xl border border-warm-border bg-white text-warm-brown text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 font-lora" />
{ e.preventDefault(); setDragOver(true) }} onDragLeave={() => setDragOver(false)} className={`border-2 border-dashed rounded-2xl p-8 text-center transition-colors ${ dragOver ? 'border-amber-500 bg-amber-50/50' : 'border-warm-border hover:border-amber-400' }`} >

Fotos, Videos oder Musik hochladen

JPG, PNG, HEIC (iPhone), MP4, MOV, MP3, M4A — mehrere auf einmal möglich

{uploadStatus && (

{uploadStatus}

)}
{/* ── Photos ──────────────────────────────────────────────────────── */}

Fotos ({photos.length})

{photos.length === 0 ? (

Noch keine Fotos hochgeladen.

) : (
{photos.map((photo) => (
{photo.caption { const newCaption = e.target.value await fetch(`/api/media/${photo.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ caption: newCaption }), }) setPhotos(photos.map(p => p.id === photo.id ? { ...p, caption: newCaption } : p)) }} placeholder="Bildunterschrift..." className="absolute bottom-0 left-0 right-0 bg-black/60 text-white text-xs px-2 py-1 rounded-b-xl opacity-0 group-hover:opacity-100 transition-opacity focus:opacity-100 outline-none" />
))}
)}
{/* ── Videos ──────────────────────────────────────────────────────── */}

Videos ({videos.length})

{videos.length === 0 ? (

Noch keine Videos hochgeladen.

) : (
{videos.map((video) => (
))}
)}
{/* ── Music ───────────────────────────────────────────────────────── */}

Musik ({music.length})

{music.length === 0 ? (

Noch keine Musik hochgeladen.

) : (
{music.map((track, i) => (
{i + 1}.

{track.original_name?.replace(/\.[^/.]+$/, '') || track.caption || track.filename}

))}
)}
{/* ── Memories ────────────────────────────────────────────────────── */}

Erinnerungen ({memories.length})

{/* New / Edit form */} {(showNewMemory || editingMemory) && (
{editingMemory ? 'Erinnerung bearbeiten' : 'Neue Erinnerung'}
editingMemory ? setEditingMemory({ ...editingMemory, title: e.target.value }) : setNewMemory({ ...newMemory, title: e.target.value }) } placeholder="Titel" required className="w-full px-4 py-2.5 rounded-xl border border-warm-border bg-white font-cormorant italic text-xl text-warm-brown focus:outline-none focus:ring-2 focus:ring-amber-400" />