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:
@@ -64,10 +64,8 @@ export default function AdminPage() {
|
||||
fetch('/api/memories'),
|
||||
fetch('/api/media'),
|
||||
])
|
||||
const [memoriesData, mediaData] = await Promise.all([
|
||||
memoriesRes.json(),
|
||||
mediaRes.json(),
|
||||
])
|
||||
const memoriesData = memoriesRes.ok ? await memoriesRes.json() : []
|
||||
const mediaData = mediaRes.ok ? await mediaRes.json() : []
|
||||
setMemories(Array.isArray(memoriesData) ? memoriesData : [])
|
||||
const items: MediaItem[] = Array.isArray(mediaData) ? mediaData : []
|
||||
setPhotos(items.filter((m) => m.type === 'photo'))
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { createHash } from 'crypto'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
function getExpectedToken() {
|
||||
return createHash('sha256')
|
||||
.update(process.env.ADMIN_PASSWORD || 'change-me')
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'path'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data')
|
||||
const DATA_DIR = path.resolve(process.cwd(), process.env.DATA_DIR || 'data')
|
||||
|
||||
const MIME: Record<string, string> = {
|
||||
'.jpg': 'image/jpeg',
|
||||
|
||||
@@ -6,6 +6,8 @@ import { unlink } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
async function isAdmin() {
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get('admin_auth')?.value
|
||||
@@ -15,7 +17,7 @@ async function isAdmin() {
|
||||
return token === expected
|
||||
}
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data')
|
||||
const DATA_DIR = path.resolve(process.cwd(), process.env.DATA_DIR || 'data')
|
||||
|
||||
export async function DELETE(
|
||||
_req: NextRequest,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getDb } from '@/lib/db'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const type = req.nextUrl.searchParams.get('type')
|
||||
const db = getDb()
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getDb } from '@/lib/db'
|
||||
import { cookies } from 'next/headers'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
async function isAdmin() {
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get('admin_auth')?.value
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getDb } from '@/lib/db'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET() {
|
||||
const db = getDb()
|
||||
const memories = db
|
||||
|
||||
@@ -17,7 +17,7 @@ async function isAdmin() {
|
||||
return token === expected
|
||||
}
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data')
|
||||
const DATA_DIR = path.resolve(process.cwd(), process.env.DATA_DIR || 'data')
|
||||
|
||||
const MIME_TO_FOLDER: Record<string, string> = {
|
||||
'image/jpeg': 'photos',
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function ImpressumPage() {
|
||||
Angaben gemäß § 5 TMG
|
||||
</h2>
|
||||
<address className="not-italic space-y-0.5">
|
||||
<p className="font-medium text-warm-brown/90">Dennis Malejka</p>
|
||||
<p className="font-medium text-warm-brown/90">Dennis Konkol</p>
|
||||
<p className="text-warm-brown-light text-xs italic">
|
||||
(Kontaktdaten auf Anfrage)
|
||||
</p>
|
||||
@@ -99,7 +99,7 @@ export default function ImpressumPage() {
|
||||
</h2>
|
||||
<p>
|
||||
Alle auf dieser Seite veröffentlichten Fotos und Medien sind privates
|
||||
Eigentum der Familie Malejka. Eine Weitergabe oder Veröffentlichung ohne
|
||||
Eigentum der Familie. Eine Weitergabe oder Veröffentlichung ohne
|
||||
ausdrückliche Genehmigung ist nicht gestattet.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
+25
-16
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user