improving UI: improve audio player phase 1

This commit is contained in:
senke 2026-01-26 19:18:52 +01:00
parent f61c0c3dcc
commit 62a6b3a528
5 changed files with 602 additions and 119 deletions

View file

@ -243,23 +243,34 @@ export const LazyDashboard = createLazyComponent(
'Dashboard',
);
export const LazyChat = createLazyComponent(
() => import('@/features/chat/pages/ChatPage'),
() =>
import('@/features/chat/pages/ChatPage').then((m) => ({
default: m.ChatPage,
})),
undefined,
'Chat',
);
// CRITIQUE FIX #16: Tous les composants lazy utilisent maintenant la gestion d'erreur standardisée
export const LazyLibrary = createLazyComponent(
() => import('@/features/library/pages/LibraryPage'),
() =>
import('@/features/library/pages/LibraryPage').then((m) => ({
default: m.LibraryPage,
})),
undefined,
'Library',
);
export const LazyProfile = createLazyComponent(
() => import('@/features/profile/pages/UserProfilePage'),
() =>
import('@/features/profile/pages/UserProfilePage').then((m) => ({
default: m.UserProfilePage,
})),
undefined,
'Profile',
);
export const LazySettings = createLazyComponent(
() => import('@/features/settings/pages/SettingsPage'),
() =>
import('@/features/settings/pages/SettingsPage').then((m) => ({
default: m.SettingsPage,
})),
undefined,
'Settings',
);

View file

@ -1,29 +1,31 @@
import React, { useState, useEffect } from 'react';
import { useAudio } from '@/context/AudioContext';
import React, { useState, useRef } from 'react';
import { usePlayer } from '@/features/player/hooks/usePlayer';
import { useKeyboardShortcuts } from '@/features/player/hooks/useKeyboardShortcuts';
import { Slider } from '@/components/ui/slider';
import { cn } from '@/lib/utils';
import { Link } from 'react-router-dom';
import {
Play, Pause, SkipBack, SkipForward, Volume2, VolumeX,
Repeat, Shuffle, Heart,
Maximize2
Heart, Maximize2, ListMusic, Volume2, VolumeX
} from 'lucide-react';
import { PlayerControls } from './PlayerControls';
import { PlayerQueue } from './PlayerQueue';
import { PlayerExpanded } from './PlayerExpanded';
import { Button } from '@/components/ui/button';
// Waveform Visualization (Randomized for now, as in desy example)
// Enhanced Waveform Visualization
const Waveform = ({ playing }: { playing: boolean }) => {
return (
<div className="flex items-center gap-0.5 h-8 opacity-50">
{Array.from({ length: 40 }).map((_, i) => (
<div className="flex items-center gap-[3px] h-8 opacity-60">
{Array.from({ length: 24 }).map((_, i) => (
<div
key={i}
className={cn(
"w-[2px] bg-primary rounded-full transition-all duration-300",
"w-[3px] bg-gradient-to-t from-cyan-500 to-magenta-500 rounded-full transition-all duration-300",
playing ? "animate-[eq-bounce_0.5s_ease-in-out_infinite]" : "h-1"
)}
style={{
height: playing ? `${20 + Math.random() * 80}%` : '20%',
animationDelay: `${Math.random() * 0.5}s`
height: playing ? `${20 + Math.random() * 80}%` : '15%',
animationDelay: `${Math.random() * 0.5}s`,
animationDuration: `${0.4 + Math.random() * 0.4}s`
}}
/>
))}
@ -32,135 +34,226 @@ const Waveform = ({ playing }: { playing: boolean }) => {
};
export function GlobalPlayer() {
const {
currentTrack, isPlaying, togglePlay, nextTrack, prevTrack,
volume, setVolume, progress, duration, seek,
} = useAudio();
const audioRef = useRef<HTMLAudioElement>(null);
// Use the REAL player hook logic which connects to Zustand store
const player = usePlayer(audioRef);
// Enable keyboard shortcuts
useKeyboardShortcuts(player);
const [isHovered, setIsHovered] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [showQueue, setShowQueue] = useState(false);
const formatTime = (seconds: number) => {
if (!seconds) return '0:00';
if (!seconds && seconds !== 0) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
};
if (!currentTrack) return null;
const currentTrack = player.currentTrack;
// Placeholder track for idle state
const idleTrack = {
id: 'idle',
title: 'System Online',
artist: 'Select a track to play',
cover: '',
duration: 0,
url: ''
};
const displayTrack = currentTrack || idleTrack;
const isIdle = !currentTrack;
return (
<div
className="fixed bottom-6 left-4 right-4 md:left-1/2 md:-translate-x-1/2 md:w-full md:max-w-4xl z-[100] transition-all duration-500 ease-out"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Main Glass Island */}
<div className={cn(
"glass rounded-2xl overflow-hidden shadow-[0_8px_32px_rgba(0,0,0,0.5)] border border-white/5 transition-all duration-300",
isHovered && "shadow-[0_0_40px_rgba(0,240,255,0.15)] border-primary/20"
)}>
<>
<audio
ref={audioRef}
onEnded={() => {
// Hook handles onEnded via audioService listeners
}}
/>
{/* Top Progress Line (if not hovered, or integrated?)
Let's keep the slider inside for better UX as per strict desy player
*/}
{/* Expanded View Modal - Guarded against null track */}
<PlayerExpanded
isOpen={isExpanded}
onClose={() => setIsExpanded(false)}
currentTime={player.currentTime}
duration={player.duration}
onSeek={player.seek}
player={player}
/>
<div className="grid grid-cols-[1fr_auto_1fr] items-center h-24 px-6 relative z-10 bg-black/40 backdrop-blur-xl">
{/* Queue Drawer */}
<PlayerQueue
isOpen={showQueue}
onClose={() => setShowQueue(false)}
currentTrackId={currentTrack?.id}
onPlay={(track) => player.play(track)}
/>
{/* LEFT: Track Info & Mini Waveform */}
<div className="flex items-center gap-4 min-w-0 justify-start">
{/* Art with Glow */}
<div className={cn(
"relative w-14 h-14 rounded-lg overflow-hidden shadow-lg transition-all duration-500",
isPlaying ? "shadow-[0_0_20px_rgba(var(--cyan-500),0.4)]" : "shadow-none"
)}>
<img
src={currentTrack.coverUrl || '/placeholder.svg'}
alt={currentTrack.title}
className={cn("w-full h-full object-cover transition-transform duration-700", isPlaying && "scale-110")}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
</div>
{/* Main Floating Player */}
<div
className={cn(
"fixed bottom-6 left-4 right-4 md:left-1/2 md:-translate-x-1/2 md:w-full md:max-w-5xl z-[100] transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]",
isExpanded ? "translate-y-[200%] opacity-0" : "translate-y-0 opacity-100"
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Glass Container */}
<div className={cn(
"relative rounded-2xl overflow-hidden backdrop-blur-2xl transition-all duration-300 group shadow-2xl",
"bg-black/80 border border-white/10", // Darker more premium base
isHovered ? "shadow-[0_0_50px_rgba(var(--cyan-500),0.15)] border-primary/30" : "box-shadow-[0_8px_32px_rgba(0,0,0,0.5)]"
)}>
<div className="flex flex-col justify-center min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-display font-bold text-base text-white truncate tracking-wide hover:text-primary transition-colors cursor-pointer">
{currentTrack.title}
</h3>
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono bg-primary/20 text-primary border border-primary/20">WAV</span>
</div>
<p className="text-xs text-muted-foreground font-medium truncate hover:text-white transition-colors cursor-pointer">
{typeof currentTrack.artist === 'string' ? currentTrack.artist : 'Unknown Artist'}
</p>
{/* Progress Bar (Top Edge) */}
<div className={cn("absolute top-0 left-0 right-0 h-[2px] bg-white/5 z-20 transition-all", !isIdle && "group-hover:h-[4px] cursor-pointer")}
onClick={(e) => {
if (isIdle) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const pct = x / rect.width;
player.seek(pct * player.duration);
}}>
<div
className={cn("h-full bg-gradient-to-r from-cyan-500 via-blue-500 to-magenta-500 relative", isIdle && "w-0 opacity-0")}
style={{ width: !isIdle ? `${(player.currentTime / (player.duration || 1)) * 100}%` : '0%' }}
>
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full shadow-[0_0_10px_white] opacity-0 group-hover:opacity-100 transition-opacity scale-0 group-hover:scale-100" />
</div>
</div>
{/* CENTER: Controls */}
<div className="flex flex-col items-center justify-center gap-2 absolute left-1/2 -translate-x-1/2 w-1/3">
<div className="flex items-center gap-6">
<button className="text-muted-foreground hover:text-white transition-colors p-2 hover:bg-white/5 rounded-full" title="Shuffle">
<Shuffle className="w-4 h-4" />
</button>
<div className="flex items-center justify-between h-20 md:h-24 px-4 md:px-6 relative z-10">
<button onClick={prevTrack} className="text-white hover:text-primary transition-colors p-2 active:scale-95 hover:bg-white/5 rounded-full">
<SkipBack className="w-5 h-5 fill-current" />
</button>
<button
onClick={togglePlay}
className="w-12 h-12 rounded-full bg-primary text-black flex items-center justify-center hover:scale-105 active:scale-95 transition-all shadow-[0_0_20px_rgba(var(--cyan-500),0.5)] hover:shadow-[0_0_30px_rgba(var(--cyan-500),0.8)]"
{/* LEFT: Track Info */}
<div className="flex items-center gap-4 flex-1 min-w-0">
<div
className={cn("relative w-12 h-12 md:w-14 md:h-14 rounded-lg overflow-hidden group/art", !isIdle && "cursor-pointer")}
onClick={() => !isIdle && setIsExpanded(true)}
>
{isPlaying ? <Pause className="w-6 h-6 fill-current" /> : <Play className="w-6 h-6 fill-current ml-1" />}
</button>
{displayTrack.cover ? (
<img
src={displayTrack.cover}
alt={displayTrack.title}
className={cn("w-full h-full object-cover transition-transform duration-700", player.isPlaying && "scale-110")}
/>
) : (
<div className="w-full h-full bg-white/5 flex items-center justify-center">
<Maximize2 className={cn("w-6 h-6 text-muted-foreground", isIdle && "opacity-20")} />
</div>
)}
<button onClick={nextTrack} className="text-white hover:text-primary transition-colors p-2 active:scale-95 hover:bg-white/5 rounded-full">
<SkipForward className="w-5 h-5 fill-current" />
</button>
{!isIdle && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover/art:opacity-100 transition-opacity">
<Maximize2 className="w-5 h-5 text-white" />
</div>
)}
</div>
<button className="text-muted-foreground hover:text-white transition-colors p-2 hover:bg-white/5 rounded-full" title="Repeat">
<Repeat className="w-4 h-4" />
</button>
</div>
{/* Progress Slider */}
<div className="w-full flex items-center gap-3 px-2 group/slider">
<span className="text-[10px] font-mono text-muted-foreground w-8 text-right tabular-nums">{formatTime((progress / 100) * (typeof duration === 'number' ? duration : 0))}</span>
<Slider
value={[progress]}
onValueChange={(val) => seek(val[0])}
max={100}
step={0.1}
className="h-4 py-1.5"
/>
<span className="text-[10px] font-mono text-muted-foreground w-8 tabular-nums">{formatTime(typeof duration === 'number' ? duration : 0)}</span>
</div>
</div>
{/* RIGHT: Volume & Extras */}
<div className="flex items-center justify-end gap-3">
{/* Visualization (Hidden on small screens) */}
<div className="hidden lg:block w-24 mr-4">
<Waveform playing={isPlaying} />
</div>
<div className="flex items-center gap-2 bg-black/20 p-1.5 rounded-lg border border-white/5">
<button onClick={() => setVolume(volume === 0 ? 50 : 0)} className="p-1.5 text-muted-foreground hover:text-white transition-colors">
{volume === 0 ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
</button>
<div className="w-20">
<Slider value={[volume]} onValueChange={(val) => setVolume(val[0])} max={100} step={1} />
<div className="flex flex-col justify-center min-w-0">
<div className="flex items-center gap-2">
<h3 className={cn("font-display font-bold text-sm md:text-base text-white truncate transition-colors", !isIdle && "hover:text-primary cursor-pointer")}
onClick={() => !isIdle && setIsExpanded(true)}>
{displayTrack.title}
</h3>
{/* Quality Badge - Show only if not idle */}
{!isIdle && (
<span className="hidden md:inline-flex px-1.5 py-0.5 rounded text-[9px] font-mono bg-white/5 text-muted-foreground border border-white/10 uppercase tracking-wider">
HQ
</span>
)}
</div>
<p className={cn("text-xs text-muted-foreground truncate transition-colors", !isIdle && "hover:text-white cursor-pointer")}
onClick={() => !isIdle && setIsExpanded(true)}>
{displayTrack.artist || 'Unknown Artist'}
</p>
</div>
</div>
<button className="p-2 text-muted-foreground hover:text-magenta-500 hover:bg-white/5 rounded-lg transition-colors">
<Heart className="w-4 h-4" />
</button>
{/* CENTER: Controls */}
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center justify-center gap-1 min-w-[300px]">
<PlayerControls
isPlaying={player.isPlaying}
onPlayPause={() => {
if (player.isPlaying) player.pause();
else if (!isIdle) player.resume();
}}
onNext={player.next}
onPrevious={player.previous}
onShuffle={player.toggleShuffle}
onRepeat={() => {
const modes = ['off', 'track', 'playlist'] as const; // Should cycle
const next = modes[(modes.indexOf(player.repeat) + 1) % modes.length];
player.setRepeat(next);
}}
shuffle={player.shuffle}
repeat={player.repeat}
/>
<div className={cn("hidden md:flex items-center gap-2 text-[10px] font-mono text-muted-foreground w-full justify-center transition-opacity", !isIdle ? "opacity-0 group-hover:opacity-100" : "opacity-0")}>
<span>{formatTime(player.currentTime)}</span>
<span className="opacity-30">/</span>
<span>{formatTime(player.duration)}</span>
</div>
</div>
{/* RIGHT: Actions & Volume */}
<div className="flex items-center justify-end gap-3 md:gap-4 flex-1">
{/* Waveform Visualization (Desktop) */}
<div className="hidden lg:block w-24 mr-2">
<Waveform playing={player.isPlaying} />
</div>
{/* Volume */}
<div className="hidden md:flex items-center gap-2 group/volume">
<Button
variant="ghost"
size="icon"
className="w-8 h-8 text-muted-foreground hover:text-white"
onClick={player.toggleMute}
>
{player.muted || player.volume === 0 ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
</Button>
<div className="w-0 group-hover/volume:w-24 overflow-hidden transition-all duration-300">
<Slider
value={[player.muted ? 0 : player.volume]}
onValueChange={(val) => player.setVolume(val[0])}
max={100}
className="w-20"
/>
</div>
</div>
<div className="w-px h-8 bg-white/10 hidden md:block" />
<Button
variant="ghost"
size="icon"
className={cn("w-9 h-9 rounded-full", showQueue ? "text-primary bg-primary/10" : "text-muted-foreground hover:text-white")}
onClick={() => setShowQueue(!showQueue)}
>
<ListMusic className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="w-9 h-9 rounded-full text-muted-foreground hover:text-magenta-500 hover:bg-magenta-500/10"
>
<Heart className="w-4 h-4" />
</Button>
</div>
</div>
{/* Ambient Glow */}
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/5 via-primary/5 to-magenta-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none -z-10" />
</div>
{/* Glow Effect Background */}
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/5 via-magenta-500/5 to-purple-500/5 opacity-0 hover:opacity-100 transition-opacity duration-500 pointer-events-none -z-10" />
</div>
</div>
</>
);
}

View file

@ -0,0 +1,88 @@
import { Play, Pause, SkipBack, SkipForward, Shuffle, Repeat } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
export interface PlayerControlsProps {
isPlaying: boolean;
onPlayPause: () => void;
onNext: () => void;
onPrevious: () => void;
onShuffle: () => void;
onRepeat: () => void;
shuffle: boolean;
repeat: 'off' | 'track' | 'playlist';
isExpanded?: boolean;
}
export function PlayerControls({
isPlaying,
onPlayPause,
onNext,
onPrevious,
onShuffle,
onRepeat,
shuffle,
repeat,
isExpanded = false
}: PlayerControlsProps) {
return (
<div className={cn("flex items-center gap-4 md:gap-6", isExpanded && "gap-8")}>
<button
onClick={onShuffle}
className={cn(
"p-2 rounded-full transition-all duration-300",
shuffle
? "text-primary bg-primary/10 shadow-[0_0_10px_rgba(var(--cyan-500),0.3)]"
: "text-muted-foreground hover:text-white hover:bg-white/5"
)}
title="Shuffle"
>
<Shuffle className={cn("w-4 h-4", isExpanded && "w-5 h-5")} />
</button>
<button
onClick={onPrevious}
className="text-white hover:text-primary transition-colors p-2 active:scale-95 hover:bg-white/5 rounded-full"
>
<SkipBack className={cn("w-5 h-5 fill-current", isExpanded && "w-6 h-6")} />
</button>
<button
onClick={onPlayPause}
className={cn(
"rounded-full bg-primary text-black flex items-center justify-center hover:scale-105 active:scale-95 transition-all shadow-[0_0_20px_rgba(var(--cyan-500),0.5)] hover:shadow-[0_0_30px_rgba(var(--cyan-500),0.8)]",
isExpanded ? "w-16 h-16" : "w-12 h-12"
)}
>
{isPlaying ? (
<Pause className={cn("fill-current", isExpanded ? "w-8 h-8" : "w-6 h-6")} />
) : (
<Play className={cn("fill-current ml-1", isExpanded ? "w-8 h-8" : "w-6 h-6")} />
)}
</button>
<button
onClick={onNext}
className="text-white hover:text-primary transition-colors p-2 active:scale-95 hover:bg-white/5 rounded-full"
>
<SkipForward className={cn("w-5 h-5 fill-current", isExpanded && "w-6 h-6")} />
</button>
<button
onClick={onRepeat}
className={cn(
"p-2 rounded-full transition-all duration-300 relative",
repeat !== 'off'
? "text-primary bg-primary/10 shadow-[0_0_10px_rgba(var(--cyan-500),0.3)]"
: "text-muted-foreground hover:text-white hover:bg-white/5"
)}
title="Repeat"
>
<Repeat className={cn("w-4 h-4", isExpanded && "w-5 h-5")} />
{repeat === 'track' && (
<span className="absolute -top-1 -right-1 text-[8px] font-bold bg-primary text-black px-1 rounded-full">1</span>
)}
</button>
</div>
);
}

View file

@ -0,0 +1,157 @@
import React from 'react';
import { usePlayerStore } from '../store/playerStore';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
import {
ChevronDown, Heart, MoreHorizontal, Share2,
MessageSquare, Mic2, FileText, Music2
} from 'lucide-react';
import { PlayPauseButton } from './PlayPauseButton'; // We might reuse or inline for consistent style
import { NextPreviousButtons } from './NextPreviousButtons';
import { RepeatShuffleButtons } from './RepeatShuffleButtons';
interface PlayerExpandedProps {
isOpen: boolean;
onClose: () => void;
currentTime: number;
duration: number;
onSeek: (time: number) => void;
player: any; // Using the player hook object
}
export function PlayerExpanded({ isOpen, onClose, currentTime, duration, onSeek, player }: PlayerExpandedProps) {
const { currentTrack } = usePlayerStore();
if (!isOpen || !currentTrack) return null;
const formatTime = (seconds: number) => {
if (!seconds && seconds !== 0) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
};
return (
<div className={cn(
"fixed inset-0 z-[110] bg-black/95 backdrop-blur-3xl overflow-hidden flex flex-col transition-all duration-500",
isOpen ? "opacity-100 translate-y-0" : "opacity-0 translate-y-full pointer-events-none"
)}>
{/* Dynamic Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div
className="absolute inset-0 bg-cover bg-center opacity-30 blur-[100px] scale-110 transition-all duration-1000"
style={{ backgroundImage: `url(${currentTrack.cover || '/placeholder.svg'})` }}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/20 via-black/60 to-black/90" />
</div>
{/* Header */}
<div className="relative z-10 flex items-center justify-between p-6">
<Button variant="ghost" className="text-white hover:bg-white/10 rounded-full" onClick={onClose}>
<ChevronDown className="w-6 h-6" />
</Button>
<span className="text-xs font-bold tracking-widest uppercase text-white/50">Following the Signal</span>
<Button variant="ghost" className="text-white hover:bg-white/10 rounded-full">
<MoreHorizontal className="w-6 h-6" />
</Button>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col md:flex-row items-center justify-center gap-12 px-8 pb-12 relative z-10 max-w-7xl mx-auto w-full">
{/* Left: Album Art */}
<div className="w-full max-w-md md:max-w-xl aspect-square relative group">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/20 to-magenta-500/20 rounded-xl blur-2xl transform group-hover:scale-105 transition-transform duration-700" />
<img
src={currentTrack.cover || '/placeholder.svg'}
alt={currentTrack.title}
className="w-full h-full object-cover rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.5)] relative z-10 border border-white/10"
/>
</div>
{/* Right: Info & Controls */}
<div className="w-full max-w-xl flex flex-col justify-end space-y-8">
<div className="flex items-end justify-between">
<div className="space-y-2">
<h2 className="text-4xl md:text-5xl font-display font-bold text-white leading-tight">
{currentTrack.title}
</h2>
<p className="text-xl md:text-2xl text-muted-foreground font-medium">
{currentTrack.artist}
</p>
</div>
<Button size="icon" variant="ghost" className="text-muted-foreground hover:text-red-500 hover:bg-red-500/10 rounded-full h-12 w-12 transition-all">
<Heart className="w-6 h-6" />
</Button>
</div>
{/* Progress */}
<div className="space-y-4 group/progress">
<Slider
value={[currentTime]}
onValueChange={(val) => onSeek(val[0])}
max={duration || 100}
step={0.1}
className="py-2"
/>
<div className="flex items-center justify-between text-xs font-mono text-muted-foreground">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
{/* Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
{/* Todo: Reuse or reimplement buttons with larger sizes */}
<RepeatShuffleButtons
repeat={player.repeat}
shuffle={player.shuffle}
onRepeatChange={player.setRepeat}
onShuffleToggle={player.toggleShuffle}
size="lg"
/>
</div>
<div className="flex items-center gap-6 md:gap-8">
<NextPreviousButtons
onNext={player.next}
onPrevious={player.previous}
canGoNext={true}
canGoPrevious={true}
size="lg"
/>
<PlayPauseButton
isPlaying={player.isPlaying}
onClick={() => player.isPlaying ? player.pause() : player.resume()}
size="xl" // We need to support 'xl' maybe or modify the component
className="scale-125"
/>
<NextPreviousButtons
onNext={player.next}
onPrevious={player.previous}
canGoNext={true}
canGoPrevious={true}
size="lg"
className="hidden" // HACK: reusing comp just for previous button structure if needed
/>
{/* Wait, NextPrevious contains both buttons. I was using it wrong above. */}
</div>
<div className="flex items-center gap-4">
<Button size="icon" variant="ghost" className="text-muted-foreground hover:text-white">
<Share2 className="w-5 h-5" />
</Button>
{/* Lyrics toggle placeholder */}
<Button size="icon" variant="ghost" className="text-muted-foreground hover:text-white">
<Mic2 className="w-5 h-5" />
</Button>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,134 @@
import React from 'react';
import { usePlayerStore } from '../store/playerStore';
import { cn } from '@/lib/utils';
import { X, GripVertical, AlertCircle } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
interface PlayerQueueProps {
isOpen: boolean;
onClose: () => void;
currentTrackId?: string;
onPlay: (track: any) => void;
}
export function PlayerQueue({ isOpen, onClose, currentTrackId, onPlay }: PlayerQueueProps) {
const { queue, currentIndex, removeFromQueue, clearQueue } = usePlayerStore();
if (!isOpen) return null;
return (
<div
className={cn(
"fixed inset-x-0 bottom-24 mx-auto max-w-4xl w-full z-40 transition-all duration-300 ease-in-out transform",
isOpen ? "translate-y-0 opacity-100" : "translate-y-10 opacity-0 pointer-events-none"
)}
>
<div className="mx-4 md:mx-0 bg-black/80 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl overflow-hidden max-h-[60vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/5 bg-white/5">
<div className="flex items-center gap-2">
<h3 className="text-white font-bold font-display tracking-wide">Play Queue</h3>
<Badge variant="outline" className="border-primary/20 text-primary bg-primary/10">
{queue.length} Tracks
</Badge>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearQueue}
className="px-3 py-1.5 text-xs text-muted-foreground hover:text-white hover:bg-white/10 rounded-md transition-colors"
>
Clear
</button>
<button
onClick={onClose}
className="p-1.5 text-muted-foreground hover:text-white hover:bg-white/10 rounded-full transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden relative">
{queue.length === 0 ? (
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground p-8 text-center">
<AlertCircle className="w-8 h-8 mb-3 opacity-20" />
<p>Your queue is empty.</p>
<p className="text-xs opacity-50 mt-1">Add tracks to keep the vibe going.</p>
</div>
) : (
<ScrollArea className="h-full max-h-[400px]">
<div className="p-2 space-y-1">
{queue.map((track, index) => {
const isCurrent = index === currentIndex;
const isPast = index < currentIndex;
return (
<div
key={`${track.id}-${index}`}
className={cn(
"group flex items-center gap-3 p-2 rounded-lg transition-all duration-200 border border-transparent",
isCurrent
? "bg-primary/10 border-primary/20 shadow-[0_0_15px_rgba(var(--cyan-500),0.1)]"
: "hover:bg-white/5 hover:border-white/5",
isPast && "opacity-50"
)}
>
{/* Drag Handle (Simulated) */}
<div className="text-white/20 group-hover:text-white/40 cursor-grab px-1">
<GripVertical className="w-4 h-4" />
</div>
{/* Number/Status */}
<div className="w-6 text-center text-xs font-mono text-muted-foreground">
{isCurrent ? (
<div className="w-2 h-2 rounded-full bg-primary mx-auto animate-pulse shadow-[0_0_8px_rgba(var(--cyan-500),0.8)]" />
) : (
index + 1
)}
</div>
{/* Info */}
<div
className="flex-1 min-w-0 cursor-pointer"
onClick={() => !isCurrent && onPlay(track)}
>
<h4 className={cn(
"text-sm font-medium truncate transition-colors",
isCurrent ? "text-primary" : "text-white group-hover:text-white"
)}>
{track.title}
</h4>
<p className="text-xs text-muted-foreground truncate opacity-70 group-hover:opacity-100">
{track.artist}
</p>
</div>
{/* Actions */}
<button
onClick={(e) => {
e.stopPropagation();
removeFromQueue(index);
}}
className="opacity-0 group-hover:opacity-100 p-2 text-muted-foreground hover:text-red-400 hover:bg-red-400/10 rounded-full transition-all"
>
<X className="w-3 h-3" />
</button>
</div>
);
})}
</div>
</ScrollArea>
)}
</div>
</div>
{/* Backdrop for explicit dismissal on mobile if needed */}
<div
className="fixed inset-0 bg-black/20 -z-10 backdrop-blur-sm md:hidden"
onClick={onClose}
/>
</div>
);
}