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:
denshooter
2026-02-16 01:45:57 +01:00
parent 8b4dc2e7e6
commit 279a07e4eb
8 changed files with 303 additions and 30 deletions
+3 -7
View File
@@ -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)
+21
View File
@@ -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;
+4
View File
@@ -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() {
</section>
)}
{/* Write section public */}
<WriteSection />
{/* Memories */}
<MemorySection memories={memories} />