feat: implement podcast discovery and management controllers and services

This commit is contained in:
Michael committed 2026-04-16 14:57:49 +02:00
1 parent 7923f4cf09
commit a993e2d658
11 files changed
+318 -183

No files matched your search

+10 -10
View File
@@ -69,16 +69,16 @@ export default function Dashboard() {
<main className="">
{/* Welcome Section / Hero */}
<div className="premium-glass rounded-[3rem] p-8 lg:p-12 mb-12 relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-64 bg-accent-indigo/10 blur-[80px] -mr-32 -mt-32 group-hover:bg-accent-indigo/20 transition-all duration-700" />
<div className="absolute top-0 right-0 w-64 h-64 bg-[var(--accent-glow)] blur-[80px] -mr-32 -mt-32 group-hover:bg-[var(--accent-primary)]/20 transition-all duration-700" />
<div className="relative z-10">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-accent-indigo/10 border border-accent-indigo/20 text-[10px] font-black text-accent-indigo uppercase tracking-widest mb-4">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--accent-glow)] border border-[var(--border-color)] text-[10px] font-black text-[var(--accent-primary)] uppercase tracking-widest mb-4">
<Sparkles className="w-3 h-3" />
Podcastic Premium
</div>
<h2 className="text-4xl lg:text-5xl font-display font-black text-white mb-4 tracking-tight leading-tight max-w-2xl">
<h2 className="text-4xl lg:text-5xl font-display font-black mb-4 tracking-tight leading-tight max-w-2xl">
Découvrez les nouveautés de votre univers audio.
</h2>
<p className="text-slate-400 text-lg mb-8 max-w-xl leading-relaxed">
<p className="text-[var(--text-secondary)] text-lg mb-8 max-w-xl leading-relaxed">
Vous avez {episodesData?.count || 0} nouveaux épisodes qui n'attendent que vous. Prêt pour une immersion ?
</p>
<div className="flex flex-wrap gap-4">
@@ -96,7 +96,7 @@ export default function Dashboard() {
Découvrir
</button>
)}
<button className="px-6 py-3 rounded-2xl bg-white/5 text-slate-300 font-bold text-sm hover:bg-white/10 transition-all border border-white/5">
<button className="px-6 py-3 rounded-2xl bg-[var(--bg-secondary)] text-[var(--text-secondary)] font-bold text-sm hover:bg-[var(--accent-primary)]/10 transition-all border border-[var(--border-color)]">
Parcourir les tendances
</button>
</div>
@@ -107,10 +107,10 @@ export default function Dashboard() {
{episodesInProgress.length > 0 && (
<div className="mb-12 animate-fade-in">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-accent-indigo/10 flex items-center justify-center text-accent-indigo shadow-glow-indigo">
<div className="w-10 h-10 rounded-xl bg-[var(--accent-glow)] flex items-center justify-center text-[var(--accent-primary)] shadow-glow-indigo">
<Play className="w-5 h-5 fill-current" />
</div>
<h3 className="text-2xl font-display font-black text-white">Continuer l'écoute</h3>
<h3 className="text-2xl font-display font-black">Continuer l'écoute</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{episodesInProgress.map((episode) => (
@@ -127,12 +127,12 @@ export default function Dashboard() {
{/* Latest Episodes Section */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-accent-cyan/10 flex items-center justify-center text-accent-cyan">
<div className="w-10 h-10 rounded-xl bg-[var(--accent-glow)] flex items-center justify-center text-[var(--accent-primary)]">
<Clock className="w-5 h-5" />
</div>
<h3 className="text-2xl font-display font-black text-white">Récemment Publiés</h3>
<h3 className="text-2xl font-display font-black">Récemment Publiés</h3>
</div>
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest px-3 py-1 rounded-lg border border-white/5">{episodesData?.episodes?.length || 0} EPISODES</span>
<span className="text-[10px] font-bold text-[var(--text-secondary)] uppercase tracking-widest px-3 py-1 rounded-lg border border-[var(--border-color)]">{episodesData?.episodes?.length || 0} EPISODES</span>
</div>
{episodesLoading ? (
+27 -27
View File
@@ -93,51 +93,51 @@ export default function Library() {
{/* Stats */}
<div className="grid grid-cols-2 gap-4 mb-10">
<div className="premium-glass p-5 rounded-[1.5rem] flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-accent-indigo/10 flex items-center justify-center text-accent-indigo shrink-0">
<div className="w-10 h-10 rounded-xl bg-[var(--accent-glow)] flex items-center justify-center text-[var(--accent-primary)] shrink-0">
<BookOpen className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Abonnements</p>
<h3 className="text-2xl font-display font-black text-white">{podcasts.length}</h3>
<p className="text-[10px] font-bold text-[var(--text-secondary)] uppercase tracking-widest">Abonnements</p>
<h3 className="text-2xl font-display font-black">{podcasts.length}</h3>
</div>
</div>
<Link to="/add" className="premium-glass p-5 rounded-[1.5rem] flex items-center gap-4 group hover:bg-accent-indigo/5 transition-all">
<div className="w-10 h-10 rounded-xl bg-accent-indigo flex items-center justify-center text-white shadow-glow-indigo group-hover:scale-110 transition-transform shrink-0">
<Link to="/add" className="premium-glass p-5 rounded-[1.5rem] flex items-center gap-4 group hover:bg-[var(--accent-primary)]/5 transition-all">
<div className="w-10 h-10 rounded-xl bg-[var(--accent-primary)] flex items-center justify-center text-white shadow-glow-indigo group-hover:scale-110 transition-transform shrink-0">
<Rss className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Action</p>
<h3 className="text-base font-bold text-white">Ajouter un podcast</h3>
<p className="text-[10px] font-bold text-[var(--text-secondary)] uppercase tracking-widest">Action</p>
<h3 className="text-base font-bold">Ajouter un podcast</h3>
</div>
</Link>
</div>
{isLoading ? (
<div className="flex flex-col items-center justify-center py-32">
<div className="w-16 h-16 rounded-full border-4 border-accent-indigo/20 border-t-accent-indigo animate-spin mb-6" />
<p className="text-slate-400 font-medium animate-pulse">Chargement de votre univers...</p>
<div className="w-16 h-16 rounded-full border-4 border-[var(--border-color)] border-t-[var(--accent-primary)] animate-spin mb-6" />
<p className="text-[var(--text-secondary)] font-medium animate-pulse text-[10px] uppercase tracking-widest">Chargement de votre univers...</p>
</div>
) : podcasts.length === 0 ? (
<div className="premium-glass py-24 px-12 rounded-[3.5rem] text-center max-w-2xl mx-auto">
<div className="w-24 h-24 rounded-[2rem] bg-white/[0.03] flex items-center justify-center text-4xl mx-auto mb-8 border border-white/5">🕳️</div>
<h2 className="text-3xl font-display font-black text-white mb-4">Bibliothèque vide</h2>
<p className="text-slate-400 mb-10 leading-relaxed">Commencez votre voyage audio en ajoutant vos podcasts préférés dès maintenant.</p>
<div className="w-24 h-24 rounded-[2rem] bg-[var(--bg-secondary)] flex items-center justify-center text-4xl mx-auto mb-8 border border-[var(--border-color)]">🕳️</div>
<h2 className="text-3xl font-display font-black mb-4">Bibliothèque vide</h2>
<p className="text-[var(--text-secondary)] mb-10 leading-relaxed">Commencez votre voyage audio en ajoutant vos podcasts préférés dès maintenant.</p>
<Link to="/add" className="neon-button inline-flex items-center gap-3">
<Rss className="w-5 h-5" />
Découvrir des podcasts
</Link>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8 pb-10">
{podcasts.map((podcast) => (
<div key={podcast._id} className="group premium-glass rounded-[2.5rem] p-6 hover:bg-white/[0.05] transition-all duration-500 border-white/5 hover:border-white/10 flex flex-col h-full">
<div key={podcast._id} className="group premium-glass rounded-[2.5rem] p-6 hover:bg-[var(--bg-secondary)] transition-all duration-500 flex flex-col h-full border-[var(--border-color)]">
<div className="flex gap-6 mb-6">
<div className="relative shrink-0">
<div className="w-24 h-24 rounded-3xl overflow-hidden shadow-2xl border border-white/10 group-hover:scale-105 transition-transform duration-500">
<div className="w-24 h-24 rounded-3xl overflow-hidden shadow-2xl border border-[var(--border-color)] group-hover:scale-105 transition-transform duration-500">
{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-3xl">🎙️</div>
<div className="w-full h-full bg-gradient-to-br from-[var(--accent-primary)] to-[var(--accent-secondary)] flex items-center justify-center text-3xl">🎙️</div>
)}
</div>
<div className="absolute -bottom-2 -right-2 w-8 h-8 rounded-xl bg-white text-obsidian flex items-center justify-center shadow-lg opacity-0 group-hover:opacity-100 transform translate-y-2 group-hover:translate-y-0 transition-all">
@@ -147,18 +147,18 @@ export default function Library() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-bold text-white truncate leading-tight group-hover:text-accent-indigo transition-colors">
<h3 className="text-lg font-bold truncate leading-tight group-hover:text-[var(--accent-primary)] transition-colors">
{podcast.title}
</h3>
</div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-3">{podcast.author || 'Artiste Inconnu'}</p>
<p className="text-[10px] font-bold text-[var(--text-secondary)] uppercase tracking-widest mb-3">{podcast.author || 'Artiste Inconnu'}</p>
<div className="flex flex-wrap gap-2">
<span className="px-2.5 py-1 rounded-lg bg-white/5 border border-white/5 text-[9px] font-black text-slate-400 uppercase tracking-tighter">
<span className="px-2.5 py-1 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[9px] font-black text-[var(--text-secondary)] uppercase tracking-tighter">
{podcast.episodeCount} EPISODES
</span>
{podcast.language && (
<span className="px-2.5 py-1 rounded-lg bg-white/5 border border-white/5 text-[9px] font-black text-slate-400 uppercase">
<span className="px-2.5 py-1 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[9px] font-black text-[var(--text-secondary)] uppercase">
{podcast.language}
</span>
)}
@@ -188,10 +188,10 @@ export default function Library() {
</button>
</div>
) : podcast.lastEpisodeTitle && (
<div className="bg-white/[0.02] border border-white/5 rounded-2xl p-4 mb-6 group-hover:bg-white/[0.04] transition-colors">
<p className="text-[10px] font-bold text-accent-indigo uppercase tracking-[0.2em] mb-2">Dernier épisode</p>
<p className="text-sm font-bold text-slate-200 line-clamp-1 mb-1">{podcast.lastEpisodeTitle}</p>
<p className="text-[10px] font-medium text-slate-500 italic">
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-2xl p-4 mb-6 group-hover:bg-[var(--accent-primary)]/5 transition-colors">
<p className="text-[10px] font-bold text-[var(--accent-primary)] uppercase tracking-[0.2em] mb-2">Dernier épisode</p>
<p className="text-sm font-bold line-clamp-1 mb-1">{podcast.lastEpisodeTitle}</p>
<p className="text-[10px] font-medium text-[var(--text-secondary)] italic">
{new Date(podcast.lastEpisodeDate!).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
</p>
</div>
@@ -199,16 +199,16 @@ export default function Library() {
</div>
{/* Actions Footer */}
<div className="flex items-center justify-between pt-6 border-t border-white/5 mt-auto">
<div className="flex items-center justify-between pt-6 border-t border-[var(--border-color)] mt-auto">
{confirmDeleteId === (podcast.subscriptionId || podcast._id) ? (
<div className="flex items-center gap-3 w-full animate-fade-in">
<button onClick={handleConfirmUnsubscribe} className="flex-1 py-2 bg-accent-rose text-white rounded-xl text-[11px] font-black uppercase hover:bg-accent-rose/80">Confirmer</button>
<button onClick={() => setConfirmDeleteId(null)} className="flex-1 py-2 bg-white/5 text-slate-400 rounded-xl text-[11px] font-black uppercase hover:bg-white/10">Annuler</button>
<button onClick={() => setConfirmDeleteId(null)} className="flex-1 py-2 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-xl text-[11px] font-black uppercase hover:bg-[var(--bg-primary)]">Annuler</button>
</div>
) : (
<button
onClick={() => setConfirmDeleteId(podcast.subscriptionId || podcast._id)}
className="ml-auto flex items-center gap-2 text-[10px] font-bold text-slate-600 hover:text-accent-rose transition-all uppercase tracking-widest"
className="ml-auto flex items-center gap-2 text-[10px] font-bold text-[var(--text-secondary)] hover:text-accent-rose transition-all uppercase tracking-widest"
>
<Trash2 className="w-3 h-3" />
Désabonner
+51 -18
View File
@@ -11,11 +11,25 @@ export default function Trending() {
const queryClient = useQueryClient();
const [user, setUser] = useState<any>(null);
const [subscribingId, setSubscribingId] = useState<string | null>(null);
const [selectedGenre, setSelectedGenre] = useState<string>('');
// Modal state
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [successPodcast, setSuccessPodcast] = useState<any>(null);
const GENRES = [
{ id: '', label: 'Tout' },
{ id: '1318', label: 'Technologie' },
{ id: '1303', label: 'Humour' },
{ id: '1321', label: 'Business' },
{ id: '1311', label: 'News' },
{ id: '1324', label: 'Société' },
{ id: '1316', label: 'Sports' },
{ id: '1310', label: 'Musique' },
{ id: '1301', label: 'Arts' },
{ id: '1315', label: 'Science' },
];
useEffect(() => {
const loadUser = async () => {
try {
@@ -29,8 +43,8 @@ export default function Trending() {
}, []);
const { data: trendingData, isLoading } = useQuery({
queryKey: ['discover', 'trending'],
queryFn: () => discoveryService.getTrendingPodcasts(30),
queryKey: ['discover', 'trending', selectedGenre],
queryFn: () => discoveryService.getTrendingPodcasts(30, selectedGenre),
staleTime: 30 * 60 * 1000,
});
@@ -89,25 +103,44 @@ export default function Trending() {
/>
<main className="">
<div className="flex items-center gap-3 mb-10">
<div className="w-10 h-10 rounded-xl bg-accent-rose/10 flex items-center justify-center text-accent-rose">
<Flame className="w-6 h-6 animate-pulse" />
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-10">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-accent-rose/10 flex items-center justify-center text-accent-rose">
<Flame className="w-6 h-6 animate-pulse" />
</div>
<div>
<h2 className="text-2xl font-display font-black">Top Podcasts</h2>
<p className="text-[10px] font-bold text-[var(--text-secondary)] uppercase tracking-widest">Mis à jour en temps réel</p>
</div>
</div>
<div>
<h2 className="text-2xl font-display font-black text-white">Top Podcasts</h2>
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Mis à jour en temps réel</p>
{/* Genre Pills */}
<div className="flex items-center gap-2 overflow-x-auto pb-2 -mx-4 px-4 sm:mx-0 sm:px-0 no-scrollbar">
{GENRES.map(genre => (
<button
key={genre.id}
onClick={() => setSelectedGenre(genre.id)}
className={`px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest whitespace-nowrap transition-all border ${
selectedGenre === genre.id
? 'bg-[var(--accent-primary)] text-white border-[var(--accent-primary)] shadow-glow-indigo'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border-[var(--border-color)] hover:border-[var(--accent-primary)]'
}`}
>
{genre.label}
</button>
))}
</div>
</div>
{isLoading ? (
<div className="flex flex-col items-center justify-center py-32">
<div className="w-12 h-12 rounded-full border-4 border-white/5 border-t-accent-rose animate-spin mb-4" />
<p className="text-slate-500 font-bold uppercase tracking-widest text-[10px]">Analyse des tendances...</p>
<div className="w-12 h-12 rounded-full border-4 border-[var(--border-color)] border-t-[var(--accent-primary)] animate-spin mb-4" />
<p className="text-[var(--text-secondary)] font-bold uppercase tracking-widest text-[10px]">Analyse des tendances...</p>
</div>
) : trendingData?.podcasts && trendingData.podcasts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{trendingData.podcasts.map((podcast, index) => (
<div key={podcast.id} className="group premium-glass rounded-[2.5rem] overflow-hidden flex flex-col hover:bg-white/[0.05] transition-all duration-500">
<div key={podcast.id} className="group premium-glass rounded-[2.5rem] overflow-hidden flex flex-col hover:bg-[var(--bg-secondary)] transition-all duration-500">
<div className="relative aspect-[16/10] overflow-hidden">
{podcast.imageUrl ? (
<img
@@ -116,22 +149,22 @@ export default function Trending() {
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-slate-800 to-obsidian flex items-center justify-center text-4xl">🎙️</div>
<div className="w-full h-full bg-gradient-to-br from-slate-200 to-slate-400 dark:from-slate-800 dark:to-obsididan flex items-center justify-center text-4xl">🎙️</div>
)}
<div className="absolute top-4 left-4 px-3 py-1 rounded-full bg-black/40 backdrop-blur-md border border-white/10 text-[10px] font-black text-white uppercase tracking-widest">
Rank #{index + 1}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-obsidian via-transparent to-transparent opacity-60" />
<div className="absolute inset-0 bg-gradient-to-t from-[var(--bg-primary)] via-transparent to-transparent opacity-60" />
</div>
<div className="p-6 flex-1 flex flex-col">
<h3 className="text-lg font-bold text-white mb-2 line-clamp-2 leading-tight group-hover:text-accent-rose transition-colors">
<h3 className="text-lg font-bold mb-2 line-clamp-2 leading-tight group-hover:text-[var(--accent-primary)] transition-colors">
{podcast.title}
</h3>
{podcast.author && (
<p className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4 truncate">{podcast.author}</p>
<p className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-widest mb-4 truncate">{podcast.author}</p>
)}
<p className="text-xs text-slate-400 line-clamp-3 mb-6 leading-relaxed flex-1">
<p className="text-xs text-[var(--text-secondary)] line-clamp-3 mb-6 leading-relaxed flex-1 opacity-80">
{podcast.description}
</p>
@@ -140,8 +173,8 @@ export default function Trending() {
disabled={subscribingId === podcast.id || subscribeMutation.isPending || isSubscribed(podcast.rssUrl)}
className={`w-full py-3.5 rounded-2xl font-bold uppercase tracking-widest text-[10px] transition-all duration-300 flex items-center justify-center gap-2 ${
isSubscribed(podcast.rssUrl)
? 'bg-accent-cyan/10 text-accent-cyan border border-accent-cyan/20 cursor-default'
: 'bg-white text-obsidian hover:bg-accent-rose hover:text-white shadow-lg active:scale-95'
? 'bg-[var(--accent-secondary)]/10 text-[var(--accent-secondary)] border border-[var(--accent-secondary)]/20 cursor-default'
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:bg-[var(--accent-primary)] hover:text-white shadow-lg active:scale-95'
}`}
>
{subscribingId === podcast.id ? (