feat: music player redesign, candle section, impressum

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 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-02-16 02:08:28 +01:00
parent 279a07e4eb
commit 4aeb08cb57
4 changed files with 582 additions and 94 deletions
+236 -75
View File
@@ -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 (
<div className="flex items-end gap-px h-4">
{[0.55, 1, 0.7, 0.9, 0.5].map((h, i) => (
<motion.div
key={i}
className="w-[3px] bg-amber-400/70 rounded-full"
animate={
playing
? { height: ['30%', `${h * 100}%`, '45%', `${h * 75}%`, '30%'] }
: { height: '30%' }
}
transition={{
duration: 0.75 + i * 0.13,
repeat: Infinity,
ease: 'easeInOut',
delay: i * 0.11,
}}
style={{ height: '30%' }}
/>
))}
</div>
)
}
function TrackNumber({ index, isCurrent, playing }: { index: number; isCurrent: boolean; playing: boolean }) {
if (isCurrent && playing) return <WaveformBars playing={playing} />
return (
<span className={`font-lora text-xs tabular-nums ${isCurrent ? 'text-amber-400/60' : 'text-amber-100/20'}`}>
{String(index + 1).padStart(2, '0')}
</span>
)
}
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<HTMLAudioElement>(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<HTMLInputElement>) => {
@@ -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 (
<>
<audio
@@ -85,93 +143,137 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
src={`/api/files/${track.filename}`}
onEnded={next}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
/>
{/* Floating button */}
<motion.button
onClick={() => setVisible((v) => !v)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`fixed bottom-6 right-6 z-40 rounded-full p-3.5 shadow-lg transition-colors backdrop-blur-sm ${
playing
? 'bg-amber-700/95 text-amber-100 ring-2 ring-amber-400/40'
: 'bg-stone-800/90 text-amber-200/80 hover:bg-amber-900/90'
}`}
aria-label="Musik"
{/* ── Inline section ── */}
<section
id="musik"
className="py-20 px-4"
style={{ background: 'linear-gradient(to bottom, #0a0706, #060304)' }}
>
<Music size={20} />
{playing && (
<span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-amber-400 animate-pulse" />
)}
</motion.button>
{/* Player panel */}
<AnimatePresence>
{visible && (
<div className="max-w-xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 16, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 16, scale: 0.97 }}
transition={{ duration: 0.2 }}
className="fixed bottom-20 right-6 z-40 bg-stone-950/95 backdrop-blur-md rounded-2xl p-5 shadow-2xl w-72 border border-amber-900/20"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1 min-w-0">
<p className="text-amber-200 font-cormorant italic text-base leading-snug truncate">
{trackName}
</p>
<p className="text-amber-600 text-xs mt-0.5">
{current + 1} / {tracks.length}
</p>
</div>
<button
onClick={() => setVisible(false)}
className="text-amber-800 hover:text-amber-500 ml-2 flex-shrink-0"
<p className="text-amber-200/30 text-xs tracking-[0.5em] uppercase font-lora mb-3">
Begleitung in Tönen
</p>
<h2 className="font-cormorant italic text-4xl sm:text-5xl text-amber-100/70 mb-3">
Musik
</h2>
<div className="flex items-center justify-center gap-4">
<div className="h-px w-12 bg-amber-400/15" />
<span className="text-amber-400/25"></span>
<div className="h-px w-12 bg-amber-400/15" />
</div>
</motion.div>
{/* Track list */}
<div className="space-y-0.5 mb-8">
{tracks.map((t, i) => (
<motion.button
key={t.id}
initial={{ opacity: 0, x: -8 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.05 }}
onClick={() => playTrack(i)}
className={`w-full flex items-center gap-4 px-4 py-3.5 rounded-xl transition-all duration-200 text-left group ${
i === current
? 'bg-amber-400/[0.07] border border-amber-400/15'
: 'hover:bg-white/[0.03] border border-transparent'
}`}
>
<X size={16} />
</button>
<div className="w-7 flex-shrink-0 flex items-center justify-center">
<TrackNumber index={i} isCurrent={i === current} playing={playing} />
</div>
<span
className={`font-cormorant italic text-lg flex-1 min-w-0 truncate transition-colors ${
i === current
? 'text-amber-200/80'
: 'text-amber-100/35 group-hover:text-amber-100/60'
}`}
>
{trackName(i)}
</span>
<span className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
{i === current && playing ? (
<Pause size={13} className="text-amber-400/50" />
) : (
<Play size={13} className="text-amber-400/40" />
)}
</span>
</motion.button>
))}
</div>
{/* Player controls */}
<div className="border-t border-amber-400/10 pt-6">
<p className="text-center font-cormorant italic text-amber-200/50 text-xl mb-5 truncate px-4">
{trackName(current)}
</p>
{/* Progress */}
<div className="flex items-center gap-3 mb-6">
<span className="font-lora text-xs text-amber-100/20 w-9 text-right tabular-nums">
{formatTime(elapsed)}
</span>
<div className="flex-1 relative group/slider">
<input
type="range"
min="0"
max="100"
step="0.1"
value={progress}
onChange={handleSeek}
className="w-full cursor-pointer accent-amber-500"
style={{ height: '2px' }}
/>
</div>
<span className="font-lora text-xs text-amber-100/20 w-9 tabular-nums">
{formatTime(duration)}
</span>
</div>
{/* Progress bar */}
<input
type="range"
min="0"
max="100"
step="0.1"
value={progress}
onChange={handleSeek}
className="w-full mb-4 accent-amber-500 h-1 cursor-pointer"
/>
{/* Controls */}
<div className="flex items-center justify-center gap-5 mb-4">
{/* Controls row */}
<div className="flex items-center justify-center gap-8 mb-5">
<button
onClick={prev}
className="text-amber-500 hover:text-amber-300 transition-colors"
className="text-amber-400/30 hover:text-amber-400/70 transition-colors"
>
<SkipBack size={18} />
<SkipBack size={20} />
</button>
<button
onClick={togglePlay}
className="bg-amber-700 hover:bg-amber-600 text-white rounded-full w-10 h-10 flex items-center justify-center transition-colors shadow-lg"
<motion.button
onClick={() => { togglePlay(); setMiniVisible(true) }}
whileTap={{ scale: 0.93 }}
className="w-14 h-14 rounded-full bg-amber-400/10 hover:bg-amber-400/18 border border-amber-400/20 hover:border-amber-400/40 text-amber-300/80 flex items-center justify-center transition-all duration-200 shadow-lg"
style={{ boxShadow: playing ? '0 0 24px rgba(196,160,74,0.15)' : undefined }}
>
{playing ? <Pause size={18} /> : <Play size={18} className="ml-0.5" />}
</button>
{playing ? <Pause size={22} /> : <Play size={22} className="ml-0.5" />}
</motion.button>
<button
onClick={next}
className="text-amber-500 hover:text-amber-300 transition-colors"
className="text-amber-400/30 hover:text-amber-400/70 transition-colors"
>
<SkipForward size={18} />
<SkipForward size={20} />
</button>
</div>
{/* Volume */}
<div className="flex items-center gap-2">
<div className="flex items-center justify-center gap-3">
<button
onClick={() => setMuted((m) => !m)}
className="text-amber-600 hover:text-amber-400 transition-colors"
className="text-amber-600/40 hover:text-amber-400/60 transition-colors"
>
{muted ? <VolumeX size={15} /> : <Volume2 size={15} />}
</button>
@@ -185,9 +287,68 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
setVolume(parseFloat(e.target.value))
setMuted(false)
}}
className="flex-1 accent-amber-600 h-1 cursor-pointer"
className="w-28 accent-amber-600 cursor-pointer"
style={{ height: '2px' }}
/>
</div>
</div>
</div>
</section>
{/* ── Floating mini-player ── */}
<AnimatePresence>
{miniVisible && (
<motion.div
initial={{ opacity: 0, y: 16, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 16, scale: 0.95 }}
transition={{ duration: 0.25 }}
className="fixed bottom-6 right-6 z-50 flex items-center gap-3 bg-stone-950/96 backdrop-blur-md px-4 py-3 rounded-2xl border border-amber-900/25 shadow-2xl"
>
{/* Icon + pulse */}
<div className="relative flex-shrink-0">
<div
className={`w-8 h-8 rounded-full bg-amber-800/40 flex items-center justify-center ${
playing ? 'ring-1 ring-amber-400/30' : ''
}`}
>
<Music size={14} className="text-amber-300/80" />
</div>
{playing && (
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-amber-400 animate-pulse" />
)}
</div>
{/* Track info */}
<button
onClick={() =>
document.getElementById('musik')?.scrollIntoView({ behavior: 'smooth' })
}
className="max-w-[130px] text-left"
>
<p className="text-amber-200/70 font-cormorant italic text-sm truncate leading-tight">
{trackName(current)}
</p>
<p className="text-amber-600/40 text-xs font-lora mt-0.5">
{playing ? `${formatTime(elapsed)} / ${formatTime(duration)}` : 'pausiert'}
</p>
</button>
{/* Play/pause */}
<button
onClick={togglePlay}
className="text-amber-400/60 hover:text-amber-300 transition-colors"
>
{playing ? <Pause size={16} /> : <Play size={16} />}
</button>
{/* Close */}
<button
onClick={() => setMiniVisible(false)}
className="text-amber-800/60 hover:text-amber-500/80 transition-colors ml-1"
>
<X size={13} />
</button>
</motion.div>
)}
</AnimatePresence>