feat: Musik global, Dennis-Teil auf eigene Seite, Mobile-Autoplay
- MusicPlayer ins Layout verschoben (läuft auf allen Seiten) - Mobile-Autoplay: Desktop startet stumm + fade-in bei Scroll, Mobile wartet auf ersten Touch und startet dann mit Fade-In - Dennis-Perspektive auf eigene Seite /meine-oma ausgelagert, dezenter Link "Von Dennis" am Ende der Tribute-Sektion - "Berge von Essen" entfernt - "Jacky und Niklas" → "ihre Enkelin" / "Meine Schwester" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Cormorant_Garamond, Lora } from 'next/font/google'
|
import { Cormorant_Garamond, Lora } from 'next/font/google'
|
||||||
|
import GlobalMusicPlayer from '@/components/GlobalMusicPlayer'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
const cormorant = Cormorant_Garamond({
|
const cormorant = Cormorant_Garamond({
|
||||||
@@ -18,6 +19,8 @@ const lora = Lora({
|
|||||||
display: 'swap',
|
display: 'swap',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'In Erinnerung an Maria Malejka',
|
title: 'In Erinnerung an Maria Malejka',
|
||||||
description:
|
description:
|
||||||
@@ -36,7 +39,10 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="de" className={`${cormorant.variable} ${lora.variable}`}>
|
<html lang="de" className={`${cormorant.variable} ${lora.variable}`}>
|
||||||
<body className="font-lora antialiased">{children}</body>
|
<body className="font-lora antialiased">
|
||||||
|
{children}
|
||||||
|
<GlobalMusicPlayer />
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ArrowLeft } from 'lucide-react'
|
||||||
|
|
||||||
|
const fade = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
whileInView: { opacity: 1, y: 0 },
|
||||||
|
viewport: { once: true },
|
||||||
|
transition: { duration: 0.8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MeineOmaPage() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-cream">
|
||||||
|
<div className="max-w-2xl mx-auto px-6 py-20 sm:py-28">
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center gap-1.5 text-warm-brown-light/50 hover:text-warm-gold text-sm font-lora transition-colors mb-16"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={14} />
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<motion.div {...fade} className="text-center mb-16">
|
||||||
|
<p className="text-warm-gold/50 text-xs tracking-[0.5em] uppercase font-lora mb-4">
|
||||||
|
Von Dennis
|
||||||
|
</p>
|
||||||
|
<h1 className="font-cormorant italic text-5xl sm:text-6xl text-warm-brown mb-4">
|
||||||
|
Meine Oma
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<div className="h-px w-16 bg-warm-gold/30" />
|
||||||
|
<span className="text-warm-gold/40 text-lg">✽</span>
|
||||||
|
<div className="h-px w-16 bg-warm-gold/30" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="space-y-8 font-lora text-warm-brown/80 text-base sm:text-lg leading-relaxed">
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Ich kam aus der Schule und das Essen stand schon da. Jedes Mal.
|
||||||
|
Rahmsauce. Ich weiß nicht, wie oft ich die gegessen habe, aber
|
||||||
|
es war nie genug. Ich würde alles dafür geben, noch einmal an
|
||||||
|
ihrem Küchentisch zu sitzen.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Oma roch nach Oma. Ich weiß nicht, wie ich das anders beschreiben
|
||||||
|
soll. Nicht nach Parfum. Nicht nach irgendwas, das man kaufen kann.
|
||||||
|
Einfach nach ihr. Wenn ich an sie denke, ist das Erste, was
|
||||||
|
kommt, dieses Gefühl. Diese Wärme. Der Geruch von ihrem Haus.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Bei ihr war es immer heiß. Immer. Der Kamin lief, die Heizung
|
||||||
|
lief, man hat geschwitzt und es war trotzdem schön. An Weihnachten
|
||||||
|
war der Karpfen in der Badewanne und die Geschenke im Wohnzimmer.
|
||||||
|
Aber die Tür blieb zu. Erst essen, dann Geschenke. Das war Gesetz.
|
||||||
|
Und wir haben uns jedes Mal gefreut, als wären wir fünf.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Wenn Pico bei ihr war, hab ich nicht mehr existiert. Sie hat mit
|
||||||
|
ihm geredet, ihn gefüttert, ihn verwöhnt. Ich saß daneben und
|
||||||
|
war Luft. Aber das war okay. Weil sie so glücklich war dabei.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Pico ist letztes Jahr gestorben. Und jetzt Oma. Ich stelle mir
|
||||||
|
vor, wie sie irgendwo sitzt und er neben ihr liegt und sie ihm
|
||||||
|
wieder irgendwas erzählt, was er nicht versteht. Und er hört
|
||||||
|
trotzdem zu.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Sie war stur. Richtig stur. Deswegen hatte sie auch den
|
||||||
|
Oberschenkelhalsbruch. Weil sie alles alleine machen wollte.
|
||||||
|
Weil sie sich nichts sagen lassen hat. Man konnte sich aufregen.
|
||||||
|
Aber im Nachhinein war das auch das, was sie so stark gemacht hat.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
{...fade}
|
||||||
|
className="border-l-2 border-warm-gold/30 pl-6 py-2 my-12"
|
||||||
|
>
|
||||||
|
<p className="font-cormorant italic text-2xl sm:text-3xl text-warm-brown/70 leading-snug">
|
||||||
|
Ich habe es dir viel zu selten gesagt, Oma.
|
||||||
|
<br />
|
||||||
|
Aber ich habe dich geliebt. Sehr.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Das letzte Mal hab ich sie im Krankenhaus besucht. In der
|
||||||
|
Geriatrie. Sie lief schon wieder, es ging ihr gut. Sie hat
|
||||||
|
im Aufenthaltsraum Blumenvasen gesehen und meinte, die könnte
|
||||||
|
man ja mitnehmen. Ich hab gelacht. So war sie halt.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Ihr Zimmer war gegenüber von dem Zimmer, in dem meine Mutter
|
||||||
|
wochenlang gelegen hatte, als sie mit mir schwanger war. Mama
|
||||||
|
durfte sich kaum bewegen damals. Im selben Flur, in dem ich auf
|
||||||
|
die Welt kam, ist Oma gegangen. Das lässt mich nicht los.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Am 10. Februar, um 13:13 Uhr, schrieb mir meine Schwester
|
||||||
|
auf Telegram: <span className="italic text-warm-brown">„Sie
|
||||||
|
möchten die Reanimation abbrechen."</span>
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Meine Schwester ist schwanger. Sie hat den Ultraschall mit in
|
||||||
|
den Sarg gelegt. Ein Kind, das Oma nie treffen wird, aber das
|
||||||
|
in ihrem Haus aufwachsen wird. In den Wänden, die nach Oma
|
||||||
|
riechen. In der Küche, in der die Rahmsauce stand.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p {...fade}>
|
||||||
|
Diese Seite ist für dich. Damit ich nicht vergesse. Damit
|
||||||
|
niemand vergisst.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<motion.div
|
||||||
|
{...fade}
|
||||||
|
className="mt-20 text-center"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-4 mb-8">
|
||||||
|
<div className="h-px w-12 bg-warm-gold/20" />
|
||||||
|
<span className="text-warm-gold/30">✽</span>
|
||||||
|
<div className="h-px w-12 bg-warm-gold/20" />
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
className="font-lora text-warm-brown-light/40 hover:text-warm-gold text-sm transition-colors"
|
||||||
|
>
|
||||||
|
Zurück zur Gedenkseite
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-8
@@ -5,7 +5,6 @@ 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 WriteSection from '@/components/WriteSection'
|
||||||
import MusicPlayer from '@/components/MusicPlayer'
|
|
||||||
import VideoGallery from '@/components/VideoGallery'
|
import VideoGallery from '@/components/VideoGallery'
|
||||||
import TributeSection from '@/components/TributeSection'
|
import TributeSection from '@/components/TributeSection'
|
||||||
|
|
||||||
@@ -25,10 +24,7 @@ export default async function HomePage() {
|
|||||||
const videos = plain<MediaItem>(
|
const videos = plain<MediaItem>(
|
||||||
db.prepare("SELECT * FROM media WHERE type = 'video' ORDER BY sort_order, created_at").all()
|
db.prepare("SELECT * FROM media WHERE type = 'video' ORDER BY sort_order, created_at").all()
|
||||||
)
|
)
|
||||||
const music = plain<MediaItem>(
|
const memories = plain<Memory>(
|
||||||
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()
|
db.prepare('SELECT * FROM memories ORDER BY created_at DESC').all()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,9 +89,6 @@ export default async function HomePage() {
|
|||||||
{/* Videos */}
|
{/* Videos */}
|
||||||
<VideoGallery videos={videos} />
|
<VideoGallery videos={videos} />
|
||||||
|
|
||||||
{/* Floating music player */}
|
|
||||||
<MusicPlayer tracks={music} />
|
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="py-12 text-center border-t border-warm-border bg-amber-50/30">
|
<footer className="py-12 text-center border-t border-warm-border bg-amber-50/30">
|
||||||
<div className="max-w-lg mx-auto px-4">
|
<div className="max-w-lg mx-auto px-4">
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { getDb } from '@/lib/db'
|
||||||
|
import type { MediaItem } from '@/lib/types'
|
||||||
|
import MusicPlayer from './MusicPlayer'
|
||||||
|
|
||||||
|
function plain<T>(rows: unknown[]): T[] {
|
||||||
|
return JSON.parse(JSON.stringify(rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GlobalMusicPlayer() {
|
||||||
|
const db = getDb()
|
||||||
|
const music = plain<MediaItem>(
|
||||||
|
db.prepare("SELECT * FROM media WHERE type = 'music' ORDER BY sort_order, created_at").all()
|
||||||
|
)
|
||||||
|
return <MusicPlayer tracks={music} />
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import type { MediaItem } from '@/lib/types'
|
|||||||
const TAIL_SKIP = 10
|
const TAIL_SKIP = 10
|
||||||
const CROSSFADE_DURATION = 3
|
const CROSSFADE_DURATION = 3
|
||||||
const VOLUME = 0.4
|
const VOLUME = 0.4
|
||||||
const FADE_IN_DURATION = 2000 // ms to fade in after first interaction
|
const FADE_IN_MS = 2000
|
||||||
|
|
||||||
export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
||||||
const track = tracks[0] ?? null
|
const track = tracks[0] ?? null
|
||||||
@@ -17,7 +17,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
|||||||
const audioB = useRef<HTMLAudioElement>(null)
|
const audioB = useRef<HTMLAudioElement>(null)
|
||||||
const activeRef = useRef<'A' | 'B'>('A')
|
const activeRef = useRef<'A' | 'B'>('A')
|
||||||
const fadingRef = useRef(false)
|
const fadingRef = useRef(false)
|
||||||
const unmutedRef = useRef(false)
|
const startedRef = useRef(false)
|
||||||
|
|
||||||
const [userMuted, setUserMuted] = useState(false)
|
const [userMuted, setUserMuted] = useState(false)
|
||||||
const [playing, setPlaying] = useState(false)
|
const [playing, setPlaying] = useState(false)
|
||||||
@@ -31,12 +31,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getVolume = useCallback(
|
// ── Crossfade loop ──────────────────────────────────────────────────────
|
||||||
() => (userMuted ? 0 : VOLUME),
|
|
||||||
[userMuted],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Crossfade to loop back
|
|
||||||
const crossfade = useCallback(() => {
|
const crossfade = useCallback(() => {
|
||||||
if (fadingRef.current) return
|
if (fadingRef.current) return
|
||||||
fadingRef.current = true
|
fadingRef.current = true
|
||||||
@@ -51,7 +46,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
|||||||
|
|
||||||
const startTime = performance.now()
|
const startTime = performance.now()
|
||||||
const outStartVol = out.volume
|
const outStartVol = out.volume
|
||||||
const targetVol = getVolume()
|
const targetVol = userMuted ? 0 : VOLUME
|
||||||
|
|
||||||
const step = () => {
|
const step = () => {
|
||||||
const t = Math.min((performance.now() - startTime) / (CROSSFADE_DURATION * 1000), 1)
|
const t = Math.min((performance.now() - startTime) / (CROSSFADE_DURATION * 1000), 1)
|
||||||
@@ -66,9 +61,9 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
requestAnimationFrame(step)
|
requestAnimationFrame(step)
|
||||||
}, [getVolume, getActive, getInactive])
|
}, [userMuted, getActive, getInactive])
|
||||||
|
|
||||||
// Monitor playback for crossfade trigger
|
// Monitor for crossfade trigger
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!playing || !src) return
|
if (!playing || !src) return
|
||||||
let id: number
|
let id: number
|
||||||
@@ -86,7 +81,7 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
|||||||
return () => cancelAnimationFrame(id)
|
return () => cancelAnimationFrame(id)
|
||||||
}, [playing, src, getActive, crossfade])
|
}, [playing, src, getActive, crossfade])
|
||||||
|
|
||||||
// Fallback: if audio ends without crossfade, restart
|
// Fallback if crossfade misses
|
||||||
const handleEnded = useCallback(() => {
|
const handleEnded = useCallback(() => {
|
||||||
const a = getActive()
|
const a = getActive()
|
||||||
if (a) {
|
if (a) {
|
||||||
@@ -95,50 +90,81 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
|
|||||||
}
|
}
|
||||||
}, [getActive])
|
}, [getActive])
|
||||||
|
|
||||||
// Sync volume when user toggles mute
|
// Sync volume on mute toggle
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!unmutedRef.current) return
|
const vol = userMuted ? 0 : VOLUME
|
||||||
const vol = getVolume()
|
|
||||||
const a = getActive()
|
const a = getActive()
|
||||||
const b = getInactive()
|
const b = getInactive()
|
||||||
if (a && !fadingRef.current) a.volume = vol
|
if (a && !fadingRef.current) a.volume = vol
|
||||||
if (b && !fadingRef.current && !b.paused) b.volume = vol
|
if (b && !fadingRef.current && !b.paused) b.volume = vol
|
||||||
}, [userMuted, getVolume, getActive, getInactive])
|
}, [userMuted, getActive, getInactive])
|
||||||
|
|
||||||
// Autoplay strategy:
|
// ── Autoplay strategy ───────────────────────────────────────────────────
|
||||||
// 1. Start muted (browsers allow this)
|
// Mobile Safari needs play() called *directly* inside a user gesture.
|
||||||
// 2. On first user interaction (scroll/click/touch/key), fade volume in
|
// Desktop Chrome allows muted autoplay (volume=0).
|
||||||
|
//
|
||||||
|
// Strategy:
|
||||||
|
// 1. Try silent autoplay immediately (desktop)
|
||||||
|
// 2. On first touch/click, call play() directly in the handler (mobile)
|
||||||
|
// 3. Then fade volume in over 2s
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!src) return
|
if (!src) return
|
||||||
const a = audioA.current
|
|
||||||
if (!a) return
|
|
||||||
|
|
||||||
// Start muted playback immediately
|
const startAndFade = (a: HTMLAudioElement) => {
|
||||||
a.volume = 0
|
if (startedRef.current) return
|
||||||
a.play().then(() => setPlaying(true)).catch(() => {})
|
startedRef.current = true
|
||||||
|
a.volume = 0
|
||||||
// Fade in on first interaction
|
a.play().then(() => {
|
||||||
const fadeIn = () => {
|
setPlaying(true)
|
||||||
if (unmutedRef.current) return
|
// Fade in
|
||||||
unmutedRef.current = true
|
const t0 = performance.now()
|
||||||
|
const fade = () => {
|
||||||
const active = activeRef.current === 'A' ? audioA.current : audioB.current
|
const t = Math.min((performance.now() - t0) / FADE_IN_MS, 1)
|
||||||
if (!active) return
|
a.volume = VOLUME * t
|
||||||
|
if (t < 1) requestAnimationFrame(fade)
|
||||||
const startTime = performance.now()
|
}
|
||||||
const step = () => {
|
requestAnimationFrame(fade)
|
||||||
const t = Math.min((performance.now() - startTime) / FADE_IN_DURATION, 1)
|
}).catch(() => {
|
||||||
active.volume = VOLUME * t
|
startedRef.current = false
|
||||||
if (t < 1) requestAnimationFrame(step)
|
})
|
||||||
}
|
|
||||||
requestAnimationFrame(step)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = ['scroll', 'click', 'touchstart', 'keydown'] as const
|
// Attempt 1: silent autoplay (works on desktop)
|
||||||
events.forEach((e) => window.addEventListener(e, fadeIn, { once: true, passive: true }))
|
const a = audioA.current
|
||||||
|
if (a) {
|
||||||
return () => {
|
a.volume = 0
|
||||||
events.forEach((e) => window.removeEventListener(e, fadeIn))
|
a.play().then(() => {
|
||||||
|
startedRef.current = true
|
||||||
|
setPlaying(true)
|
||||||
|
// Already playing silently, fade in on first interaction
|
||||||
|
const fadeIn = () => {
|
||||||
|
if (!startedRef.current) return
|
||||||
|
const t0 = performance.now()
|
||||||
|
const fade = () => {
|
||||||
|
const active = activeRef.current === 'A' ? audioA.current : audioB.current
|
||||||
|
if (!active) return
|
||||||
|
const t = Math.min((performance.now() - t0) / FADE_IN_MS, 1)
|
||||||
|
if (!fadingRef.current) active.volume = VOLUME * t
|
||||||
|
if (t < 1) requestAnimationFrame(fade)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(fade)
|
||||||
|
}
|
||||||
|
const events = ['scroll', 'click', 'touchstart', 'keydown'] as const
|
||||||
|
events.forEach((e) => window.addEventListener(e, fadeIn, { once: true, passive: true }))
|
||||||
|
}).catch(() => {
|
||||||
|
// Attempt 2: autoplay blocked (mobile) → wait for user gesture
|
||||||
|
// play() MUST be called synchronously inside the handler
|
||||||
|
const onGesture = () => {
|
||||||
|
const el = audioA.current
|
||||||
|
if (el) startAndFade(el)
|
||||||
|
// Clean up other listeners
|
||||||
|
gestureEvents.forEach((e) => document.removeEventListener(e, onGesture))
|
||||||
|
}
|
||||||
|
const gestureEvents = ['click', 'touchstart', 'touchend', 'keydown'] as const
|
||||||
|
gestureEvents.forEach((e) =>
|
||||||
|
document.addEventListener(e, onGesture, { once: true, passive: true })
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [src])
|
}, [src])
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export default function TributeSection() {
|
|||||||
return (
|
return (
|
||||||
<section id="ueber-oma" className="py-20 sm:py-28">
|
<section id="ueber-oma" className="py-20 sm:py-28">
|
||||||
<div className="max-w-2xl mx-auto px-6">
|
<div className="max-w-2xl mx-auto px-6">
|
||||||
|
|
||||||
{/* ── Family perspective ─────────────────────────────────── */}
|
|
||||||
<motion.div {...fade} className="text-center mb-16">
|
<motion.div {...fade} className="text-center mb-16">
|
||||||
<p className="text-warm-gold/50 text-xs tracking-[0.5em] uppercase font-lora mb-4">
|
<p className="text-warm-gold/50 text-xs tracking-[0.5em] uppercase font-lora mb-4">
|
||||||
Für immer in unseren Herzen
|
Für immer in unseren Herzen
|
||||||
@@ -56,8 +54,8 @@ export default function TributeSection() {
|
|||||||
Weihnachten bei Oma und Opa war jedes Jahr das Gleiche, und genau
|
Weihnachten bei Oma und Opa war jedes Jahr das Gleiche, und genau
|
||||||
das hat es so besonders gemacht. Der Kamin lief. Der Karpfen
|
das hat es so besonders gemacht. Der Kamin lief. Der Karpfen
|
||||||
schwamm in der Badewanne. Die Geschenke lagen im Wohnzimmer,
|
schwamm in der Badewanne. Die Geschenke lagen im Wohnzimmer,
|
||||||
aber die Tür blieb zu, bis alle fertig gegessen hatten. Berge
|
aber die Tür blieb zu, bis alle fertig gegessen hatten.
|
||||||
von Essen. Und sie mittendrin, glücklich wenn alle satt waren.
|
Und sie mittendrin, glücklich wenn alle satt waren.
|
||||||
Sie hat es geliebt, zu geben.
|
Sie hat es geliebt, zu geben.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
@@ -76,8 +74,8 @@ export default function TributeSection() {
|
|||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
<motion.p {...fade}>
|
<motion.p {...fade}>
|
||||||
Ihr Haus, in dem so viel passiert ist, wird jetzt von Jacky und
|
Ihr Haus, in dem so viel passiert ist, wird jetzt von ihrer
|
||||||
Niklas weitergeführt. Bald wird dort ein neues Leben beginnen.
|
Enkelin weitergeführt. Bald wird dort ein neues Leben beginnen.
|
||||||
Ein Kind, das seine Uroma nie kennenlernen wird. Aber dessen
|
Ein Kind, das seine Uroma nie kennenlernen wird. Aber dessen
|
||||||
Ultraschallbild bei ihr im Sarg liegt. So nah, wie es nur geht.
|
Ultraschallbild bei ihr im Sarg liegt. So nah, wie es nur geht.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
@@ -113,116 +111,7 @@ export default function TributeSection() {
|
|||||||
</motion.p>
|
</motion.p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider between family and personal */}
|
{/* Closing */}
|
||||||
<motion.div {...fade} className="my-24 flex items-center justify-center gap-4">
|
|
||||||
<div className="h-px w-20 bg-warm-gold/15" />
|
|
||||||
<span className="text-warm-gold/25 text-sm">✽✽✽</span>
|
|
||||||
<div className="h-px w-20 bg-warm-gold/15" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* ── Dennis's perspective ───────────────────────────────── */}
|
|
||||||
<motion.div {...fade} className="text-center mb-16">
|
|
||||||
<p className="text-warm-gold/50 text-xs tracking-[0.5em] uppercase font-lora mb-4">
|
|
||||||
Von Dennis
|
|
||||||
</p>
|
|
||||||
<h2 className="font-cormorant italic text-5xl sm:text-6xl text-warm-brown mb-4">
|
|
||||||
Meine Oma
|
|
||||||
</h2>
|
|
||||||
<div className="flex items-center justify-center gap-4">
|
|
||||||
<div className="h-px w-16 bg-warm-gold/30" />
|
|
||||||
<span className="text-warm-gold/40 text-lg">✽</span>
|
|
||||||
<div className="h-px w-16 bg-warm-gold/30" />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="space-y-8 font-lora text-warm-brown/80 text-base sm:text-lg leading-relaxed">
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Ich kam aus der Schule und das Essen stand schon da. Jedes Mal.
|
|
||||||
Rahmsauce. Ich weiß nicht, wie oft ich die gegessen habe, aber
|
|
||||||
es war nie genug. Ich würde alles dafür geben, noch einmal an
|
|
||||||
ihrem Küchentisch zu sitzen.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Oma roch nach Oma. Ich weiß nicht, wie ich das anders beschreiben
|
|
||||||
soll. Nicht nach Parfum. Nicht nach irgendwas, das man kaufen kann.
|
|
||||||
Einfach nach ihr. Wenn ich an sie denke, ist das Erste, was
|
|
||||||
kommt, dieses Gefühl. Diese Wärme. Der Geruch von ihrem Haus.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Bei ihr war es immer heiß. Immer. Der Kamin lief, die Heizung
|
|
||||||
lief, man hat geschwitzt und es war trotzdem schön. An Weihnachten
|
|
||||||
war der Karpfen in der Badewanne und die Geschenke im Wohnzimmer.
|
|
||||||
Aber die Tür blieb zu. Erst essen, dann Geschenke. Das war Gesetz.
|
|
||||||
Und wir haben uns jedes Mal gefreut, als wären wir fünf.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Wenn Pico bei ihr war, hab ich nicht mehr existiert. Sie hat mit
|
|
||||||
ihm geredet, ihn gefüttert, ihn verwöhnt. Ich saß daneben und
|
|
||||||
war Luft. Aber das war okay. Weil sie so glücklich war dabei.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Pico ist letztes Jahr gestorben. Und jetzt Oma. Ich stelle mir
|
|
||||||
vor, wie sie irgendwo sitzt und er neben ihr liegt und sie ihm
|
|
||||||
wieder irgendwas erzählt, was er nicht versteht. Und er hört
|
|
||||||
trotzdem zu.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Sie war stur. Richtig stur. Deswegen hatte sie auch den
|
|
||||||
Oberschenkelhalsbruch. Weil sie alles alleine machen wollte.
|
|
||||||
Weil sie sich nichts sagen lassen hat. Man konnte sich aufregen.
|
|
||||||
Aber im Nachhinein war das auch das, was sie so stark gemacht hat.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
{...fade}
|
|
||||||
className="border-l-2 border-warm-gold/30 pl-6 py-2 my-12"
|
|
||||||
>
|
|
||||||
<p className="font-cormorant italic text-2xl sm:text-3xl text-warm-brown/70 leading-snug">
|
|
||||||
Ich habe es dir viel zu selten gesagt, Oma.
|
|
||||||
<br />
|
|
||||||
Aber ich habe dich geliebt. Sehr.
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Das letzte Mal hab ich sie im Krankenhaus besucht. In der
|
|
||||||
Geriatrie. Sie lief schon wieder, es ging ihr gut. Sie hat
|
|
||||||
im Aufenthaltsraum Blumenvasen gesehen und meinte, die könnte
|
|
||||||
man ja mitnehmen. Ich hab gelacht. So war sie halt.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Ihr Zimmer war gegenüber von dem Zimmer, in dem meine Mutter
|
|
||||||
wochenlang gelegen hatte, als sie mit mir schwanger war. Mama
|
|
||||||
durfte sich kaum bewegen damals. Im selben Flur, in dem ich auf
|
|
||||||
die Welt kam, ist Oma gegangen. Das lässt mich nicht los.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Am 10. Februar, um 13:13 Uhr, schrieb mir meine Schwester
|
|
||||||
auf Telegram: <span className="italic text-warm-brown">„Sie
|
|
||||||
möchten die Reanimation abbrechen."</span>
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Jacky ist schwanger. Sie hat den Ultraschall mit in den Sarg
|
|
||||||
gelegt. Ein Kind, das Oma nie treffen wird, aber das in ihrem
|
|
||||||
Haus aufwachsen wird. In den Wänden, die nach Oma riechen. In
|
|
||||||
der Küche, in der die Rahmsauce stand.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.p {...fade}>
|
|
||||||
Diese Seite ist für dich. Damit ich nicht vergesse. Damit
|
|
||||||
niemand vergisst.
|
|
||||||
</motion.p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Closing details */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
{...fade}
|
{...fade}
|
||||||
className="mt-20 text-center"
|
className="mt-20 text-center"
|
||||||
@@ -241,6 +130,13 @@ export default function TributeSection() {
|
|||||||
<p className="font-lora text-warm-brown-light/30 text-xs mt-3 tracking-wide">
|
<p className="font-lora text-warm-brown-light/30 text-xs mt-3 tracking-wide">
|
||||||
Beerdigung am 20. Februar 2026
|
Beerdigung am 20. Februar 2026
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/meine-oma"
|
||||||
|
className="inline-block mt-10 font-cormorant italic text-warm-brown-light/30 hover:text-warm-gold/60 text-sm transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Von Dennis
|
||||||
|
</a>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user