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
+131
View File
@@ -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 (
<main className="min-h-screen bg-cream py-16 px-4">
<div className="max-w-2xl mx-auto">
{/* Back link */}
<Link
href="/"
className="inline-flex items-center gap-2 text-warm-brown-light hover:text-warm-gold text-sm font-lora tracking-wider transition-colors duration-200 mb-12"
>
<span className="text-base leading-none"></span>
Zurück zur Gedenkseite
</Link>
{/* Header */}
<div className="mb-12">
<h1 className="font-cormorant italic text-5xl sm:text-6xl text-warm-brown mb-4">
Impressum
</h1>
<div className="flex items-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>
</div>
{/* Content */}
<div className="space-y-10 font-lora text-warm-brown/75 text-sm leading-loose">
<section>
<h2 className="font-cormorant text-2xl text-warm-brown mb-3">
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="text-warm-brown-light text-xs italic">
(Kontaktdaten auf Anfrage)
</p>
</address>
</section>
<section>
<h2 className="font-cormorant text-2xl text-warm-brown mb-3">
Kontakt
</h2>
<p>
Bei Fragen oder Anliegen bezüglich dieser Gedenkseite wenden Sie sich bitte
per E-Mail an den Betreiber.
</p>
</section>
<section>
<h2 className="font-cormorant text-2xl text-warm-brown mb-3">
Zweck dieser Seite
</h2>
<p>
Diese Website ist eine private, nicht-kommerzielle Gedenkseite zum ehrenvollen
Andenken an{' '}
<span className="text-warm-brown font-medium">Maria Malejka</span>{' '}
(29. November 1944 10. Februar 2026). Sie dient ausschließlich dem
persönlichen Gedenken und dem Austausch von Erinnerungen im Familienund
Freundeskreis.
</p>
</section>
<section>
<h2 className="font-cormorant text-2xl text-warm-brown mb-3">
Datenschutz
</h2>
<p>
Diese Seite verwendet keine Cookies, kein externes Tracking und keine
Analyse-Dienste. Es werden ausschließlich folgende Daten gespeichert:
</p>
<ul className="mt-3 space-y-1.5 list-none pl-0">
{[
'Hochgeladene Bilder, Videos und Musikdateien (durch den Administrator)',
'Freiwillig hinterlassene Erinnerungen mit optionalem Namen (öffentlich sichtbar)',
].map((item) => (
<li key={item} className="flex items-start gap-3">
<span className="text-warm-gold/50 mt-0.5 flex-shrink-0"></span>
<span>{item}</span>
</li>
))}
</ul>
<p className="mt-4">
Möchten Sie eine von Ihnen hinterlassene Erinnerung löschen lassen,
wenden Sie sich bitte an den Betreiber.
</p>
</section>
<section>
<h2 className="font-cormorant text-2xl text-warm-brown mb-3">
Urheberrecht
</h2>
<p>
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.
</p>
</section>
<section>
<h2 className="font-cormorant text-2xl text-warm-brown mb-3">
Haftungsausschluss
</h2>
<p>
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.
</p>
</section>
</div>
{/* Footer ornament */}
<div className="mt-16 pt-8 border-t border-warm-border/50 text-center">
<p className="font-cormorant italic text-warm-gold/40 text-xl">
In liebevoller Erinnerung
</p>
<p className="font-lora text-warm-brown-light/40 text-xs mt-2 tracking-widest">
Maria Malejka · 1944 2026
</p>
</div>
</div>
</main>
)
}
+41 -19
View File
@@ -5,6 +5,7 @@ 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 CandleSection from '@/components/CandleSection'
import MusicPlayer from '@/components/MusicPlayer' import MusicPlayer from '@/components/MusicPlayer'
import VideoGallery from '@/components/VideoGallery' import VideoGallery from '@/components/VideoGallery'
@@ -31,7 +32,7 @@ export default async function HomePage() {
{/* Hero */} {/* Hero */}
<HeroSection heroPhoto={photos[0]?.filename ?? null} /> <HeroSection heroPhoto={photos[0]?.filename ?? null} />
{/* Navigation anchors */} {/* Navigation */}
<nav className="sticky top-0 z-20 bg-cream/80 backdrop-blur-sm border-b border-warm-border"> <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"> <div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-center gap-6 sm:gap-10">
{photos.length > 0 && ( {photos.length > 0 && (
@@ -42,14 +43,12 @@ export default async function HomePage() {
Bilder Bilder
</a> </a>
)} )}
{memories.length > 0 && ( <a
<a href="#erinnerungen"
href="#erinnerungen" className="text-warm-brown-light hover:text-warm-gold text-sm font-cormorant italic transition-colors"
className="text-warm-brown-light hover:text-warm-gold text-sm font-cormorant italic transition-colors" >
> Erinnerungen
Erinnerungen </a>
</a>
)}
{videos.length > 0 && ( {videos.length > 0 && (
<a <a
href="#videos" href="#videos"
@@ -58,6 +57,14 @@ export default async function HomePage() {
Videos Videos
</a> </a>
)} )}
{music.length > 0 && (
<a
href="#musik"
className="text-warm-brown-light hover:text-warm-gold text-sm font-cormorant italic transition-colors"
>
Musik
</a>
)}
</div> </div>
</nav> </nav>
@@ -86,11 +93,16 @@ export default async function HomePage() {
<WriteSection /> <WriteSection />
{/* Memories */} {/* Memories */}
<MemorySection memories={memories} /> <section id="erinnerungen">
<MemorySection memories={memories} />
</section>
{/* Videos */} {/* Videos */}
<VideoGallery videos={videos} /> <VideoGallery videos={videos} />
{/* Candle section */}
<CandleSection />
{/* Music player */} {/* Music player */}
<MusicPlayer tracks={music} /> <MusicPlayer tracks={music} />
@@ -117,16 +129,26 @@ export default async function HomePage() {
<p className="font-cormorant italic text-warm-brown-light/60 text-lg"> <p className="font-cormorant italic text-warm-brown-light/60 text-lg">
Du bist nicht fort, nur ein Schritt voraus." Du bist nicht fort, nur ein Schritt voraus."
</p> </p>
</div>
{/* Hidden admin link */} {/* Footer links */}
<a <div className="mt-10 flex items-center justify-center gap-6">
href="/admin" <a
className="mt-10 inline-block text-warm-border/30 hover:text-warm-border/60 text-xs transition-colors" href="/impressum"
title="Verwaltung" className="text-warm-brown-light/40 hover:text-warm-brown-light/70 text-xs font-lora tracking-wider transition-colors duration-200"
> >
· Impressum
</a> </a>
<span className="text-warm-border/40 text-xs">·</span>
{/* Hidden admin link */}
<a
href="/admin"
className="text-warm-border/30 hover:text-warm-border/60 text-xs transition-colors"
title="Verwaltung"
>
·
</a>
</div>
</div>
</footer> </footer>
</main> </main>
) )
+174
View File
@@ -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 (
<div className="flex flex-col items-center" style={{ gap: 0 }}>
{/* Flame */}
<motion.div
style={{ width: flameW, height: flameH, position: 'relative' }}
animate={{
scaleX: [1, 0.82, 1.08, 0.9, 1.04, 1],
scaleY: [1, 1.08, 0.94, 1.06, 0.97, 1],
x: [-0.5, 0.8, -0.8, 1.2, -0.3, 0],
}}
transition={{
duration: 2.2 + delay * 0.5,
repeat: Infinity,
ease: 'easeInOut',
delay,
}}
>
{/* Outer glow */}
<div
style={{
position: 'absolute',
bottom: 0,
left: '50%',
transform: 'translateX(-50%)',
width: flameW * 1.3,
height: flameH * 1.3,
background:
'radial-gradient(ellipse at 50% 80%, rgba(255,180,40,0.18) 0%, transparent 70%)',
borderRadius: '50%',
filter: 'blur(6px)',
}}
/>
{/* Main flame */}
<div
style={{
position: 'absolute',
bottom: 0,
left: '50%',
transform: 'translateX(-50%)',
width: flameW,
height: flameH,
background:
'radial-gradient(ellipse at 50% 90%, rgba(255,200,60,0.95) 0%, rgba(255,110,10,0.80) 45%, rgba(180,50,0,0.40) 75%, transparent 100%)',
borderRadius: '50% 50% 35% 35% / 55% 55% 45% 45%',
filter: `blur(${bodyW * 0.09}px)`,
}}
/>
{/* Inner core */}
<div
style={{
position: 'absolute',
bottom: 2,
left: '50%',
transform: 'translateX(-50%)',
width: flameW * 0.45,
height: flameH * 0.55,
background:
'radial-gradient(ellipse at 50% 100%, rgba(255,255,220,0.95) 0%, rgba(255,230,80,0.7) 50%, transparent 100%)',
borderRadius: '50% 50% 40% 40%',
}}
/>
</motion.div>
{/* Wick */}
<div
style={{
width: 1.5,
height: 5,
backgroundColor: 'rgba(60,30,10,0.9)',
marginBottom: -1,
zIndex: 1,
}}
/>
{/* Candle body */}
<div
style={{
width: bodyW,
height: bodyH,
background:
'linear-gradient(to right, rgba(240,230,210,0.10) 0%, rgba(255,248,235,0.07) 40%, rgba(220,200,170,0.04) 100%)',
borderRadius: '1px 1px 0 0',
border: '1px solid rgba(255,255,255,0.05)',
borderBottom: 'none',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Wax drip highlight */}
<div
style={{
position: 'absolute',
top: 0,
left: bodyW * 0.2,
width: bodyW * 0.15,
height: bodyH * 0.4,
background: 'rgba(255,255,255,0.06)',
borderRadius: '0 0 50% 50%',
}}
/>
</div>
{/* Base plate */}
<div
style={{
width: bodyW + 6,
height: 3,
background: 'rgba(255,255,255,0.04)',
borderRadius: '0 0 2px 2px',
}}
/>
</div>
)
}
export default function CandleSection() {
return (
<section
className="py-24 overflow-hidden"
style={{ background: 'linear-gradient(to bottom, #060304 0%, #0d0807 50%, #060304 100%)' }}
>
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 1.2 }}
className="text-center"
>
{/* Candles */}
<div className="flex items-end justify-center gap-3 sm:gap-5 mb-14">
{candleData.map((c, i) => (
<Candle key={i} {...c} />
))}
</div>
{/* Text */}
<p
className="font-cormorant italic text-amber-200/40 text-2xl sm:text-3xl tracking-widest"
style={{ textShadow: '0 0 40px rgba(196,160,74,0.12)' }}
>
Ruhe in Frieden
</p>
<div className="flex items-center justify-center gap-4 mt-5">
<div className="h-px w-20 bg-amber-400/10" />
<span className="text-amber-400/15 text-xs"></span>
<div className="h-px w-20 bg-amber-400/10" />
</div>
<p className="font-lora text-amber-100/20 text-xs tracking-[0.4em] uppercase mt-4">
29. November 1944 10. Februar 2026
</p>
</motion.div>
</section>
)
}
+236 -75
View File
@@ -14,17 +14,65 @@ import {
} from 'lucide-react' } from 'lucide-react'
import type { MediaItem } from '@/lib/types' 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[] }) { export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
const [current, setCurrent] = useState(0) const [current, setCurrent] = useState(0)
const [playing, setPlaying] = useState(false) const [playing, setPlaying] = useState(false)
const [volume, setVolume] = useState(0.35) const [volume, setVolume] = useState(0.4)
const [muted, setMuted] = useState(false) const [muted, setMuted] = useState(false)
const [visible, setVisible] = useState(false)
const [progress, setProgress] = useState(0) 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 audioRef = useRef<HTMLAudioElement>(null)
const track = tracks[current] ?? null const track = tracks[current] ?? null
const trackName = (i: number) =>
tracks[i]?.original_name?.replace(/\.[^/.]+$/, '') ||
tracks[i]?.caption ||
`Titel ${i + 1}`
useEffect(() => { useEffect(() => {
const audio = audioRef.current const audio = audioRef.current
if (!audio) return if (!audio) return
@@ -36,9 +84,10 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
if (!audio || !track) return if (!audio || !track) return
audio.src = `/api/files/${track.filename}` audio.src = `/api/files/${track.filename}`
audio.volume = muted ? 0 : volume audio.volume = muted ? 0 : volume
if (playing) { setDuration(0)
audio.play().catch(() => setPlaying(false)) setElapsed(0)
} setProgress(0)
if (playing) audio.play().catch(() => setPlaying(false))
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [current]) }, [current])
@@ -47,11 +96,19 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
if (!audio) return if (!audio) return
if (playing) { if (playing) {
audio.pause() audio.pause()
setPlaying(false)
} else { } else {
audio.play().catch(() => setPlaying(false)) audio.play().catch(() => setPlaying(false))
}
}
const playTrack = (i: number) => {
if (i === current) {
togglePlay()
} else {
setCurrent(i)
setPlaying(true) setPlaying(true)
} }
setMiniVisible(true)
} }
const prev = () => setCurrent((c) => (c - 1 + tracks.length) % tracks.length) 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 const audio = audioRef.current
if (!audio || !audio.duration) return if (!audio || !audio.duration) return
setProgress((audio.currentTime / audio.duration) * 100) 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>) => { const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -73,11 +136,6 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
if (tracks.length === 0 || !track) return null if (tracks.length === 0 || !track) return null
const trackName =
track.original_name?.replace(/\.[^/.]+$/, '') ||
track.caption ||
`Titel ${current + 1}`
return ( return (
<> <>
<audio <audio
@@ -85,93 +143,137 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
src={`/api/files/${track.filename}`} src={`/api/files/${track.filename}`}
onEnded={next} onEnded={next}
onTimeUpdate={handleTimeUpdate} onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onPlay={() => setPlaying(true)} onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)} onPause={() => setPlaying(false)}
/> />
{/* Floating button */} {/* ── Inline section ── */}
<motion.button <section
onClick={() => setVisible((v) => !v)} id="musik"
whileHover={{ scale: 1.05 }} className="py-20 px-4"
whileTap={{ scale: 0.95 }} style={{ background: 'linear-gradient(to bottom, #0a0706, #060304)' }}
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"
> >
<Music size={20} /> <div className="max-w-xl mx-auto">
{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 && (
<motion.div <motion.div
initial={{ opacity: 0, y: 16, scale: 0.97 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0, scale: 1 }} whileInView={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 16, scale: 0.97 }} viewport={{ once: true }}
transition={{ duration: 0.2 }} className="text-center mb-12"
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"
> >
<div className="flex items-start justify-between mb-4"> <p className="text-amber-200/30 text-xs tracking-[0.5em] uppercase font-lora mb-3">
<div className="flex-1 min-w-0"> Begleitung in Tönen
<p className="text-amber-200 font-cormorant italic text-base leading-snug truncate"> </p>
{trackName} <h2 className="font-cormorant italic text-4xl sm:text-5xl text-amber-100/70 mb-3">
</p> Musik
<p className="text-amber-600 text-xs mt-0.5"> </h2>
{current + 1} / {tracks.length} <div className="flex items-center justify-center gap-4">
</p> <div className="h-px w-12 bg-amber-400/15" />
</div> <span className="text-amber-400/25"></span>
<button <div className="h-px w-12 bg-amber-400/15" />
onClick={() => setVisible(false)} </div>
className="text-amber-800 hover:text-amber-500 ml-2 flex-shrink-0" </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} /> <div className="w-7 flex-shrink-0 flex items-center justify-center">
</button> <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> </div>
{/* Progress bar */} {/* Controls row */}
<input <div className="flex items-center justify-center gap-8 mb-5">
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">
<button <button
onClick={prev} 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>
<button
onClick={togglePlay} <motion.button
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" 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" />} {playing ? <Pause size={22} /> : <Play size={22} className="ml-0.5" />}
</button> </motion.button>
<button <button
onClick={next} 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> </button>
</div> </div>
{/* Volume */} {/* Volume */}
<div className="flex items-center gap-2"> <div className="flex items-center justify-center gap-3">
<button <button
onClick={() => setMuted((m) => !m)} 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} />} {muted ? <VolumeX size={15} /> : <Volume2 size={15} />}
</button> </button>
@@ -185,9 +287,68 @@ export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) {
setVolume(parseFloat(e.target.value)) setVolume(parseFloat(e.target.value))
setMuted(false) 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>
</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> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>