mirror of
https://github.com/R0m1k3/podcastic.git
synced 2026-10-22 06:28:03 +02:00
feat: initialize frontend entry point with HTML boilerplate and custom fonts
This commit is contained in:
1 parent
f441251bb1
commit
6e9da01488
14 files changed
+872
-907
No files matched your search
@@ -8,6 +8,14 @@ import {
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
ChevronDown,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
List,
|
||||
VolumeX,
|
||||
FastForward,
|
||||
Rewind,
|
||||
X,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
|
||||
interface AudioPlayerProps {
|
||||
@@ -24,6 +32,8 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
|
||||
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);
|
||||
|
||||
const podcast = typeof episode?.podcastId === 'object' ? episode.podcastId : null;
|
||||
|
||||
@@ -124,172 +134,165 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
if (!episode) {
|
||||
return null;
|
||||
}
|
||||
if (!episode) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-white z-[100] overflow-y-auto animate-fade-in"
|
||||
onClick={(e) => {
|
||||
// Close if clicking the backdrop itself
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
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="min-h-full gradient-light">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white/80 backdrop-blur-xl border-b border-light-200">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-light-900">En cours de lecture</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-light-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ChevronDown className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Player Content */}
|
||||
<div className="max-w-4xl mx-auto px-4 py-12">
|
||||
{/* Artwork */}
|
||||
<div className="aspect-square max-w-sm mx-auto mb-8 rounded-3xl overflow-hidden shadow-2xl">
|
||||
{podcast?.imageUrl ? (
|
||||
<img
|
||||
src={podcast.imageUrl}
|
||||
alt={podcast.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-primary flex items-center justify-center text-white text-9xl">
|
||||
🎙️
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={episode.audioUrl}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
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="text-center mb-8">
|
||||
<p className="text-sm font-semibold text-light-500 uppercase mb-2">
|
||||
{podcast?.author || 'Podcast inconnu'}
|
||||
</p>
|
||||
<h1 className="text-4xl font-bold text-light-900 mb-3">{episode.title}</h1>
|
||||
<p className="text-light-600 line-clamp-3">{episode.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-6 card">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration || 0}
|
||||
value={currentTime}
|
||||
onChange={handleProgressChange}
|
||||
className="w-full h-2 bg-light-200 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-light-600 mt-2">
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
<span>{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center justify-center gap-6 mb-8">
|
||||
{/* Previous 15s */}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = Math.max(0, currentTime - 15);
|
||||
}
|
||||
}}
|
||||
className="p-3 hover:bg-light-100 rounded-full transition-colors"
|
||||
title="Reculer de 15s"
|
||||
>
|
||||
<SkipBack className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Play/Pause */}
|
||||
<button
|
||||
onClick={handlePlayPause}
|
||||
className="w-20 h-20 rounded-full bg-gradient-primary text-white flex items-center justify-center hover:shadow-lg transition-shadow"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause className="w-8 h-8 ml-1" fill="currentColor" />
|
||||
<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" />
|
||||
) : (
|
||||
<Play className="w-8 h-8 ml-1" fill="currentColor" />
|
||||
<div className="w-full h-full bg-gradient-to-br from-accent-indigo to-accent- violet flex items-center justify-center text-2xl">🎙️</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Next 30s */}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = Math.min(duration, currentTime + 30);
|
||||
}
|
||||
}}
|
||||
className="p-3 hover:bg-light-100 rounded-full transition-colors"
|
||||
title="Avancer de 30s"
|
||||
>
|
||||
<SkipForward className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Speed & Volume */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Playback Speed */}
|
||||
<div className="card">
|
||||
<h3 className="text-sm font-semibold text-light-900 mb-3">Vitesse</h3>
|
||||
<div className="flex gap-2">
|
||||
{[0.75, 1, 1.25, 1.5, 1.75, 2].map((speed) => (
|
||||
<button
|
||||
key={speed}
|
||||
onClick={() => handleSpeedChange(speed)}
|
||||
className={`flex-1 py-2 rounded-lg font-medium transition-colors ${
|
||||
playbackSpeed === speed
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-light-100 text-light-700 hover:bg-light-200'
|
||||
}`}
|
||||
>
|
||||
{speed}x
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Volume */}
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Volume2 className="w-4 h-4 text-light-600" />
|
||||
<h3 className="text-sm font-semibold text-light-900">Volume</h3>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-full h-2 bg-light-200 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Hidden Audio Element */}
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={episode.audioUrl}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
onEnded={() => setIsPlaying(false)}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLAudioElement;
|
||||
console.error('Audio playback error:', target.error);
|
||||
alert("Erreur de lecture audio. Le lien est peut-être mort ou bloqué.");
|
||||
}}
|
||||
crossOrigin="anonymous"
|
||||
autoPlay
|
||||
/>
|
||||
{/* 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" />
|
||||
</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>
|
||||
<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>
|
||||
</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}
|
||||
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}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-slate-500 w-10 tabular-nums">{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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<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 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>
|
||||
|
||||
<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}
|
||||
onChange={handleProgressChange}
|
||||
className="w-full h-2 bg-white/10 rounded-full appearance-none cursor-pointer accent-accent-indigo"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Episode } from '../services/episodeService';
|
||||
import { Play } from 'lucide-react';
|
||||
import { Play, Clock, Calendar } from 'lucide-react';
|
||||
|
||||
interface EpisodeCardProps {
|
||||
episode: Episode;
|
||||
@@ -11,52 +11,59 @@ export default function EpisodeCard({ episode, onPlay }: EpisodeCardProps) {
|
||||
const pubDate = new Date(episode.pubDate).toLocaleDateString('fr-FR', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
|
||||
const durationMinutes = Math.floor(episode.duration / 60);
|
||||
|
||||
return (
|
||||
<div className="group cursor-pointer">
|
||||
<div className="relative rounded-2xl overflow-hidden mb-3 glossy">
|
||||
{/* Podcast Image */}
|
||||
<div className="aspect-square bg-light-200 flex items-center justify-center relative overflow-hidden">
|
||||
{podcast?.imageUrl ? (
|
||||
<img
|
||||
src={podcast.imageUrl}
|
||||
alt={podcast.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-primary flex items-center justify-center text-white text-4xl">
|
||||
🎙️
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Play Button Overlay */}
|
||||
<button
|
||||
onClick={() => onPlay(episode)}
|
||||
className="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/40 transition-all duration-300"
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-white/90 group-hover:bg-white flex items-center justify-center shadow-lg transform group-hover:scale-110 transition-transform">
|
||||
<Play className="w-6 h-6 text-blue-500 ml-1" fill="currentColor" />
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
className="group premium-glass rounded-[2rem] p-4 hover:bg-white/5 transition-all duration-500 border-white/5 hover:border-white/10 flex flex-col h-full cursor-pointer"
|
||||
onClick={() => onPlay(episode)}
|
||||
>
|
||||
{/* Visual Header */}
|
||||
<div className="relative aspect-square rounded-2xl overflow-hidden mb-5 shadow-xl border border-white/5">
|
||||
{podcast?.imageUrl ? (
|
||||
<img
|
||||
src={podcast.imageUrl}
|
||||
alt={podcast.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-to-br from-accent-indigo to-accent-rose flex items-center justify-center text-4xl">🎙️</div>
|
||||
)}
|
||||
|
||||
{/* Hover Play Button Overlay */}
|
||||
<div className="absolute inset-0 bg-obsidian/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center backdrop-blur-[2px]">
|
||||
<div className="w-16 h-16 rounded-full bg-white text-obsidian flex items-center justify-center shadow-glow-indigo transform scale-90 group-hover:scale-100 transition-transform duration-500">
|
||||
<Play className="w-6 h-6 fill-current ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Duration Badge */}
|
||||
<div className="absolute bottom-3 right-3 px-2 py-1 rounded-lg bg-black/60 backdrop-blur-md border border-white/10 text-[9px] font-black text-white uppercase tracking-widest">
|
||||
{durationMinutes} MIN
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Podcast Title */}
|
||||
<p className="text-xs font-semibold text-light-500 uppercase tracking-wider mb-1">
|
||||
{podcast?.author || 'Inconnu'}
|
||||
</p>
|
||||
|
||||
{/* Episode Title */}
|
||||
<h3 className="text-sm font-bold text-light-900 line-clamp-2 mb-2 group-hover:text-blue-500 transition-colors">
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[9px] font-black text-accent-indigo uppercase tracking-[0.2em] truncate flex-1">
|
||||
{podcast?.author || 'Artiste Premium'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-bold text-white line-clamp-2 mb-4 leading-relaxed group-hover:text-accent-indigo transition-colors flex-1">
|
||||
{episode.title}
|
||||
</h3>
|
||||
|
||||
{/* Episode Meta */}
|
||||
<div className="flex items-center justify-between text-xs text-light-500">
|
||||
<span>{pubDate}</span>
|
||||
<span>{durationMinutes} min</span>
|
||||
{/* Footer Meta */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/5 text-[10px] font-bold text-slate-500 uppercase tracking-widest">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
<span>{pubDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { LogOut, User as UserIcon } from 'lucide-react';
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
@@ -9,50 +10,49 @@ interface HeaderProps {
|
||||
|
||||
export default function Header({ title, subtitle, user, onLogout }: HeaderProps) {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-xl border-b border-light-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Logo & Title */}
|
||||
<div className="flex-1">
|
||||
<Link to="/" className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-primary flex items-center justify-center text-white font-bold">
|
||||
🎙️
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-light-900">{title}</h1>
|
||||
{subtitle && <p className="text-sm text-light-500">{subtitle}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
<header className="mb-12">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Title Section */}
|
||||
<div className="flex-1">
|
||||
<div className="inline-block px-3 py-1 rounded-full bg-accent-indigo/10 border border-accent-indigo/20 text-[10px] font-bold text-accent-indigo uppercase tracking-widest mb-3">
|
||||
{subtitle || "Exploration Audio"}
|
||||
</div>
|
||||
|
||||
{/* User Menu */}
|
||||
{user && (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{user.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.username}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-light-300 flex items-center justify-center text-sm font-semibold text-light-700">
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium text-light-900">{user.username}</span>
|
||||
</div>
|
||||
{onLogout && (
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="px-4 py-2 text-sm text-light-600 hover:text-light-900 transition-colors"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-4xl lg:text-5xl font-display font-extrabold text-white tracking-tight text-balance leading-tight">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* User Profile */}
|
||||
{user && (
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="hidden sm:flex flex-col items-end">
|
||||
<span className="text-sm font-bold text-white tracking-wide">{user.username}</span>
|
||||
<span className="text-[10px] font-medium text-slate-500 uppercase tracking-widest">Premium Member</span>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<div className="w-12 h-12 rounded-2xl p-[1px] bg-gradient-to-tr from-accent-indigo/50 to-accent-rose/50 group-hover:from-accent-indigo group-hover:to-accent-rose transition-all">
|
||||
<div className="w-full h-full rounded-2xl bg-obsidian-800 flex items-center justify-center overflow-hidden">
|
||||
{user.avatar ? (
|
||||
<img src={user.avatar} alt={user.username} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span className="text-lg font-bold text-accent-indigo">{user.username.charAt(0).toUpperCase()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Popup (Minimal) */}
|
||||
{onLogout && (
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="absolute top-full right-0 mt-4 opacity-0 scale-90 translate-y-2 group-hover:opacity-100 group-hover:scale-100 group-hover:translate-y-0 transition-all duration-300 premium-glass px-4 py-2 flex items-center gap-2 hover:bg-white/10"
|
||||
>
|
||||
<LogOut className="w-4 h-4 text-accent-rose" />
|
||||
<span className="text-xs font-bold text-white whitespace-nowrap">Logout</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -1,78 +1,90 @@
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Home, Flame, PlusCircle, BookOpen } from 'lucide-react';
|
||||
import { Home, Flame, PlusCircle, BookOpen, Music2, Search, Library } from 'lucide-react';
|
||||
|
||||
export default function Navigation() {
|
||||
const location = useLocation();
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
const navItems = [
|
||||
{ path: '/dashboard', label: 'Dashboard', icon: Home, color: 'text-accent-indigo' },
|
||||
{ path: '/trending', label: 'Trending', icon: Flame, color: 'text-accent-rose' },
|
||||
{ path: '/library', label: 'Library', icon: Library, color: 'text-accent-cyan' },
|
||||
{ path: '/add', label: 'Add Podcast', icon: PlusCircle, color: 'text-accent-violet' },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="fixed left-0 top-0 h-screen w-64 bg-white border-r border-light-200 pt-6 overflow-y-auto hidden lg:flex flex-col">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="px-6 mb-8 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-primary flex items-center justify-center text-white font-bold">
|
||||
🎙️
|
||||
<>
|
||||
{/* Desktop Sidebar */}
|
||||
<nav className="fixed left-6 top-6 bottom-6 w-72 h-auto premium-glass z-50 p-6 hidden lg:flex flex-col gap-8 rounded-[2.5rem]">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center gap-4 group">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-accent-indigo to-accent-rose flex items-center justify-center text-2xl shadow-glow-indigo transition-transform group-hover:scale-110">
|
||||
🎙️
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-display font-extrabold text-white tracking-tight">PODCASTIC</span>
|
||||
<span className="text-[10px] text-slate-500 font-bold uppercase tracking-[0.2em]">Premium Audio</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex-1 space-y-2">
|
||||
{navItems.map((item) => {
|
||||
const ActiveIcon = item.icon;
|
||||
const active = isActive(item.path);
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`group relative flex items-center gap-4 px-5 py-4 rounded-2xl transition-all duration-300 ${
|
||||
active
|
||||
? 'bg-white/10 text-white shadow-lg shadow-black/20'
|
||||
: 'text-slate-400 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{active && (
|
||||
<div className={`absolute left-0 w-1.5 h-6 rounded-full bg-accent-indigo shadow-[0_0_12px_rgba(99,102,241,0.8)]`} />
|
||||
)}
|
||||
<ActiveIcon className={`w-5 h-5 transition-transform group-hover:scale-110 ${active ? 'text-accent-indigo' : ''}`} />
|
||||
<span className="text-sm font-semibold tracking-wide">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className="text-xl font-bold text-light-900">Podcastic</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex-1 px-4">
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive('/dashboard')
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'text-light-700 hover:bg-light-100'
|
||||
}`}
|
||||
>
|
||||
<Home className="w-5 h-5" />
|
||||
<span className="font-medium">Tableau de bord</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/trending"
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive('/trending')
|
||||
? 'bg-orange-500 text-white'
|
||||
: 'text-light-700 hover:bg-light-100'
|
||||
}`}
|
||||
>
|
||||
<Flame className="w-5 h-5" />
|
||||
<span className="font-medium">Tendance</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/library"
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive('/library')
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'text-light-700 hover:bg-light-100'
|
||||
}`}
|
||||
>
|
||||
<BookOpen className="w-5 h-5" />
|
||||
<span className="font-medium">Bibliothèque</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/add"
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive('/add')
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'text-light-700 hover:bg-light-100'
|
||||
}`}
|
||||
>
|
||||
<PlusCircle className="w-5 h-5" />
|
||||
<span className="font-medium">Ajouter</span>
|
||||
</Link>
|
||||
{/* Footer info/stats */}
|
||||
<div className="mt-auto pt-6 border-t border-white/5 space-y-4">
|
||||
<div className="p-4 rounded-3xl bg-white/[0.03] border border-white/[0.05]">
|
||||
<p className="text-[10px] text-slate-500 font-bold uppercase mb-2">My Stats</p>
|
||||
<div className="flex justify-between items-end">
|
||||
<span className="text-2xl font-display font-extrabold text-white">2.4k</span>
|
||||
<span className="text-[10px] text-accent-indigo font-bold">Mins Listened</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="px-2 text-[10px] text-slate-600 font-medium">Podcastic Premium © 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-light-200 text-xs text-light-500">
|
||||
<p>Podcastic © 2026</p>
|
||||
</div>
|
||||
</nav>
|
||||
{/* Mobile Bottom Nav */}
|
||||
<nav className="fixed bottom-6 left-6 right-6 h-20 premium-glass lg:hidden z-50 flex items-center justify-around px-4 rounded-[2rem] shadow-2xl">
|
||||
{navItems.map((item) => {
|
||||
const MobileIcon = item.icon;
|
||||
const active = isActive(item.path);
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`flex flex-col items-center gap-1.5 transition-all duration-300 ${
|
||||
active ? 'scale-110 text-accent-indigo' : 'text-slate-500'
|
||||
}`}
|
||||
>
|
||||
<MobileIcon className="w-6 h-6" />
|
||||
{active && <div className="w-1 h-1 rounded-full bg-accent-indigo shadow-[0_0_8px_rgba(99,102,241,1)]" />}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckCircle2, ChevronRight, BookOpen } from 'lucide-react';
|
||||
import { CheckCircle2, ChevronRight, BookOpen, Sparkles, X } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface SuccessModalProps {
|
||||
@@ -24,61 +24,63 @@ export default function SuccessModal({
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm animate-fade-in"
|
||||
className="absolute inset-0 bg-obsidian/60 backdrop-blur-md animate-fade-in"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white rounded-3xl shadow-2xl w-full max-w-sm overflow-hidden animate-slide-up">
|
||||
{/* Top Gradient */}
|
||||
<div className="h-2 bg-gradient-primary w-full" />
|
||||
{/* Modal Content */}
|
||||
<div className="relative premium-glass rounded-[3.5rem] w-full max-w-sm overflow-hidden animate-slide-up shadow-[0_0_50px_rgba(0,0,0,0.5)] border-white/10">
|
||||
{/* Decorative Aura */}
|
||||
<div className="absolute -top-24 -right-24 w-48 h-48 bg-accent-indigo/20 blur-[60px] rounded-full" />
|
||||
|
||||
<div className="p-8 flex flex-col items-center text-center">
|
||||
{/* Animated Success Icon */}
|
||||
<div className="w-16 h-16 bg-green-50 rounded-full flex items-center justify-center mb-6 animate-pulse">
|
||||
<CheckCircle2 className="w-8 h-8 text-green-500" />
|
||||
<div className="p-10 flex flex-col items-center text-center relative z-10">
|
||||
{/* Header Actions */}
|
||||
<button onClick={onClose} className="absolute top-0 right-0 p-6 text-slate-500 hover:text-white transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="w-20 h-20 bg-accent-cyan/10 rounded-3xl flex items-center justify-center mb-8 shadow-glow-cyan border border-accent-cyan/20">
|
||||
<Sparkles className="w-8 h-8 text-accent-cyan animate-pulse" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-light-900 mb-2">
|
||||
<h3 className="text-2xl font-display font-black text-white mb-2 leading-tight">
|
||||
{message}
|
||||
</h3>
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-8">Nouveau membre de votre univers</p>
|
||||
|
||||
<div className="w-full bg-light-50 rounded-2xl p-4 mb-8 flex items-center gap-4 border border-light-100">
|
||||
{podcastImage ? (
|
||||
<img
|
||||
src={podcastImage}
|
||||
alt={podcastTitle}
|
||||
className="w-16 h-16 rounded-xl object-cover shadow-sm"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-xl bg-light-200 flex items-center justify-center text-2xl">
|
||||
🎙️
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full bg-white/[0.03] rounded-[2rem] p-5 mb-10 flex items-center gap-5 border border-white/5">
|
||||
<div className="w-20 h-20 rounded-2xl overflow-hidden shadow-2xl border border-white/10 shrink-0">
|
||||
{podcastImage ? (
|
||||
<img src={podcastImage} alt={podcastTitle} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-to-br from-accent-indigo to-accent-rose flex items-center justify-center text-2xl">🎙️</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-left flex-1 min-w-0">
|
||||
<p className="font-bold text-light-900 truncate">{podcastTitle}</p>
|
||||
<p className="text-lg font-bold text-white truncate mb-1">{podcastTitle}</p>
|
||||
{podcastAuthor && (
|
||||
<p className="text-sm text-light-500 truncate">{podcastAuthor}</p>
|
||||
<p className="text-xs font-bold text-accent-indigo truncate uppercase tracking-widest">{podcastAuthor}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full gap-3">
|
||||
<div className="flex flex-col w-full gap-4">
|
||||
<Link
|
||||
to="/library"
|
||||
className="flex items-center justify-center gap-2 w-full py-3.5 rounded-xl bg-blue-500 text-white font-bold hover:bg-blue-600 transition-all shadow-soft active:scale-[0.98]"
|
||||
className="neon-button !rounded-2xl !py-4 flex items-center justify-center gap-3"
|
||||
onClick={onClose}
|
||||
>
|
||||
<BookOpen className="w-5 h-5" />
|
||||
Voir dans ma Bibliothèque
|
||||
<span className="text-xs uppercase tracking-[0.2em]">Ma Bibliothèque</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-3.5 rounded-xl bg-light-100 text-light-700 font-bold hover:bg-light-200 transition-all"
|
||||
className="w-full py-4 rounded-2xl bg-white/5 text-slate-400 text-xs font-black uppercase tracking-widest hover:bg-white/10 hover:text-white transition-all"
|
||||
>
|
||||
Continuer la recherche
|
||||
Continuer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user