From 4aeb08cb57bc3ac911c61a1c21f987fb171fdc5d Mon Sep 17 00:00:00 2001 From: denshooter Date: Mon, 16 Feb 2026 02:08:28 +0100 Subject: [PATCH] feat: music player redesign, candle section, impressum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MusicPlayer: - Beautiful inline section with numbered track list + click-to-play - Animated waveform bars on playing track - Time display (elapsed / duration) on progress bar - Floating mini-player with track name + time, shows after first play - Play/pause in mini-player, close button CandleSection: - 7 hand-coded CSS/Framer Motion candle flames with organic flicker - Layered flame (outer glow + main + inner core) + wax drip highlight - "Ruhe in Frieden" text with subtle glow Impressum: - New /impressum page with TMG §5 structure (placeholder address) - Privacy notice (no cookies/tracking, voluntary memory data) - Copyright, liability disclaimer - Consistent cream design with Cormorant/Lora typography page.tsx: - CandleSection added between VideoGallery and MusicPlayer - Musik nav link (conditional on tracks) - Footer Impressum link - MemorySection wrapped in id="erinnerungen" section for nav anchor Co-Authored-By: Claude Sonnet 4.5 --- src/app/impressum/page.tsx | 131 +++++++++++++ src/app/page.tsx | 60 ++++-- src/components/CandleSection.tsx | 174 +++++++++++++++++ src/components/MusicPlayer.tsx | 311 +++++++++++++++++++++++-------- 4 files changed, 582 insertions(+), 94 deletions(-) create mode 100644 src/app/impressum/page.tsx create mode 100644 src/components/CandleSection.tsx diff --git a/src/app/impressum/page.tsx b/src/app/impressum/page.tsx new file mode 100644 index 0000000..e5ca647 --- /dev/null +++ b/src/app/impressum/page.tsx @@ -0,0 +1,131 @@ +import type { Metadata } from 'next' +import Link from 'next/link' + +export const metadata: Metadata = { + title: 'Impressum – In Erinnerung an Maria Malejka', +} + +export default function ImpressumPage() { + return ( +
+
+ {/* Back link */} + + + Zurück zur Gedenkseite + + + {/* Header */} +
+

+ Impressum +

+
+
+ +
+
+
+ + {/* Content */} +
+
+

+ Angaben gemäß § 5 TMG +

+
+

Dennis Malejka

+

+ (Kontaktdaten auf Anfrage) +

+
+
+ +
+

+ Kontakt +

+

+ Bei Fragen oder Anliegen bezüglich dieser Gedenkseite wenden Sie sich bitte + per E-Mail an den Betreiber. +

+
+ +
+

+ Zweck dieser Seite +

+

+ Diese Website ist eine private, nicht-kommerzielle Gedenkseite zum ehrenvollen + Andenken an{' '} + Maria Malejka{' '} + (29. November 1944 – 10. Februar 2026). Sie dient ausschließlich dem + persönlichen Gedenken und dem Austausch von Erinnerungen im Familienund + Freundeskreis. +

+
+ +
+

+ Datenschutz +

+

+ Diese Seite verwendet keine Cookies, kein externes Tracking und keine + Analyse-Dienste. Es werden ausschließlich folgende Daten gespeichert: +

+
    + {[ + 'Hochgeladene Bilder, Videos und Musikdateien (durch den Administrator)', + 'Freiwillig hinterlassene Erinnerungen mit optionalem Namen (öffentlich sichtbar)', + ].map((item) => ( +
  • + + {item} +
  • + ))} +
+

+ Möchten Sie eine von Ihnen hinterlassene Erinnerung löschen lassen, + wenden Sie sich bitte an den Betreiber. +

+
+ +
+

+ Urheberrecht +

+

+ Alle auf dieser Seite veröffentlichten Fotos und Medien sind privates + Eigentum der Familie Malejka. Eine Weitergabe oder Veröffentlichung ohne + ausdrückliche Genehmigung ist nicht gestattet. +

+
+ +
+

+ Haftungsausschluss +

+

+ Die Inhalte dieser Seite wurden mit größter Sorgfalt erstellt. Für die + Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann keine Gewähr + übernommen werden. +

+
+
+ + {/* Footer ornament */} +
+

+ In liebevoller Erinnerung +

+

+ Maria Malejka · 1944 – 2026 +

+
+
+
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 67dcdc0..be322f3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import PhotoSlideshow from '@/components/PhotoSlideshow' import PhotoGallery from '@/components/PhotoGallery' import MemorySection from '@/components/MemorySection' import WriteSection from '@/components/WriteSection' +import CandleSection from '@/components/CandleSection' import MusicPlayer from '@/components/MusicPlayer' import VideoGallery from '@/components/VideoGallery' @@ -31,7 +32,7 @@ export default async function HomePage() { {/* Hero */} - {/* Navigation anchors */} + {/* Navigation */} @@ -86,11 +93,16 @@ export default async function HomePage() { {/* Memories */} - +
+ +
{/* Videos */} + {/* Candle section */} + + {/* Music player */} @@ -117,16 +129,26 @@ export default async function HomePage() {

„Du bist nicht fort, nur ein Schritt voraus."

- - {/* Hidden admin link */} - - · - + {/* Footer links */} +
+ + Impressum + + · + {/* Hidden admin link */} + + · + +
+ ) diff --git a/src/components/CandleSection.tsx b/src/components/CandleSection.tsx new file mode 100644 index 0000000..c2c62cb --- /dev/null +++ b/src/components/CandleSection.tsx @@ -0,0 +1,174 @@ +'use client' + +import { motion } from 'framer-motion' + +const candleData = [ + { delay: 0.0, bodyH: 88, bodyW: 9 }, + { delay: 0.4, bodyH: 112, bodyW: 11 }, + { delay: 0.2, bodyH: 76, bodyW: 8 }, + { delay: 0.6, bodyH: 100, bodyW: 10 }, + { delay: 0.1, bodyH: 92, bodyW: 9 }, + { delay: 0.5, bodyH: 120, bodyW: 12 }, + { delay: 0.3, bodyH: 82, bodyW: 9 }, +] + +function Candle({ bodyH, bodyW, delay }: { bodyH: number; bodyW: number; delay: number }) { + const flameW = bodyW * 1.8 + const flameH = bodyW * 2.6 + + return ( +
+ {/* Flame */} + + {/* Outer glow */} +
+ {/* Main flame */} +
+ {/* Inner core */} +
+ + + {/* Wick */} +
+ + {/* Candle body */} +
+ {/* Wax drip highlight */} +
+
+ + {/* Base plate */} +
+
+ ) +} + +export default function CandleSection() { + return ( +
+ + {/* Candles */} +
+ {candleData.map((c, i) => ( + + ))} +
+ + {/* Text */} +

+ Ruhe in Frieden +

+ +
+
+ +
+
+ +

+ 29. November 1944 — 10. Februar 2026 +

+ +
+ ) +} diff --git a/src/components/MusicPlayer.tsx b/src/components/MusicPlayer.tsx index 8852f5d..818603d 100644 --- a/src/components/MusicPlayer.tsx +++ b/src/components/MusicPlayer.tsx @@ -14,17 +14,65 @@ import { } from 'lucide-react' import type { MediaItem } from '@/lib/types' +function formatTime(s: number) { + if (!s || isNaN(s) || !isFinite(s)) return '--:--' + const m = Math.floor(s / 60) + const sec = Math.floor(s % 60) + return `${m}:${sec.toString().padStart(2, '0')}` +} + +function WaveformBars({ playing }: { playing: boolean }) { + return ( +
+ {[0.55, 1, 0.7, 0.9, 0.5].map((h, i) => ( + + ))} +
+ ) +} + +function TrackNumber({ index, isCurrent, playing }: { index: number; isCurrent: boolean; playing: boolean }) { + if (isCurrent && playing) return + return ( + + {String(index + 1).padStart(2, '0')} + + ) +} + export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { const [current, setCurrent] = useState(0) const [playing, setPlaying] = useState(false) - const [volume, setVolume] = useState(0.35) + const [volume, setVolume] = useState(0.4) const [muted, setMuted] = useState(false) - const [visible, setVisible] = useState(false) const [progress, setProgress] = useState(0) + const [duration, setDuration] = useState(0) + const [elapsed, setElapsed] = useState(0) + const [miniVisible, setMiniVisible] = useState(false) const audioRef = useRef(null) const track = tracks[current] ?? null + const trackName = (i: number) => + tracks[i]?.original_name?.replace(/\.[^/.]+$/, '') || + tracks[i]?.caption || + `Titel ${i + 1}` + useEffect(() => { const audio = audioRef.current if (!audio) return @@ -36,9 +84,10 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { if (!audio || !track) return audio.src = `/api/files/${track.filename}` audio.volume = muted ? 0 : volume - if (playing) { - audio.play().catch(() => setPlaying(false)) - } + setDuration(0) + setElapsed(0) + setProgress(0) + if (playing) audio.play().catch(() => setPlaying(false)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [current]) @@ -47,11 +96,19 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { if (!audio) return if (playing) { audio.pause() - setPlaying(false) } else { audio.play().catch(() => setPlaying(false)) + } + } + + const playTrack = (i: number) => { + if (i === current) { + togglePlay() + } else { + setCurrent(i) setPlaying(true) } + setMiniVisible(true) } const prev = () => setCurrent((c) => (c - 1 + tracks.length) % tracks.length) @@ -61,6 +118,12 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { const audio = audioRef.current if (!audio || !audio.duration) return setProgress((audio.currentTime / audio.duration) * 100) + setElapsed(audio.currentTime) + } + + const handleLoadedMetadata = () => { + const audio = audioRef.current + if (audio) setDuration(audio.duration) } const handleSeek = (e: React.ChangeEvent) => { @@ -73,11 +136,6 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { if (tracks.length === 0 || !track) return null - const trackName = - track.original_name?.replace(/\.[^/.]+$/, '') || - track.caption || - `Titel ${current + 1}` - return ( <>