From 279a07e4eb82c5b5ba3140528c0eb16521eeebf3 Mon Sep 17 00:00:00 2001 From: denshooter Date: Mon, 16 Feb 2026 01:45:57 +0100 Subject: [PATCH] feat: dark cinematic hero + public memory write section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app/api/memories/route.ts | 10 +- src/app/globals.css | 21 ++++ src/app/page.tsx | 4 + src/components/HeroSection.tsx | 100 +++++++++++++---- src/components/MemorySection.tsx | 3 + src/components/WriteSection.tsx | 187 +++++++++++++++++++++++++++++++ src/lib/db.ts | 7 ++ src/lib/types.ts | 1 + 8 files changed, 303 insertions(+), 30 deletions(-) create mode 100644 src/components/WriteSection.tsx diff --git a/src/app/api/memories/route.ts b/src/app/api/memories/route.ts index 51b6d49..280171b 100644 --- a/src/app/api/memories/route.ts +++ b/src/app/api/memories/route.ts @@ -20,11 +20,7 @@ export async function GET() { } export async function POST(req: NextRequest) { - if (!isAdmin()) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { title, content } = await req.json() + const { title, content, author } = await req.json() if (!title?.trim() || !content?.trim()) { return NextResponse.json( { error: 'Titel und Inhalt sind erforderlich' }, @@ -34,8 +30,8 @@ export async function POST(req: NextRequest) { const db = getDb() const result = db - .prepare('INSERT INTO memories (title, content) VALUES (?, ?)') - .run(title.trim(), content.trim()) + .prepare('INSERT INTO memories (title, content, author) VALUES (?, ?, ?)') + .run(title.trim(), content.trim(), author?.trim() || null) const memory = db .prepare('SELECT * FROM memories WHERE id = ?') .get(result.lastInsertRowid) diff --git a/src/app/globals.css b/src/app/globals.css index 7984efe..a93c326 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -19,6 +19,27 @@ } @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 */ ::-webkit-scrollbar { width: 5px; diff --git a/src/app/page.tsx b/src/app/page.tsx index 16295b2..67dcdc0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,7 @@ import HeroSection from '@/components/HeroSection' import PhotoSlideshow from '@/components/PhotoSlideshow' import PhotoGallery from '@/components/PhotoGallery' import MemorySection from '@/components/MemorySection' +import WriteSection from '@/components/WriteSection' import MusicPlayer from '@/components/MusicPlayer' import VideoGallery from '@/components/VideoGallery' @@ -81,6 +82,9 @@ export default async function HomePage() { )} + {/* Write section – public */} + + {/* Memories */} diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index fd96573..497f5e0 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -18,23 +18,69 @@ export default function HeroSection({ heroPhoto }: HeroSectionProps) { alt="Maria Malejka" className="w-full h-full object-cover" /> -
+
+ {/* Grain overlay */} +
) : ( -
- {/* Subtle pattern */} -
+ {/* Animated orbs */} + + + + {/* Grain overlay */} +
)} - {/* Vignette */} -
- {/* Content */}
-

+

In liebevoller Erinnerung

-

Maria

-

Malejka

-
-
- -
+
+
+ +
-

+

29. November 1944

-

+

— 10. Februar 2026 —

- scroll + scroll diff --git a/src/components/MemorySection.tsx b/src/components/MemorySection.tsx index 4886e29..d74a5fb 100644 --- a/src/components/MemorySection.tsx +++ b/src/components/MemorySection.tsx @@ -61,6 +61,9 @@ export default function MemorySection({ memories }: { memories: Memory[] }) {

{formatDate(memory.created_at)} + {memory.author && ( + — {memory.author} + )}

))} diff --git a/src/components/WriteSection.tsx b/src/components/WriteSection.tsx new file mode 100644 index 0000000..3c3459a --- /dev/null +++ b/src/components/WriteSection.tsx @@ -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 ( +
+
+ + {!open && !done && ( + +

+ Hast du eine Erinnerung,
die du teilen möchtest? +

+ +
+ )} + + {open && !done && ( + + +
+

+ Schreibe ihr etwas +

+
+
+ +
+
+
+ +
+
+ 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" + /> +
+ +
+ 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" + /> +
+ +
+