Refonte thème + fixes lecteur + suppression faux stats

AudioPlayer:
- Supprime section vitesse de lecture
- -30s/+30s (compact et expanded), avec icônes RotateCcw/RotateCw
- Rewind compact passé de -15s à -30s

Thème:
- globals.css refait : animations slide-up/fade-in/reveal définies, glass card améliorée
- Background plus profond (#080d1a), glow/blur plus soignés
- Boutons neon-button avec hover translateY

Corrections:
- Supprime Settings2 (bouton sans action) dans Library
- Supprime stat "2.4k mins listened" inventée dans Navigation
- Stats Library passent de 3 à 2 colonnes (réelles uniquement)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-04-16 14:26:19 +02:00
1 parent d3b356ce26
commit b73f06c2b2
4 files changed
+267 -303

No files matched your search

+163 -196
View File
@@ -5,17 +5,12 @@ import {
Play,
Pause,
Volume2,
SkipBack,
SkipForward,
VolumeX,
ChevronDown,
Maximize2,
Minimize2,
List,
VolumeX,
FastForward,
Rewind,
X,
Info
RotateCcw,
RotateCw,
} from 'lucide-react';
interface AudioPlayerProps {
@@ -30,8 +25,6 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const [isSaving, setIsSaving] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
@@ -40,256 +33,230 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
// Save progress periodically
useEffect(() => {
if (!episode || !userId) return;
const saveProgressInterval = setInterval(async () => {
const interval = setInterval(async () => {
if (isPlaying) {
try {
setIsSaving(true);
await episodeService.saveProgress(
episode._id,
Math.floor(currentTime),
false
);
} catch (error) {
console.error('Failed to save progress:', error);
} finally {
setIsSaving(false);
}
await episodeService.saveProgress(episode._id, Math.floor(currentTime), false);
} catch {}
}
}, 30000); // Save every 30 seconds
return () => clearInterval(saveProgressInterval);
}, 30000);
return () => clearInterval(interval);
}, [episode, userId, currentTime, isPlaying]);
// Load previous progress
useEffect(() => {
if (!episode || !userId) return;
const loadProgress = async () => {
try {
const response = await episodeService.getProgress(episode._id);
if (response.progress) {
if (audioRef.current) {
audioRef.current.currentTime = response.progress.position;
}
}
} catch (error) {
console.error('Failed to load progress:', error);
episodeService.getProgress(episode._id).then((r) => {
if (r.progress?.position && audioRef.current) {
audioRef.current.currentTime = r.progress.position;
}
};
loadProgress();
}).catch(() => {});
}, [episode, userId]);
const handlePlayPause = () => {
if (!audioRef.current) return;
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
if (isPlaying) audioRef.current.pause();
else audioRef.current.play();
setIsPlaying(!isPlaying);
};
const handleTimeUpdate = () => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime);
}
};
const handleLoadedMetadata = () => {
if (audioRef.current) {
setDuration(audioRef.current.duration);
}
const handleSeek = (delta: number) => {
if (!audioRef.current) return;
audioRef.current.currentTime = Math.max(0, Math.min(duration, currentTime + delta));
};
const handleProgressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTime = parseFloat(e.target.value);
setCurrentTime(newTime);
if (audioRef.current) {
audioRef.current.currentTime = newTime;
}
const t = parseFloat(e.target.value);
setCurrentTime(t);
if (audioRef.current) audioRef.current.currentTime = t;
};
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value);
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume;
}
const v = parseFloat(e.target.value);
setVolume(v);
setIsMuted(v === 0);
if (audioRef.current) audioRef.current.volume = v;
};
const handleSpeedChange = (speed: number) => {
setPlaybackSpeed(speed);
if (audioRef.current) {
audioRef.current.playbackRate = speed;
}
const toggleMute = () => {
if (!audioRef.current) return;
const muted = !isMuted;
setIsMuted(muted);
audioRef.current.volume = muted ? 0 : volume;
};
const formatTime = (seconds: number) => {
if (!isFinite(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
const formatTime = (s: number) => {
if (!isFinite(s)) return '0:00';
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${sec.toString().padStart(2, '0')}`;
};
if (!episode) return null;
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
<div
className={`fixed bottom-8 left-1/2 -translate-x-1/2 w-[95%] max-w-6xl z-[100] transition-all duration-700 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
isExpanded ? 'h-[80vh] bottom-1/2 translate-y-1/2 rounded-[3rem]' : 'h-24 rounded-[2.5rem]'
} premium-glass p-0 shadow-2xl border-white/10 flex flex-col overflow-hidden animate-slide-up`}
>
<div className={`fixed z-[100] transition-all duration-500 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
isExpanded
? 'inset-0 rounded-none'
: 'bottom-6 left-1/2 -translate-x-1/2 w-[96%] max-w-5xl rounded-[2rem] h-[88px]'
} premium-glass border-white/10 shadow-2xl overflow-hidden flex flex-col`}>
<audio
ref={audioRef}
src={episode.audioUrl}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onTimeUpdate={() => audioRef.current && setCurrentTime(audioRef.current.currentTime)}
onLoadedMetadata={() => audioRef.current && setDuration(audioRef.current.duration)}
onEnded={() => setIsPlaying(false)}
crossOrigin="anonymous"
autoPlay
/>
{/* COMPACT VIEW (DOCK) */}
<div className={`flex items-center gap-6 p-4 h-24 shrink-0 transition-opacity duration-300 ${isExpanded ? 'opacity-0 pointer-events-none absolute' : 'opacity-100'}`}>
{/* Info */}
<div className="flex items-center gap-4 min-w-[200px] lg:min-w-[300px] max-w-[400px]">
<div className="w-16 h-16 rounded-2xl overflow-hidden shadow-lg border border-white/10 shrink-0">
{podcast?.imageUrl ? (
<img src={podcast.imageUrl} alt={podcast.title} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gradient-to-br from-accent-indigo to-accent- violet flex items-center justify-center text-2xl">🎙️</div>
)}
{/* ── COMPACT BAR ── */}
<div className={`flex items-center gap-4 px-5 h-[88px] shrink-0 ${isExpanded ? 'hidden' : 'flex'}`}>
{/* Artwork + info */}
<div className="flex items-center gap-3 min-w-0 flex-1 max-w-[280px]">
<div className="w-13 h-13 w-12 h-12 rounded-xl overflow-hidden border border-white/10 shrink-0 shadow-lg">
{podcast?.imageUrl
? <img src={podcast.imageUrl} alt="" className="w-full h-full object-cover" />
: <div className="w-full h-full bg-gradient-to-br from-accent-indigo to-accent-violet flex items-center justify-center text-xl">🎙️</div>
}
</div>
<div className="overflow-hidden">
<h4 className="text-sm font-bold text-white truncate leading-tight mb-1">{episode.title}</h4>
<p className="text-[10px] text-slate-400 font-bold truncate uppercase tracking-widest">{podcast?.title || 'Unknown Podcast'}</p>
<div className="min-w-0">
<p className="text-sm font-bold text-white truncate leading-tight">{episode.title}</p>
<p className="text-[10px] text-slate-500 font-bold truncate uppercase tracking-wider">{podcast?.title || ''}</p>
</div>
</div>
{/* Playback Controls */}
<div className="flex-1 flex flex-col items-center gap-2">
<div className="flex items-center gap-6">
<button
onClick={() => audioRef.current && (audioRef.current.currentTime -= 15)}
className="text-slate-400 hover:text-white transition-all hover:scale-110 active:scale-90"
>
<Rewind className="w-5 h-5" />
{/* Controls + progress */}
<div className="flex-1 flex flex-col items-center gap-1.5">
<div className="flex items-center gap-5">
<button onClick={() => handleSeek(-30)} className="group flex flex-col items-center gap-0.5 text-slate-500 hover:text-white transition-colors">
<RotateCcw className="w-4 h-4" />
<span className="text-[8px] font-black">30</span>
</button>
<button
onClick={handlePlayPause}
className="w-12 h-12 rounded-full bg-white text-obsidian flex items-center justify-center transition-all hover:scale-110 active:scale-95 shadow-glow-indigo hover:shadow-white/50"
>
{isPlaying ? <Pause className="w-5 h-5 fill-current" /> : <Play className="w-5 h-5 fill-current ml-0.5" />}
<button onClick={handlePlayPause}
className="w-11 h-11 rounded-full bg-white text-obsidian flex items-center justify-center shadow-glow-indigo hover:scale-105 active:scale-95 transition-transform">
{isPlaying ? <Pause className="w-4 h-4 fill-current" /> : <Play className="w-4 h-4 fill-current ml-0.5" />}
</button>
<button
onClick={() => audioRef.current && (audioRef.current.currentTime += 30)}
className="text-slate-400 hover:text-white transition-all hover:scale-110 active:scale-90"
>
<FastForward className="w-5 h-5" />
<button onClick={() => handleSeek(30)} className="group flex flex-col items-center gap-0.5 text-slate-500 hover:text-white transition-colors">
<RotateCw className="w-4 h-4" />
<span className="text-[8px] font-black">30</span>
</button>
</div>
<div className="w-full max-w-xl flex items-center gap-3 group">
<span className="text-[10px] font-bold text-slate-500 w-10 text-right tabular-nums">{formatTime(currentTime)}</span>
<div className="relative flex-1 h-1.5 flex items-center cursor-pointer">
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
<div className="w-full max-w-lg flex items-center gap-2">
<span className="text-[9px] text-slate-600 tabular-nums w-8 text-right">{formatTime(currentTime)}</span>
<div className="relative flex-1 h-1 group cursor-pointer">
<input type="range" min="0" max={duration || 0} value={currentTime}
onChange={handleProgressChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
<div className="w-full h-1 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-accent-indigo to-accent-cyan shadow-[0_0_10px_rgba(99,102,241,0.5)] transition-all duration-100"
style={{ width: `${(currentTime / duration) * 100}%` }}
/>
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" />
<div className="w-full h-full bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-accent-indigo to-accent-cyan rounded-full transition-all duration-100"
style={{ width: `${progress}%` }} />
</div>
</div>
<span className="text-[10px] font-bold text-slate-500 w-10 tabular-nums">{formatTime(duration)}</span>
<span className="text-[9px] text-slate-600 tabular-nums w-8">{formatTime(duration)}</span>
</div>
</div>
{/* Sidebar-like utility */}
<div className="flex items-center gap-4 min-w-[200px] justify-end">
<button
onClick={() => setIsExpanded(true)}
className="p-3 rounded-2xl bg-white/[0.03] text-slate-400 hover:text-white hover:bg-white/10 transition-all"
>
<Maximize2 className="w-4 h-4" />
</button>
<button
onClick={onClose}
className="p-3 rounded-2xl bg-white/[0.03] text-slate-400 hover:text-accent-rose hover:bg-accent-rose/10 transition-all"
>
<X className="w-4 h-4" />
</button>
{/* Right actions */}
<div className="flex items-center gap-2 shrink-0">
<button onClick={toggleMute} className="p-2 text-slate-500 hover:text-white transition-colors">
{isMuted ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
</button>
<button onClick={() => setIsExpanded(true)}
className="p-2 rounded-xl bg-white/5 text-slate-500 hover:text-white hover:bg-white/10 transition-all">
<Maximize2 className="w-4 h-4" />
</button>
<button onClick={onClose}
className="p-2 rounded-xl bg-white/5 text-slate-500 hover:text-accent-rose hover:bg-accent-rose/10 transition-all">
<X className="w-4 h-4" />
</button>
</div>
</div>
{/* EXPANDED VIEW */}
{/* ── EXPANDED VIEW ── */}
{isExpanded && (
<div className="flex-1 flex flex-col p-12 transition-all duration-500 reveal">
<div className="flex justify-between items-start mb-12">
<button onClick={() => setIsExpanded(false)} className="p-4 rounded-3xl bg-white/5 text-slate-400 hover:text-white transition-all">
<ChevronDown className="w-6 h-6" />
</button>
<div className="text-center">
<span className="text-[11px] font-bold text-accent-indigo uppercase tracking-[0.3em] mb-2 block">Premium Experience</span>
<h2 className="text-xl font-display font-extrabold text-white">Now Playing</h2>
</div>
<button onClick={onClose} className="p-4 rounded-3xl bg-white/5 text-slate-400 hover:text-accent-rose transition-all">
<X className="w-6 h-6" />
</button>
<div className="flex-1 flex flex-col p-8 lg:p-16 overflow-y-auto">
{/* Top bar */}
<div className="flex items-center justify-between mb-10">
<button onClick={() => setIsExpanded(false)}
className="p-3 rounded-2xl bg-white/5 text-slate-400 hover:text-white transition-all">
<ChevronDown className="w-5 h-5" />
</button>
<p className="text-[10px] font-black text-accent-indigo uppercase tracking-[0.3em]">En cours de lecture</p>
<button onClick={onClose}
className="p-3 rounded-2xl bg-white/5 text-slate-400 hover:text-accent-rose transition-all">
<X className="w-5 h-5" />
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center flex-1">
<div className="aspect-square w-full max-w-md mx-auto rounded-[3.5rem] overflow-hidden shadow-2xl border-4 border-white/10 transform rotate-1">
{podcast?.imageUrl ? (
<img src={podcast.imageUrl} alt={podcast.title} className="w-full h-full object-cover scale-105" />
) : (
<div className="w-full h-full bg-gradient-to-tr from-accent-indigo via-accent-violet to-accent-rose flex items-center justify-center text-9xl">🎙️</div>
)}
</div>
<div className="flex flex-col lg:flex-row gap-12 items-center flex-1">
{/* Artwork */}
<div className="w-64 h-64 lg:w-80 lg:h-80 rounded-[2.5rem] overflow-hidden shadow-2xl border border-white/10 shrink-0">
{podcast?.imageUrl
? <img src={podcast.imageUrl} alt="" className="w-full h-full object-cover" />
: <div className="w-full h-full bg-gradient-to-br from-accent-indigo to-accent-violet flex items-center justify-center text-7xl">🎙️</div>
}
</div>
<div className="flex flex-col gap-8">
<div>
<h1 className="text-5xl font-display font-black text-white leading-tight mb-4">{episode.title}</h1>
<p className="text-lg text-slate-400 font-medium leading-relaxed line-clamp-4">{episode.description}</p>
</div>
{/* Info + controls */}
<div className="flex flex-col gap-8 flex-1 w-full max-w-lg">
<div>
<p className="text-xs font-black text-accent-indigo uppercase tracking-widest mb-2">{podcast?.title}</p>
<h2 className="text-3xl lg:text-4xl font-display font-black text-white leading-tight mb-3">{episode.title}</h2>
<p className="text-sm text-slate-500 leading-relaxed line-clamp-3">{episode.description}</p>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center mb-2">
<span className="text-xs font-bold text-accent-indigo tabular-nums">{formatTime(currentTime)}</span>
<span className="text-xs font-bold text-slate-500 tabular-nums">{formatTime(duration)}</span>
</div>
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
{/* Progress */}
<div>
<div className="relative h-1.5 group cursor-pointer mb-2">
<input type="range" min="0" max={duration || 0} value={currentTime}
onChange={handleProgressChange}
className="w-full h-2 bg-white/10 rounded-full appearance-none cursor-pointer accent-accent-indigo"
/>
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" />
<div className="w-full h-full bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-accent-indigo to-accent-cyan rounded-full"
style={{ width: `${progress}%` }} />
</div>
</div>
<div className="flex justify-between text-xs text-slate-500 tabular-nums">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
<div className="flex items-center justify-center gap-12">
<button onClick={() => handleSpeedChange(playbackSpeed === 2 ? 1 : playbackSpeed + 0.5)} className="text-xs font-bold text-slate-400 hover:text-white">
SPEED {playbackSpeed}x
</button>
<button onClick={handlePlayPause} className="w-24 h-24 rounded-full bg-white text-obsidian flex items-center justify-center shadow-glow-indigo transform active:scale-90 transition-transform">
{isPlaying ? <Pause className="w-10 h-10 fill-current" /> : <Play className="w-10 h-10 fill-current ml-2" />}
</button>
<div className="flex items-center gap-4">
<Volume2 className="w-5 h-5 text-slate-400" />
<input type="range" min="0" max="1" step="0.1" value={volume} onChange={handleVolumeChange} className="w-32 h-1 bg-white/10 rounded-full accent-white" />
</div>
</div>
</div>
{/* Controls */}
<div className="flex items-center justify-center gap-8">
<button onClick={() => handleSeek(-30)}
className="flex flex-col items-center gap-1 text-slate-400 hover:text-white transition-colors group">
<RotateCcw className="w-6 h-6 group-hover:scale-110 transition-transform" />
<span className="text-[9px] font-black">-30s</span>
</button>
<button onClick={handlePlayPause}
className="w-20 h-20 rounded-full bg-white text-obsidian flex items-center justify-center shadow-glow-indigo hover:scale-105 active:scale-95 transition-transform">
{isPlaying ? <Pause className="w-8 h-8 fill-current" /> : <Play className="w-8 h-8 fill-current ml-1" />}
</button>
<button onClick={() => handleSeek(30)}
className="flex flex-col items-center gap-1 text-slate-400 hover:text-white transition-colors group">
<RotateCw className="w-6 h-6 group-hover:scale-110 transition-transform" />
<span className="text-[9px] font-black">+30s</span>
</button>
</div>
{/* Volume */}
<div className="flex items-center gap-4">
<button onClick={toggleMute} className="text-slate-400 hover:text-white transition-colors shrink-0">
{isMuted ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
</button>
<input type="range" min="0" max="1" step="0.05" value={isMuted ? 0 : volume}
onChange={handleVolumeChange}
className="flex-1 h-1 bg-white/10 rounded-full appearance-none cursor-pointer accent-white" />
</div>
</div>
</div>
</div>
)}
+1 -1
View File
@@ -55,7 +55,7 @@ export default function Navigation() {
{/* Footer */}
<div className="mt-auto pt-6 border-t border-white/5">
<p className="px-2 text-[10px] text-slate-600 font-medium">Podcastic © 2026</p>
<p className="px-2 text-[10px] text-slate-700 font-bold uppercase tracking-widest">Podcastic © 2026</p>
</div>
</nav>