Import source: src/components/Player/PlayerBar
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,146 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import Waveform3D from './Waveform3D'
|
||||||
|
|
||||||
|
export default function PlayerBar({ currentTrack, isPlaying, progress, volume, onToggle, onNext, onPrev, onSeek, onVolume }) {
|
||||||
|
const [liked, setLiked] = useState(false)
|
||||||
|
const [shuffled, setShuffled] = useState(false)
|
||||||
|
const [repeated, setRepeated] = useState(false)
|
||||||
|
|
||||||
|
if (!currentTrack) return null
|
||||||
|
|
||||||
|
const pct = `${(progress * 100).toFixed(1)}%`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.footer
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className="fixed bottom-0 left-0 right-0 z-50 glass-dark"
|
||||||
|
style={{ borderTop: '1px solid rgba(201,168,76,0.1)', boxShadow: '0 -16px 48px rgba(0,0,0,0.7)' }}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-5 h-20 flex items-center gap-4 md:gap-6">
|
||||||
|
|
||||||
|
{/* ── Track info ──────────────────── */}
|
||||||
|
<div className="flex items-center gap-3 w-48 md:w-56 flex-shrink-0 min-w-0">
|
||||||
|
<div className="relative w-11 h-11 rounded-lg overflow-hidden flex-shrink-0 shadow-card">
|
||||||
|
<img src={currentTrack.cover} alt={currentTrack.title} className="w-full h-full object-cover" />
|
||||||
|
{isPlaying && (
|
||||||
|
<div className="absolute inset-0 rounded-lg"
|
||||||
|
style={{ boxShadow: 'inset 0 0 0 1.5px rgba(201,168,76,0.6)', animation: 'pulse-gold 2s ease-in-out infinite' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-white text-xs font-semibold truncate leading-snug">{currentTrack.title}</p>
|
||||||
|
<p className="text-secondary text-xs truncate">{currentTrack.artist}</p>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.2 }}
|
||||||
|
whileTap={{ scale: 0.85 }}
|
||||||
|
onClick={() => setLiked(l => !l)}
|
||||||
|
className="flex-shrink-0 transition-colors"
|
||||||
|
style={{ color: liked ? 'var(--gold)' : 'var(--text-muted)' }}
|
||||||
|
>
|
||||||
|
<svg className="w-3.5 h-3.5" fill={liked ? 'currentColor' : 'none'}
|
||||||
|
stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8}
|
||||||
|
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||||
|
</svg>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Centre: controls + progress ─ */}
|
||||||
|
<div className="flex-1 flex flex-col items-center gap-1.5 min-w-0">
|
||||||
|
{/* Control buttons */}
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
{/* Shuffle */}
|
||||||
|
<motion.button whileHover={{ scale: 1.15 }} whileTap={{ scale: 0.88 }}
|
||||||
|
onClick={() => setShuffled(s => !s)}
|
||||||
|
style={{ color: shuffled ? 'var(--gold)' : 'var(--text-muted)' }}
|
||||||
|
className="hidden sm:block transition-colors">
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8}
|
||||||
|
d="M16 3l4 4-4 4M20 7H4M8 21l-4-4 4-4M4 17h16" />
|
||||||
|
</svg>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Prev */}
|
||||||
|
<motion.button whileHover={{ scale: 1.12 }} whileTap={{ scale: 0.88 }}
|
||||||
|
onClick={onPrev} className="text-secondary hover:text-white transition-colors">
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M6 6h2v12H6zm3.5 6 8.5 6V6z" />
|
||||||
|
</svg>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Play/Pause */}
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.08, boxShadow: '0 0 28px rgba(201,168,76,0.45)' }}
|
||||||
|
whileTap={{ scale: 0.92 }}
|
||||||
|
onClick={onToggle}
|
||||||
|
className="w-9 h-9 rounded-full flex items-center justify-center text-dark-base shadow-gold-sm"
|
||||||
|
style={{ background: 'linear-gradient(135deg, #c9a84c, #e8c547)' }}
|
||||||
|
>
|
||||||
|
{isPlaying
|
||||||
|
? <svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /></svg>
|
||||||
|
: <svg className="w-3.5 h-3.5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
|
||||||
|
}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Next */}
|
||||||
|
<motion.button whileHover={{ scale: 1.12 }} whileTap={{ scale: 0.88 }}
|
||||||
|
onClick={onNext} className="text-secondary hover:text-white transition-colors">
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
|
||||||
|
</svg>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Repeat */}
|
||||||
|
<motion.button whileHover={{ scale: 1.15 }} whileTap={{ scale: 0.88 }}
|
||||||
|
onClick={() => setRepeated(r => !r)}
|
||||||
|
style={{ color: repeated ? 'var(--gold)' : 'var(--text-muted)' }}
|
||||||
|
className="hidden sm:block transition-colors">
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8}
|
||||||
|
d="M17 1l4 4-4 4M3 11V9a4 4 0 014-4h14M7 23l-4-4 4-4M21 13v2a4 4 0 01-4 4H3" />
|
||||||
|
</svg>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="w-full max-w-md flex items-center gap-2.5">
|
||||||
|
<span className="text-xs text-muted font-mono w-8 text-right flex-shrink-0">0:00</span>
|
||||||
|
<div className="flex-1 h-1 rounded-full cursor-pointer group relative"
|
||||||
|
style={{ background: 'var(--dark-border)' }}
|
||||||
|
onClick={e => {
|
||||||
|
const r = e.currentTarget.getBoundingClientRect()
|
||||||
|
onSeek((e.clientX - r.left) / r.width)
|
||||||
|
}}>
|
||||||
|
<div className="h-full rounded-full relative transition-all"
|
||||||
|
style={{ width: pct, background: 'linear-gradient(90deg, #c9a84c, #e8c547)' }}>
|
||||||
|
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 rounded-full bg-white scale-0 group-hover:scale-100 transition-transform shadow-gold-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted font-mono w-8 flex-shrink-0">{currentTrack.duration}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Right: waveform + volume ─────── */}
|
||||||
|
<div className="hidden lg:flex items-center gap-4 w-52 flex-shrink-0 justify-end">
|
||||||
|
<div className="w-24 h-9">
|
||||||
|
<Waveform3D isPlaying={isPlaying} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg className="w-3.5 h-3.5 text-muted flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z" />
|
||||||
|
</svg>
|
||||||
|
<input type="range" min="0" max="1" step="0.01"
|
||||||
|
value={volume} onChange={e => onVolume(parseFloat(e.target.value))}
|
||||||
|
className="w-20" style={{ accentColor: 'var(--gold)' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.footer>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user