feat: persönliche Gedenkseite – Tribute, Autoplay-Musik, Next.js 16 Fixes

- TributeSection: zwei Perspektiven (Familie + Dennis), emotional und persönlich
- MusicPlayer: Autoplay mit stummem Start + Fade-In bei Interaktion, nahtloser
  Crossfade-Loop (überspringt stille letzten 10s), kompakter Mute-Button
- Alle API-Routes: export const runtime = 'nodejs' für node:sqlite in Next.js 16
- Admin: defensive res.ok Checks vor .json() Parsing
- DB: mkdirSync erst zur Laufzeit, path.resolve für DATA_DIR
- page.tsx: plain() Helper für null-prototype SQLite-Rows
- erinnerungen.md: alle Familieninfos und Erinnerungen dokumentiert
- Nav: Musik-Tab entfernt, "Über Oma" hinzugefügt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-02-16 03:48:46 +01:00
parent 313b5ff7fd
commit 4d56d4904a
17 changed files with 1121 additions and 600 deletions
+25 -16
View File
@@ -7,24 +7,30 @@ import MemorySection from '@/components/MemorySection'
import WriteSection from '@/components/WriteSection'
import MusicPlayer from '@/components/MusicPlayer'
import VideoGallery from '@/components/VideoGallery'
import TributeSection from '@/components/TributeSection'
export const dynamic = 'force-dynamic'
// node:sqlite returns null-prototype objects; convert to plain objects for Client Components
function plain<T>(rows: unknown[]): T[] {
return JSON.parse(JSON.stringify(rows))
}
export default async function HomePage() {
const db = getDb()
const photos = db
.prepare("SELECT * FROM media WHERE type = 'photo' ORDER BY sort_order, created_at")
.all() as MediaItem[]
const videos = db
.prepare("SELECT * FROM media WHERE type = 'video' ORDER BY sort_order, created_at")
.all() as MediaItem[]
const music = db
.prepare("SELECT * FROM media WHERE type = 'music' ORDER BY sort_order, created_at")
.all() as MediaItem[]
const memories = db
.prepare('SELECT * FROM memories ORDER BY created_at DESC')
.all() as Memory[]
const photos = plain<MediaItem>(
db.prepare("SELECT * FROM media WHERE type = 'photo' ORDER BY sort_order, created_at").all()
)
const videos = plain<MediaItem>(
db.prepare("SELECT * FROM media WHERE type = 'video' ORDER BY sort_order, created_at").all()
)
const music = plain<MediaItem>(
db.prepare("SELECT * FROM media WHERE type = 'music' ORDER BY sort_order, created_at").all()
)
const memories = plain<Memory>(
db.prepare('SELECT * FROM memories ORDER BY created_at DESC').all()
)
return (
<main className="min-h-screen bg-cream">
@@ -34,6 +40,9 @@ export default async function HomePage() {
{/* Navigation */}
<nav className="sticky top-0 z-20 bg-cream/80 backdrop-blur-sm border-b border-warm-border">
<div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-center gap-6 sm:gap-10">
<a href="#ueber-oma" className="text-warm-brown-light hover:text-warm-gold text-sm font-cormorant italic transition-colors">
Über Oma
</a>
{photos.length > 0 && (
<a href="#bilder" className="text-warm-brown-light hover:text-warm-gold text-sm font-cormorant italic transition-colors">
Bilder
@@ -47,12 +56,12 @@ export default async function HomePage() {
Videos
</a>
)}
<a href="#musik" className="text-warm-brown-light hover:text-warm-gold text-sm font-cormorant italic transition-colors">
Musik
</a>
</div>
</nav>
{/* Personal tribute */}
<TributeSection />
{/* Photos */}
{photos.length > 0 && (
<section id="bilder" className="py-16 sm:py-20">
@@ -84,7 +93,7 @@ export default async function HomePage() {
{/* Videos */}
<VideoGallery videos={videos} />
{/* Music always rendered (ambient fallback when no tracks) */}
{/* Floating music player */}
<MusicPlayer tracks={music} />
{/* Footer */}