feat: dark cinematic hero + public memory write section
- HeroSection: animated warmgold/burgundy/indigo orbs on #060304 bg, film grain CSS overlay, larger glowing typography - WriteSection: new public component – collapsible form (name optional, title, textarea) posts to /api/memories without auth, shows success state and refreshes page - API: remove isAdmin() guard from POST /api/memories, accept author field - DB: migration adds author TEXT column to memories (try/catch for existing DBs) - Types: add author: string | null to Memory type - MemorySection: display "— [Name]" beneath date when author is set - globals.css: grain keyframe animation for film-texture overlay Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,7 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
if (!isAdmin()) {
|
const { title, content, author } = await req.json()
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title, content } = await req.json()
|
|
||||||
if (!title?.trim() || !content?.trim()) {
|
if (!title?.trim() || !content?.trim()) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Titel und Inhalt sind erforderlich' },
|
{ error: 'Titel und Inhalt sind erforderlich' },
|
||||||
@@ -34,8 +30,8 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
const result = db
|
const result = db
|
||||||
.prepare('INSERT INTO memories (title, content) VALUES (?, ?)')
|
.prepare('INSERT INTO memories (title, content, author) VALUES (?, ?, ?)')
|
||||||
.run(title.trim(), content.trim())
|
.run(title.trim(), content.trim(), author?.trim() || null)
|
||||||
const memory = db
|
const memory = db
|
||||||
.prepare('SELECT * FROM memories WHERE id = ?')
|
.prepare('SELECT * FROM memories WHERE id = ?')
|
||||||
.get(result.lastInsertRowid)
|
.get(result.lastInsertRowid)
|
||||||
|
|||||||
@@ -19,6 +19,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
/* Film grain overlay */
|
||||||
|
.grain-overlay {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E");
|
||||||
|
background-size: 200px 200px;
|
||||||
|
animation: grain 0.8s steps(1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes grain {
|
||||||
|
0% { background-position: 0 0; }
|
||||||
|
10% { background-position: -50px -30px; }
|
||||||
|
20% { background-position: 30px -60px; }
|
||||||
|
30% { background-position: -70px 20px; }
|
||||||
|
40% { background-position: 60px -10px; }
|
||||||
|
50% { background-position: -20px 50px; }
|
||||||
|
60% { background-position: 40px 30px; }
|
||||||
|
70% { background-position: -60px -40px; }
|
||||||
|
80% { background-position: 20px 60px; }
|
||||||
|
90% { background-position: -40px -20px; }
|
||||||
|
100% { background-position: 0 0; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import HeroSection from '@/components/HeroSection'
|
|||||||
import PhotoSlideshow from '@/components/PhotoSlideshow'
|
import PhotoSlideshow from '@/components/PhotoSlideshow'
|
||||||
import PhotoGallery from '@/components/PhotoGallery'
|
import PhotoGallery from '@/components/PhotoGallery'
|
||||||
import MemorySection from '@/components/MemorySection'
|
import MemorySection from '@/components/MemorySection'
|
||||||
|
import WriteSection from '@/components/WriteSection'
|
||||||
import MusicPlayer from '@/components/MusicPlayer'
|
import MusicPlayer from '@/components/MusicPlayer'
|
||||||
import VideoGallery from '@/components/VideoGallery'
|
import VideoGallery from '@/components/VideoGallery'
|
||||||
|
|
||||||
@@ -81,6 +82,9 @@ export default async function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Write section – public */}
|
||||||
|
<WriteSection />
|
||||||
|
|
||||||
{/* Memories */}
|
{/* Memories */}
|
||||||
<MemorySection memories={memories} />
|
<MemorySection memories={memories} />
|
||||||
|
|
||||||
|
|||||||
@@ -18,23 +18,69 @@ export default function HeroSection({ heroPhoto }: HeroSectionProps) {
|
|||||||
alt="Maria Malejka"
|
alt="Maria Malejka"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/40 to-black/70" />
|
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/40 to-black/80" />
|
||||||
|
{/* Grain overlay */}
|
||||||
|
<div className="absolute inset-0 grain-overlay opacity-30 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-amber-950 via-stone-900 to-stone-950">
|
<div className="absolute inset-0" style={{ backgroundColor: '#060304' }}>
|
||||||
{/* Subtle pattern */}
|
{/* Animated orbs */}
|
||||||
<div
|
<motion.div
|
||||||
className="absolute inset-0 opacity-5"
|
className="absolute rounded-full blur-[120px]"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23C4A04A' fill-opacity='1'%3E%3Cpath d='M20 20.5V18H0v5h5v5H0v5h20v-2.5c0-.83-.36-1.58-.93-2.1L20 26.5V20.5zm0-13V5H0v5h5v5H0v5h20V17.5c0-.83-.36-1.58-.93-2.1L20 13V7.5zM20 39V37H0v5h5v-5h15zM40 20.5V18H20v5h5v5h-5v5h20v-2.5c0-.83-.36-1.58-.93-2.1L40 26.5V20.5zm0-13V5H20v5h5v5h-5v5h20V17.5c0-.83-.36-1.58-.93-2.1L40 13V7.5zM40 39V37H20v5h5v-5h15z'/%3E%3C/g%3E%3C/svg%3E")`,
|
width: '70vw',
|
||||||
|
height: '70vw',
|
||||||
|
maxWidth: 900,
|
||||||
|
maxHeight: 900,
|
||||||
|
background: 'rgba(196,160,74,0.10)',
|
||||||
|
top: '-20%',
|
||||||
|
left: '-20%',
|
||||||
}}
|
}}
|
||||||
|
animate={{
|
||||||
|
x: [0, 80, -40, 0],
|
||||||
|
y: [0, 60, -30, 0],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 28, repeat: Infinity, ease: 'easeInOut' }}
|
||||||
/>
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute rounded-full blur-[140px]"
|
||||||
|
style={{
|
||||||
|
width: '60vw',
|
||||||
|
height: '60vw',
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 800,
|
||||||
|
background: 'rgba(120,30,30,0.14)',
|
||||||
|
bottom: '-15%',
|
||||||
|
right: '-15%',
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
x: [0, -60, 50, 0],
|
||||||
|
y: [0, -50, 40, 0],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 34, repeat: Infinity, ease: 'easeInOut' }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute rounded-full blur-[160px]"
|
||||||
|
style={{
|
||||||
|
width: '50vw',
|
||||||
|
height: '50vw',
|
||||||
|
maxWidth: 700,
|
||||||
|
maxHeight: 700,
|
||||||
|
background: 'rgba(50,40,90,0.10)',
|
||||||
|
top: '30%',
|
||||||
|
left: '30%',
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
x: [0, 40, -60, 0],
|
||||||
|
y: [0, -40, 50, 0],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 40, repeat: Infinity, ease: 'easeInOut' }}
|
||||||
|
/>
|
||||||
|
{/* Grain overlay */}
|
||||||
|
<div className="absolute inset-0 grain-overlay opacity-40 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Vignette */}
|
|
||||||
<div className="absolute inset-0 bg-radial-gradient pointer-events-none" />
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="relative z-10 text-center px-6 max-w-3xl mx-auto">
|
<div className="relative z-10 text-center px-6 max-w-3xl mx-auto">
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -42,36 +88,44 @@ export default function HeroSection({ heroPhoto }: HeroSectionProps) {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 1.5, ease: 'easeOut' }}
|
transition={{ duration: 1.5, ease: 'easeOut' }}
|
||||||
>
|
>
|
||||||
<p className="text-amber-200/70 text-xs tracking-[0.4em] uppercase mb-8 font-lora">
|
<p className="text-amber-200/60 text-xs tracking-[0.5em] uppercase mb-12 font-lora">
|
||||||
In liebevoller Erinnerung
|
In liebevoller Erinnerung
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 className="font-cormorant text-7xl sm:text-8xl md:text-9xl font-light text-white leading-none mb-2"
|
<h1
|
||||||
style={{ textShadow: '0 2px 40px rgba(0,0,0,0.5)' }}
|
className="font-cormorant text-8xl sm:text-9xl md:text-[11rem] font-light text-white leading-none mb-4"
|
||||||
|
style={{
|
||||||
|
textShadow:
|
||||||
|
'0 0 80px rgba(196,160,74,0.25), 0 2px 40px rgba(0,0,0,0.6)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Maria
|
Maria
|
||||||
</h1>
|
</h1>
|
||||||
<h1 className="font-cormorant text-5xl sm:text-6xl md:text-7xl font-light text-amber-200 leading-none mb-10"
|
<h1
|
||||||
style={{ textShadow: '0 2px 30px rgba(0,0,0,0.4)' }}
|
className="font-cormorant text-6xl sm:text-7xl md:text-8xl font-light text-amber-200 leading-none mb-14"
|
||||||
|
style={{
|
||||||
|
textShadow:
|
||||||
|
'0 0 60px rgba(196,160,74,0.20), 0 2px 30px rgba(0,0,0,0.5)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Malejka
|
Malejka
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-4 mb-8">
|
<div className="flex items-center justify-center gap-4 mb-10">
|
||||||
<div className="h-px w-12 bg-amber-400/30" />
|
<div className="h-px w-16 bg-amber-400/25" />
|
||||||
<span className="text-amber-400/50 text-lg">✦</span>
|
<span className="text-amber-400/40 text-lg">✦</span>
|
||||||
<div className="h-px w-12 bg-amber-400/30" />
|
<div className="h-px w-16 bg-amber-400/25" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="font-lora text-amber-100/80 text-base sm:text-lg tracking-[0.2em]">
|
<p className="font-lora text-amber-100/70 text-base sm:text-lg tracking-[0.25em]">
|
||||||
29. November 1944
|
29. November 1944
|
||||||
</p>
|
</p>
|
||||||
<p className="font-lora text-amber-100/50 text-sm tracking-[0.15em] mt-1">
|
<p className="font-lora text-amber-100/40 text-sm tracking-[0.2em] mt-2">
|
||||||
— 10. Februar 2026 —
|
— 10. Februar 2026 —
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<motion.p
|
<motion.p
|
||||||
className="mt-12 font-cormorant italic text-xl sm:text-2xl text-amber-200/70 max-w-lg mx-auto leading-relaxed"
|
className="mt-14 font-cormorant italic text-xl sm:text-2xl text-amber-200/60 max-w-lg mx-auto leading-relaxed"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 1.2, duration: 2 }}
|
transition={{ delay: 1.2, duration: 2 }}
|
||||||
@@ -85,11 +139,11 @@ export default function HeroSection({ heroPhoto }: HeroSectionProps) {
|
|||||||
|
|
||||||
{/* Scroll indicator */}
|
{/* Scroll indicator */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute bottom-8 left-1/2 -translate-x-1/2 text-amber-300/50 flex flex-col items-center gap-2"
|
className="absolute bottom-8 left-1/2 -translate-x-1/2 text-amber-300/40 flex flex-col items-center gap-2"
|
||||||
animate={{ y: [0, 8, 0] }}
|
animate={{ y: [0, 8, 0] }}
|
||||||
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut' }}
|
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut' }}
|
||||||
>
|
>
|
||||||
<span className="text-xs tracking-widest text-amber-300/40 font-lora">scroll</span>
|
<span className="text-xs tracking-widest text-amber-300/30 font-lora">scroll</span>
|
||||||
<ChevronDown size={20} />
|
<ChevronDown size={20} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export default function MemorySection({ memories }: { memories: Memory[] }) {
|
|||||||
|
|
||||||
<p className="mt-6 text-xs text-warm-brown-light font-lora">
|
<p className="mt-6 text-xs text-warm-brown-light font-lora">
|
||||||
{formatDate(memory.created_at)}
|
{formatDate(memory.created_at)}
|
||||||
|
{memory.author && (
|
||||||
|
<span className="ml-2 text-warm-brown-light/60">— {memory.author}</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</motion.article>
|
</motion.article>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { PenLine, CheckCircle } from 'lucide-react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function WriteSection() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [content, setContent] = useState('')
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!title.trim() || !content.trim()) {
|
||||||
|
setError('Bitte Titel und Text ausfüllen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError('')
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/memories', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title, content, author: name || null }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Speichern')
|
||||||
|
setDone(true)
|
||||||
|
router.refresh()
|
||||||
|
setTimeout(() => {
|
||||||
|
setDone(false)
|
||||||
|
setOpen(false)
|
||||||
|
setName('')
|
||||||
|
setTitle('')
|
||||||
|
setContent('')
|
||||||
|
}, 2800)
|
||||||
|
} catch {
|
||||||
|
setError('Etwas ist schiefgelaufen. Bitte nochmal versuchen.')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-16 px-4" style={{ background: 'rgba(6,3,4,0.96)' }}>
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{!open && !done && (
|
||||||
|
<motion.div
|
||||||
|
key="invite"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<p className="font-cormorant italic text-amber-100/60 text-2xl sm:text-3xl mb-6 leading-relaxed">
|
||||||
|
Hast du eine Erinnerung,<br />die du teilen möchtest?
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="inline-flex items-center gap-3 px-7 py-3 border border-amber-400/30 text-amber-300/70 hover:text-amber-200 hover:border-amber-400/60 font-lora text-sm tracking-widest uppercase transition-all duration-300 rounded-full"
|
||||||
|
>
|
||||||
|
<PenLine size={15} />
|
||||||
|
Schreibe ihr etwas
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{open && !done && (
|
||||||
|
<motion.div
|
||||||
|
key="form"
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.5, ease: 'easeInOut' }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2, duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="font-cormorant italic text-3xl sm:text-4xl text-amber-100/80 mb-3">
|
||||||
|
Schreibe ihr etwas
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<div className="h-px w-12 bg-amber-400/20" />
|
||||||
|
<span className="text-amber-400/30 text-lg">✦</span>
|
||||||
|
<div className="h-px w-12 bg-amber-400/20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
placeholder="Wie heißt du? (optional)"
|
||||||
|
className="w-full bg-transparent border-b border-amber-400/20 focus:border-amber-400/50 outline-none text-amber-100/60 placeholder-amber-100/25 font-lora text-sm py-2 transition-colors duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={e => setTitle(e.target.value)}
|
||||||
|
placeholder="Titel deiner Erinnerung …"
|
||||||
|
required
|
||||||
|
className="w-full bg-transparent border-b border-amber-400/20 focus:border-amber-400/50 outline-none text-amber-200/80 placeholder-amber-100/25 font-cormorant italic text-2xl py-2 transition-colors duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
value={content}
|
||||||
|
onChange={e => setContent(e.target.value)}
|
||||||
|
placeholder="Deine Worte …"
|
||||||
|
required
|
||||||
|
rows={6}
|
||||||
|
className="w-full bg-white/[0.03] border border-amber-400/15 focus:border-amber-400/40 focus:ring-1 focus:ring-amber-400/20 outline-none rounded-xl p-5 text-amber-100/70 placeholder-amber-100/20 font-cormorant italic text-lg leading-relaxed resize-none transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-red-400/70 font-lora text-sm">{error}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="text-amber-100/30 hover:text-amber-100/60 font-lora text-sm tracking-wider transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting}
|
||||||
|
className="px-8 py-2.5 bg-amber-400/10 hover:bg-amber-400/20 border border-amber-400/30 hover:border-amber-400/50 text-amber-300/80 hover:text-amber-200 font-lora text-sm tracking-widest uppercase rounded-full transition-all duration-300 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{submitting ? 'Speichern …' : 'Teilen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{done && (
|
||||||
|
<motion.div
|
||||||
|
key="done"
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="text-center py-8"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 200, damping: 15 }}
|
||||||
|
className="flex justify-center mb-5"
|
||||||
|
>
|
||||||
|
<CheckCircle size={40} className="text-amber-400/60" />
|
||||||
|
</motion.div>
|
||||||
|
<p className="font-cormorant italic text-amber-100/70 text-2xl sm:text-3xl leading-relaxed">
|
||||||
|
Danke für deine Worte.
|
||||||
|
</p>
|
||||||
|
<p className="font-lora text-amber-100/40 text-sm mt-3 tracking-wider">
|
||||||
|
Deine Erinnerung wurde gespeichert.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -46,4 +46,11 @@ function initDb(db: DatabaseSync) {
|
|||||||
created_at TEXT DEFAULT (datetime('now'))
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// Migration: add author column if missing
|
||||||
|
try {
|
||||||
|
db.exec(`ALTER TABLE memories ADD COLUMN author TEXT`)
|
||||||
|
} catch {
|
||||||
|
// Column already exists – ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type Memory = {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
|
author: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user