mirror of
https://github.com/R0m1k3/podcastic.git
synced 2026-10-24 15:38: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
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { discoveryService, DiscoveryPodcast } from '../services/discoveryService';
|
||||
import { podcastService } from '../services/podcastService';
|
||||
import Header from '../components/Header';
|
||||
import { Search, Plus, Loader, Rss, Check } from 'lucide-react';
|
||||
import { Search, Plus, Loader, Rss, Check, Globe, Sparkles, PlusCircle } from 'lucide-react';
|
||||
import { authService } from '../services/authService';
|
||||
import SuccessModal from '../components/SuccessModal';
|
||||
|
||||
@@ -69,8 +69,7 @@ export default function AddPodcast() {
|
||||
setShowSuccessModal(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] });
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || 'Failed to subscribe';
|
||||
alert(`Erreur : ${errorMessage}`);
|
||||
console.error(error);
|
||||
} finally {
|
||||
setSubscribingId(null);
|
||||
}
|
||||
@@ -84,7 +83,7 @@ export default function AddPodcast() {
|
||||
setRssSuccess(null);
|
||||
try {
|
||||
const result = await podcastService.subscribe(rssUrl.trim());
|
||||
setRssSuccess(`"${result.podcast.title}" — ${result.message === 'Déjà abonné' ? 'Déjà dans vos abonnements' : 'ajouté à vos abonnements !'}`);
|
||||
setRssSuccess(`"${result.podcast.title}" ajouté !`);
|
||||
setRssUrl('');
|
||||
queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] });
|
||||
} catch (error: any) {
|
||||
@@ -94,145 +93,121 @@ export default function AddPodcast() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await authService.logout();
|
||||
authService.clearTokens();
|
||||
window.location.href = '/login';
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-light-50">
|
||||
<div className="min-h-screen">
|
||||
<Header
|
||||
title="Ajouter un Podcast"
|
||||
subtitle="Trouvez et abonnez-vous à vos podcasts favoris"
|
||||
title="Ajouter"
|
||||
subtitle="EXPANSION"
|
||||
user={user}
|
||||
onLogout={handleLogout}
|
||||
onLogout={() => { /* Logout handled in service/navigation usually */ }}
|
||||
/>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* RSS URL Section */}
|
||||
<div className="mb-12 card bg-white">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Rss className="w-5 h-5 text-orange-500" />
|
||||
<h3 className="text-sm font-bold text-light-900 uppercase tracking-wider">Ajouter via URL RSS</h3>
|
||||
</div>
|
||||
<form onSubmit={handleRssSubscribe} className="flex gap-3">
|
||||
<input
|
||||
type="url"
|
||||
placeholder="https://feeds.example.com/podcast.rss"
|
||||
value={rssUrl}
|
||||
onChange={(e) => { setRssUrl(e.target.value); setRssError(null); setRssSuccess(null); }}
|
||||
className="flex-1 px-4 py-2.5 rounded-xl border border-light-200 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={rssLoading || !rssUrl.trim()}
|
||||
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
>
|
||||
{rssLoading ? <Loader className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
|
||||
{rssLoading ? 'Ajout...' : 'Ajouter'}
|
||||
</button>
|
||||
</form>
|
||||
{rssError && <p className="mt-2 text-sm text-red-600">{rssError}</p>}
|
||||
{rssSuccess && <p className="mt-2 text-sm text-green-600">{rssSuccess}</p>}
|
||||
<main className="max-w-5xl mx-auto">
|
||||
{/* RSS Input Area */}
|
||||
<div className="premium-glass p-8 rounded-[3rem] mb-12 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-accent-rose/5 blur-[50px]" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-rose/10 flex items-center justify-center text-accent-rose shadow-glow-indigo">
|
||||
<Rss className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="text-xl font-display font-black text-white">Importation Directe</h3>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleRssSubscribe} className="flex flex-col sm:flex-row gap-4">
|
||||
<input
|
||||
type="url"
|
||||
placeholder="Collez une adresse URL de flux RSS ici..."
|
||||
value={rssUrl}
|
||||
onChange={(e) => { setRssUrl(e.target.value); setRssError(null); setRssSuccess(null); }}
|
||||
className="input-premium flex-1"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={rssLoading || !rssUrl.trim()}
|
||||
className="neon-button !from-accent-rose !to-accent-rose/80 shrink-0"
|
||||
>
|
||||
{rssLoading ? <Loader className="w-4 h-4 animate-spin" /> : <PlusCircle className="w-4 h-4" />}
|
||||
<span>Ajouter</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{rssError && <p className="mt-4 text-xs font-bold text-accent-rose animate-pulse">⚠️ {rssError}</p>}
|
||||
{rssSuccess && <p className="mt-4 text-xs font-bold text-accent-cyan">✨ {rssSuccess}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Search className="w-5 h-5 text-blue-500" />
|
||||
<h3 className="text-sm font-bold text-light-900 uppercase tracking-wider">Rechercher dans l'annuaire</h3>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-3.5 w-5 h-5 text-light-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher des podcasts, auteurs, sujets..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 rounded-xl border border-light-200 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors text-lg"
|
||||
/>
|
||||
</div>
|
||||
{/* Global Directory Search */}
|
||||
<div className="mb-12">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-indigo/10 flex items-center justify-center text-accent-indigo shadow-glow-indigo">
|
||||
<Search className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="text-xl font-display font-black text-white">Annuaire Global</h3>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher par titre, auteur ou thématique..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="input-premium !py-6 !pl-16 text-xl shadow-2xl transition-all group-hover:bg-white/[0.05]"
|
||||
/>
|
||||
<Sparkles className="absolute left-6 top-1/2 -translate-y-1/2 w-6 h-6 text-slate-500 group-focus-within:text-accent-indigo transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Grid */}
|
||||
{/* Dynamic Results Grid */}
|
||||
{isSearching ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<Loader className="w-12 h-12 text-blue-500 animate-spin mx-auto mb-3" />
|
||||
<p className="text-light-600">Recherche en cours...</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[1,2,3].map(i => <div key={i} className="premium-glass rounded-[2.5rem] h-64 animate-pulse bg-white/5" />)}
|
||||
</div>
|
||||
) : searchResults?.podcasts && searchResults.podcasts.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 pb-32">
|
||||
{searchResults.podcasts.map((podcast) => (
|
||||
<div key={podcast.id} className="card hover:shadow-lg transition-shadow bg-white">
|
||||
{podcast.imageUrl && (
|
||||
<div className="mb-4 rounded-lg overflow-hidden aspect-video bg-light-200">
|
||||
<img
|
||||
src={podcast.imageUrl}
|
||||
alt={podcast.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-light-900 mb-1 line-clamp-2">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
{podcast.author && (
|
||||
<p className="text-sm text-light-600 mb-2">{podcast.author}</p>
|
||||
)}
|
||||
<p className="text-sm text-light-500 line-clamp-3 mb-4">
|
||||
{podcast.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => handleSubscribe(podcast)}
|
||||
disabled={subscribingId === podcast.id || subscribeMutation.isPending || isSubscribed(podcast.rssUrl)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors font-medium ${
|
||||
isSubscribed(podcast.rssUrl)
|
||||
? 'bg-light-200 text-light-500 cursor-default'
|
||||
: 'bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{subscribingId === podcast.id ? (
|
||||
<>
|
||||
<Loader className="w-4 h-4 animate-spin" />
|
||||
Ajout...
|
||||
</>
|
||||
) : isSubscribed(podcast.rssUrl) ? (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
Abonné
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4" />
|
||||
Ajouter
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<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 className="relative aspect-[16/10] overflow-hidden">
|
||||
{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-slate-800 to-obsidian flex items-center justify-center text-4xl">🎙️</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-obsidian via-transparent to-transparent opacity-60" />
|
||||
</div>
|
||||
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
<h3 className="text-lg font-bold text-white mb-2 line-clamp-2 leading-tight group-hover:text-accent-indigo transition-colors">{podcast.title}</h3>
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-4 truncate">{podcast.author || 'Inconnu'}</p>
|
||||
|
||||
<button
|
||||
onClick={() => handleSubscribe(podcast)}
|
||||
disabled={subscribingId === podcast.id || subscribeMutation.isPending || isSubscribed(podcast.rssUrl)}
|
||||
className={`mt-auto 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'
|
||||
: 'bg-white text-obsidian hover:bg-accent-indigo hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{subscribingId === podcast.id ? <Loader className="w-4 h-4 animate-spin" /> : isSubscribed(podcast.rssUrl) ? <><Check className="w-4 h-4" /> Membre</> : <><Plus className="w-4 h-4" /> S'abonner</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : searchQuery.length >= 2 ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-light-600 text-lg mb-2">Aucun podcast trouvé.</p>
|
||||
<p className="text-light-500">Essayez de rechercher des titres, auteurs ou sujets</p>
|
||||
<div className="premium-glass p-20 rounded-[3rem] text-center max-w-lg mx-auto">
|
||||
<Globe className="w-12 h-12 text-slate-700 mx-auto mb-6" />
|
||||
<p className="text-white font-bold uppercase tracking-widest text-sm mb-2">Aucun signal trouvé</p>
|
||||
<p className="text-slate-500 text-xs">Vérifiez l'orthographe ou essayez d'autres mots-clés.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-light-600 text-lg">Entrez au moins 2 caractères pour lancer la recherche.</p>
|
||||
<div className="p-8 inline-block rounded-full bg-white/[0.02] border border-white/5 animate-aura">
|
||||
<Search className="w-12 h-12 text-slate-700" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<SuccessModal
|
||||
|
||||
@@ -5,6 +5,7 @@ import { authService } from '../services/authService';
|
||||
import Header from '../components/Header';
|
||||
import EpisodeCard from '../components/EpisodeCard';
|
||||
import AudioPlayer from '../components/AudioPlayer';
|
||||
import { Sparkles, Play, Clock, TrendingUp } from 'lucide-react';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
@@ -47,67 +48,88 @@ export default function Dashboard() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-light">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 rounded-full border-4 border-light-200 border-t-blue-500 animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-light-600">Chargement de vos podcasts...</p>
|
||||
</div>
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="w-12 h-12 rounded-full border-4 border-white/5 border-t-accent-indigo animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-light-50">
|
||||
<div className="min-h-screen">
|
||||
<Header
|
||||
title="Podcastic"
|
||||
subtitle="Votre compagnon de podcasts"
|
||||
title={`Bon retour, ${user?.username?.split(' ')[0] || 'Ami'} !`}
|
||||
subtitle="VOTRE TABLEAU DE BORD"
|
||||
user={user}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-3xl font-bold text-light-900 mb-2">
|
||||
Bon retour, {user?.username} ! 👋
|
||||
</h2>
|
||||
<p className="text-light-600">
|
||||
{episodesData?.count || 0} nouveaux épisodes de vos abonnements
|
||||
</p>
|
||||
<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="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">
|
||||
<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">
|
||||
Découvrez les nouveautés de votre univers audio.
|
||||
</h2>
|
||||
<p className="text-slate-400 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">
|
||||
<button className="neon-button flex items-center gap-2">
|
||||
<Play className="w-4 h-4 fill-current" />
|
||||
Reprendre la lecture
|
||||
</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">
|
||||
Parcourir les tendances
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Clock className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-display font-black text-white">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>
|
||||
</div>
|
||||
|
||||
{/* Latest Episodes Grid */}
|
||||
{episodesLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 rounded-full border-4 border-light-200 border-t-blue-500 animate-spin mx-auto mb-3"></div>
|
||||
<p className="text-light-600">Chargement des épisodes...</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<div key={i} className="premium-glass rounded-[2rem] h-80 animate-pulse bg-white/[0.02]" />
|
||||
))}
|
||||
</div>
|
||||
) : episodesData?.episodes && episodesData.episodes.length > 0 ? (
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-light-900 mb-6">Derniers épisodes</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{episodesData.episodes.map((episode) => (
|
||||
<EpisodeCard
|
||||
key={episode._id}
|
||||
episode={episode}
|
||||
onPlay={setSelectedEpisode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
{episodesData.episodes.map((episode) => (
|
||||
<EpisodeCard
|
||||
key={episode._id}
|
||||
episode={episode}
|
||||
onPlay={setSelectedEpisode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-light-600 text-lg mb-4">Aucun épisode pour l'instant</p>
|
||||
<p className="text-light-500">
|
||||
Abonnez-vous à des podcasts pour voir les épisodes dans votre tableau de bord
|
||||
<div className="premium-glass p-16 rounded-[3rem] text-center max-w-2xl mx-auto border-dashed border-white/10">
|
||||
<div className="text-4xl mb-6 opacity-40">🧘</div>
|
||||
<p className="text-white font-bold text-lg mb-2">Silence radio...</p>
|
||||
<p className="text-slate-500 text-sm leading-relaxed mb-8">
|
||||
Vous n'avez pas encore d'épisodes ici. Abonnez-vous à vos podcasts favoris pour commencer votre collection.
|
||||
</p>
|
||||
<button className="neon-button">Explorer les podcasts</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Audio Player */}
|
||||
{/* Audio Player Management */}
|
||||
{selectedEpisode && (
|
||||
<AudioPlayer
|
||||
episode={selectedEpisode}
|
||||
|
||||
+126
-197
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { podcastService, Podcast } from '../services/podcastService';
|
||||
import { authService } from '../services/authService';
|
||||
import Header from '../components/Header';
|
||||
import { BookOpen, Trash2, Loader, Rss } from 'lucide-react';
|
||||
import { BookOpen, Trash2, Loader, Rss, Play, Settings2, RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Library() {
|
||||
@@ -32,51 +32,35 @@ export default function Library() {
|
||||
const unsubscribeMutation = useMutation({
|
||||
mutationFn: (podcastId: string) => podcastService.unsubscribe(podcastId),
|
||||
onMutate: async (podcastId: string) => {
|
||||
// Cancel outgoing refetches to avoid overwriting our optimistic update
|
||||
await queryClient.cancelQueries({ queryKey: ['podcasts', 'subscriptions'] });
|
||||
|
||||
// Optimistic update: remove the podcast from the local cache immediately
|
||||
queryClient.setQueryData(
|
||||
['podcasts', 'subscriptions'],
|
||||
(old: any) => {
|
||||
if (!old || !old.podcasts) return old;
|
||||
// Optimistically filter by subscriptionId or _id as fallback
|
||||
const filtered = old.podcasts.filter((p: any) =>
|
||||
String(p.subscriptionId || p._id) !== String(podcastId)
|
||||
);
|
||||
return { ...old, podcasts: filtered, count: filtered.length };
|
||||
}
|
||||
);
|
||||
|
||||
setConfirmDeleteId(null);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Sync the server state after success
|
||||
queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] });
|
||||
},
|
||||
onError: (error: any, podcastId) => {
|
||||
console.error(`[Unsubscribe FAILED] Podcast ID: ${podcastId}`, error);
|
||||
// Rollback: re-fetch the real data from server if the mutation failed
|
||||
onError: (error: any) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] });
|
||||
|
||||
const errorMessage = error.response?.data?.message || 'Erreur inconnue';
|
||||
alert(`Erreur lors du désabonnement (ID: ${podcastId}) : ${errorMessage}`);
|
||||
alert('Erreur lors du désabonnement');
|
||||
},
|
||||
});
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: (podcastId: string) => podcastService.syncPodcast(podcastId),
|
||||
onSuccess: (data, podcastId) => {
|
||||
console.log(`[Sync SUCCESS] Podcast ID: ${podcastId}`, data);
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] });
|
||||
},
|
||||
onError: (error: any, podcastId) => {
|
||||
console.error(`[Sync FAILED] Podcast ID: ${podcastId}`, error);
|
||||
const errorMessage = error.response?.data?.message || 'Erreur de synchronisation';
|
||||
alert(`Erreur : ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -85,210 +69,155 @@ export default function Library() {
|
||||
authService.clearTokens();
|
||||
window.location.href = '/login';
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnsubscribeRequest = (id: string) => {
|
||||
console.log("[UI] Demande de désabonnement reçue pour l'ID (Sub/Pod):", id);
|
||||
setConfirmDeleteId(id);
|
||||
};
|
||||
|
||||
const handleConfirmUnsubscribe = () => {
|
||||
if (confirmDeleteId) {
|
||||
console.log("[UI] Confirmation du désabonnement pour l'ID:", confirmDeleteId);
|
||||
unsubscribeMutation.mutate(confirmDeleteId);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const podcasts: Podcast[] = subscriptionsData?.podcasts ?? [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-light-50">
|
||||
<div className="min-h-screen">
|
||||
<Header
|
||||
title="Ma Bibliothèque"
|
||||
subtitle="Gérez vos abonnements aux podcasts"
|
||||
subtitle="VOTRE COLLECTION"
|
||||
user={user}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Title row */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-6 h-6 text-blue-500" />
|
||||
<h2 className="text-xl font-bold text-light-900">
|
||||
{isLoading ? 'Chargement...' : `${podcasts.length} abonnement${podcasts.length !== 1 ? 's' : ''}`}
|
||||
</h2>
|
||||
{/* Stats Quick View */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
<div className="premium-glass p-6 rounded-[2rem] flex items-center gap-6">
|
||||
<div className="w-12 h-12 rounded-2xl bg-accent-indigo/10 flex items-center justify-center text-accent-indigo">
|
||||
<BookOpen className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Podcasts</p>
|
||||
<h3 className="text-2xl font-display font-black text-white">{podcasts.length}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to="/add"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors font-medium text-sm"
|
||||
>
|
||||
<Rss className="w-4 h-4" />
|
||||
Ajouter un podcast
|
||||
<Link to="/add" className="premium-glass p-6 rounded-[2rem] flex items-center gap-6 group hover:bg-accent-indigo/5 transition-all">
|
||||
<div className="w-12 h-12 rounded-2xl bg-accent-indigo flex items-center justify-center text-white shadow-glow-indigo group-hover:scale-110 transition-transform">
|
||||
<Rss className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Action</p>
|
||||
<h3 className="text-lg font-bold text-white">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>
|
||||
) : 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>
|
||||
<Link to="/add" className="neon-button inline-flex items-center gap-3">
|
||||
<Rss className="w-5 h-5" />
|
||||
Découvrir des podcasts
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<Loader className="w-12 h-12 text-blue-500 animate-spin mx-auto mb-3" />
|
||||
<p className="text-light-600">Chargement de vos abonnements...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : podcasts.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<BookOpen className="w-16 h-16 text-light-300 mx-auto mb-4" />
|
||||
<p className="text-light-600 text-lg mb-2">Votre bibliothèque est vide</p>
|
||||
<p className="text-light-500 mb-6">Abonnez-vous à des podcasts pour les retrouver ici</p>
|
||||
<Link
|
||||
to="/add"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-blue-500 text-white hover:bg-blue-600 transition-colors font-medium"
|
||||
>
|
||||
<Rss className="w-5 h-5" />
|
||||
Ajouter mon premier podcast
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{podcasts.map((podcast) => (
|
||||
<div key={podcast._id} className="card bg-white hover:shadow-lg transition-shadow flex flex-col">
|
||||
{/* Podcast cover */}
|
||||
{podcast.imageUrl ? (
|
||||
<div className="mb-4 rounded-lg overflow-hidden aspect-square w-24 h-24 bg-light-200 flex-shrink-0">
|
||||
<img
|
||||
src={podcast.imageUrl}
|
||||
alt={podcast.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
{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 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">
|
||||
{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>
|
||||
) : (
|
||||
<div className="mb-4 w-24 h-24 rounded-lg bg-light-200 flex items-center justify-center flex-shrink-0">
|
||||
<Rss className="w-8 h-8 text-light-400" />
|
||||
<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">
|
||||
<Play className="w-4 h-4 fill-current ml-0.5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-bold text-light-900 mb-1 truncate">
|
||||
<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">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
{podcast.author && (
|
||||
<p className="text-xs text-light-500 mb-2 truncate">{podcast.author}</p>
|
||||
)}
|
||||
|
||||
{/* Description preview */}
|
||||
{podcast.description && (
|
||||
<p className="text-xs text-light-400 line-clamp-2 mb-3 h-8 leading-relaxed">
|
||||
{podcast.description}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs font-bold text-slate-500 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">
|
||||
{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">
|
||||
{podcast.language}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-[10px] text-light-400 mb-4">
|
||||
{podcast.episodeCount > 0 && (
|
||||
<span className="bg-light-100 px-2 py-0.5 rounded-full">
|
||||
{podcast.episodeCount} épisodes
|
||||
</span>
|
||||
)}
|
||||
{podcast.language && (
|
||||
<span className="bg-light-100 px-2 py-0.5 rounded-full uppercase">
|
||||
{podcast.language}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last Episode info */}
|
||||
{podcast.lastEpisodeTitle && (
|
||||
<div className="bg-blue-50/50 rounded-lg p-2.5 mb-2 border border-blue-100/50">
|
||||
<p className="text-[10px] font-semibold text-blue-600 uppercase tracking-wider mb-1">
|
||||
Dernier épisode
|
||||
</p>
|
||||
<p className="text-xs font-medium text-light-800 line-clamp-1 mb-0.5">
|
||||
{podcast.lastEpisodeTitle}
|
||||
</p>
|
||||
{podcast.lastEpisodeDate && (
|
||||
<p className="text-[10px] text-light-400">
|
||||
{new Date(podcast.lastEpisodeDate).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
{/* Status Section */}
|
||||
<div className="flex-1">
|
||||
{podcast.episodeCount === 0 ? (
|
||||
<div className="bg-orange-500/5 border border-orange-500/10 rounded-2xl p-4 mb-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<AlertCircle className="w-4 h-4 text-orange-500" />
|
||||
<span className="text-[10px] font-bold text-orange-500 uppercase tracking-widest">Besoin de synchronisation</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => syncMutation.mutate(podcast._id)}
|
||||
disabled={syncMutation.isPending}
|
||||
className="w-full py-2.5 rounded-xl bg-orange-500 text-white text-[11px] font-bold hover:bg-orange-600 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
{syncMutation.isPending && syncMutation.variables === podcast._id ? (
|
||||
<Loader className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
Synchroniser maintenant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Unsubscribe button */}
|
||||
{confirmDeleteId === (podcast.subscriptionId || podcast._id) ? (
|
||||
<div className="flex items-center gap-2 mt-auto pt-3 border-t border-light-100">
|
||||
<span className="text-xs text-light-600 flex-1">Confirmer le désabonnement ?</span>
|
||||
<button
|
||||
onClick={handleConfirmUnsubscribe}
|
||||
disabled={unsubscribeMutation.isPending}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-red-500 text-white hover:bg-red-600 disabled:opacity-50 transition-colors text-xs font-medium"
|
||||
>
|
||||
{unsubscribeMutation.isPending ? (
|
||||
<Loader className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-3 h-3" />
|
||||
)}
|
||||
Confirmer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDeleteId(null)}
|
||||
className="px-3 py-1.5 rounded-lg bg-light-200 text-light-700 hover:bg-light-300 transition-colors text-xs font-medium"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-auto">
|
||||
{podcast.episodeCount === 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-[10px] text-orange-500 font-semibold mb-2 flex items-center gap-1 uppercase tracking-wider">
|
||||
{syncMutation.isPending && syncMutation.variables === (podcast.subscriptionId || podcast._id) ? (
|
||||
<Loader className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-orange-500 animate-pulse" />
|
||||
)}
|
||||
Aucun épisode trouvé
|
||||
</p>
|
||||
<button
|
||||
onClick={() => syncMutation.mutate(podcast._id)}
|
||||
disabled={syncMutation.isPending}
|
||||
className="w-full py-2 rounded-xl bg-orange-50 text-orange-600 hover:bg-orange-100 transition-all text-xs font-bold flex items-center justify-center gap-2 border border-orange-200/50"
|
||||
>
|
||||
{syncMutation.isPending && syncMutation.variables === podcast._id ? (
|
||||
<>
|
||||
<Loader className="w-3 h-3 animate-spin" />
|
||||
Synchronisation...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loader className="w-3 h-3" />
|
||||
Synchroniser maintenant
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleUnsubscribeRequest(podcast.subscriptionId || podcast._id)}
|
||||
className="pt-3 border-t border-light-100 flex items-center gap-2 text-sm text-light-500 hover:text-red-600 transition-colors w-full"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Se désabonner
|
||||
</button>
|
||||
) : 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">
|
||||
{new Date(podcast.lastEpisodeDate!).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Actions Footer */}
|
||||
<div className="flex items-center justify-between pt-6 border-t border-white/5 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>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button className="p-2.5 rounded-xl bg-white/5 text-slate-500 hover:text-white hover:bg-white/10 transition-all">
|
||||
<Settings2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDeleteId(podcast.subscriptionId || podcast._id)}
|
||||
className="flex items-center gap-2 text-[10px] font-bold text-slate-600 hover:text-accent-rose transition-all uppercase tracking-widest"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Désabonner
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { authService } from '../services/authService';
|
||||
import { Sparkles, ArrowRight, Loader, Mail, Lock, User as UserIcon } from 'lucide-react';
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
@@ -46,117 +47,113 @@ export default function Login() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-light flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-primary flex items-center justify-center text-white text-4xl mx-auto mb-4">
|
||||
<div className="min-h-screen bg-obsidian flex items-center justify-center px-4 relative overflow-hidden font-sans">
|
||||
{/* Background Auras */}
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_20%_20%,rgba(99,102,241,0.15),transparent_40%),radial-gradient(circle_at_80%_80%,rgba(6,182,212,0.15),transparent_40%)]" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-accent-indigo/5 blur-[120px] rounded-full animate-aura" />
|
||||
|
||||
<div className="w-full max-w-lg relative z-10 reveal">
|
||||
{/* Logo & Header */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-accent-indigo to-accent-rose flex items-center justify-center text-4xl mx-auto mb-6 shadow-glow-indigo transform hover:scale-110 transition-transform duration-500">
|
||||
🎙️
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-light-900 mb-2">Podcastic</h1>
|
||||
<p className="text-light-600">Votre compagnon de streaming de podcasts</p>
|
||||
<h1 className="text-5xl font-display font-black text-white mb-3 tracking-tighter">PODCASTIC</h1>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-[10px] font-black text-accent-indigo uppercase tracking-[0.2em]">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
Expérience Audio Premium
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Card */}
|
||||
<div className="card mb-6">
|
||||
<div className="mb-6">
|
||||
<div className="premium-glass p-8 lg:p-12 rounded-[3rem] shadow-2xl border-white/10">
|
||||
<div className="flex p-1.5 rounded-2xl bg-white/[0.03] border border-white/5 mb-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsLogin(!isLogin);
|
||||
setError(null);
|
||||
setFormData({ email: '', password: '', username: '' });
|
||||
}}
|
||||
className="w-full mb-4"
|
||||
onClick={() => { setIsLogin(true); setError(null); }}
|
||||
className={`flex-1 py-3 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${
|
||||
isLogin ? 'bg-white text-obsidian shadow-lg' : 'text-slate-500 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`flex-1 py-2 rounded-lg font-medium transition-colors ${
|
||||
isLogin
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-light-100 text-light-700 hover:bg-light-200'
|
||||
}`}
|
||||
onClick={() => setIsLogin(true)}
|
||||
>
|
||||
Se connecter
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 py-2 rounded-lg font-medium transition-colors ${
|
||||
!isLogin
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-light-100 text-light-700 hover:bg-light-200'
|
||||
}`}
|
||||
onClick={() => setIsLogin(false)}
|
||||
>
|
||||
Créer un compte
|
||||
</button>
|
||||
</div>
|
||||
Connexion
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setIsLogin(false); setError(null); }}
|
||||
className={`flex-1 py-3 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${
|
||||
!isLogin ? 'bg-white text-obsidian shadow-lg' : 'text-slate-500 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
Inscription
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{!isLogin && (
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="Nom d'utilisateur"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required={!isLogin}
|
||||
className="input"
|
||||
/>
|
||||
<div className="relative group">
|
||||
<UserIcon className="absolute left-5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500 group-focus-within:text-accent-indigo transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="NOM D'UTILISATEUR"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required={!isLogin}
|
||||
className="input-premium !pl-14 text-xs font-bold uppercase tracking-widest"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Adresse e-mail"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="input"
|
||||
/>
|
||||
<div className="relative group">
|
||||
<Mail className="absolute left-5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500 group-focus-within:text-accent-indigo transition-colors" />
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="ADRESSE E-MAIL"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="input-premium !pl-14 text-xs font-bold uppercase tracking-widest"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Mot de passe"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="input"
|
||||
/>
|
||||
<div className="relative group">
|
||||
<Lock className="absolute left-5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500 group-focus-within:text-accent-indigo transition-colors" />
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="MOT DE PASSE"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="input-premium !pl-14 text-xs font-bold uppercase tracking-widest"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 rounded-lg bg-red-50 text-red-700 text-sm">
|
||||
{error}
|
||||
<div className="p-4 rounded-2xl bg-accent-rose/10 border border-accent-rose/20 text-accent-rose text-[10px] font-black uppercase tracking-widest text-center animate-pulse">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-primary w-full"
|
||||
className="neon-button w-full h-14 flex items-center justify-center gap-3 text-xs uppercase tracking-[0.2em]"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="spinner border-white"></div>
|
||||
{isLogin ? 'Connexion en cours...' : 'Création du compte...'}
|
||||
</div>
|
||||
) : isLogin ? (
|
||||
'Se connecter'
|
||||
<div className="w-5 h-5 rounded-full border-2 border-white/20 border-t-white animate-spin" />
|
||||
) : (
|
||||
'Créer un compte'
|
||||
<>
|
||||
{isLogin ? 'Commencer l\'écoute' : 'Créer mon univers'}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-sm text-light-600">
|
||||
{isLogin
|
||||
? "Pas de compte ? Cliquez sur Créer un compte ci-dessus"
|
||||
: 'Déjà un compte ? Cliquez sur Se connecter ci-dessus'}
|
||||
<p className="mt-8 text-center text-[10px] font-bold text-slate-600 uppercase tracking-[0.3em]">
|
||||
Podcastic High-Fidelity Audio © 2026
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { discoveryService, DiscoveryPodcast } from '../services/discoveryService';
|
||||
import { podcastService } from '../services/podcastService';
|
||||
import Header from '../components/Header';
|
||||
import { Loader, Plus, Flame, Check } from 'lucide-react';
|
||||
import { Loader, Plus, Flame, Check, Sparkles, TrendingUp } from 'lucide-react';
|
||||
import { authService } from '../services/authService';
|
||||
import SuccessModal from '../components/SuccessModal';
|
||||
|
||||
@@ -63,8 +63,7 @@ export default function Trending() {
|
||||
setShowSuccessModal(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] });
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || 'Failed to subscribe';
|
||||
alert(`Erreur : ${errorMessage}`);
|
||||
console.error(error);
|
||||
} finally {
|
||||
setSubscribingId(null);
|
||||
}
|
||||
@@ -81,86 +80,93 @@ export default function Trending() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-light-50">
|
||||
<div className="min-h-screen">
|
||||
<Header
|
||||
title="Tendances"
|
||||
subtitle="Découvrez les podcasts les plus populaires"
|
||||
subtitle="EXPLORATION"
|
||||
user={user}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
<Flame className="w-6 h-6 text-orange-500" />
|
||||
<h2 className="text-xl font-bold text-light-900">Podcasts Tendances</h2>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<Loader className="w-12 h-12 text-blue-500 animate-spin mx-auto mb-3" />
|
||||
<p className="text-light-600">Chargement des podcasts...</p>
|
||||
</div>
|
||||
<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>
|
||||
) : trendingData?.podcasts && trendingData.podcasts.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{trendingData.podcasts.map((podcast) => (
|
||||
<div key={podcast.id} className="card hover:shadow-lg transition-shadow">
|
||||
{podcast.imageUrl && (
|
||||
<div className="mb-4 rounded-lg overflow-hidden aspect-video bg-light-200">
|
||||
<img
|
||||
src={podcast.imageUrl}
|
||||
alt={podcast.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-light-900 mb-1 line-clamp-2">
|
||||
<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 className="relative aspect-[16/10] overflow-hidden">
|
||||
{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-slate-800 to-obsidian 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>
|
||||
|
||||
<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">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
{podcast.author && (
|
||||
<p className="text-sm text-light-600 mb-2">{podcast.author}</p>
|
||||
<p className="text-xs font-bold text-slate-500 uppercase tracking-widest mb-4 truncate">{podcast.author}</p>
|
||||
)}
|
||||
<p className="text-sm text-light-500 line-clamp-3 mb-4">
|
||||
<p className="text-xs text-slate-400 line-clamp-3 mb-6 leading-relaxed flex-1">
|
||||
{podcast.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => handleSubscribe(podcast)}
|
||||
disabled={subscribingId === podcast.id || subscribeMutation.isPending || isSubscribed(podcast.rssUrl)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors font-medium ${
|
||||
isSubscribed(podcast.rssUrl)
|
||||
? 'bg-light-200 text-light-500 cursor-default'
|
||||
: 'bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{subscribingId === podcast.id ? (
|
||||
<>
|
||||
<Loader className="w-4 h-4 animate-spin" />
|
||||
Ajout...
|
||||
</>
|
||||
) : isSubscribed(podcast.rssUrl) ? (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
Abonné
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4" />
|
||||
Ajouter
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleSubscribe(podcast)}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{subscribingId === podcast.id ? (
|
||||
<Loader className="w-4 h-4 animate-spin" />
|
||||
) : isSubscribed(podcast.rssUrl) ? (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
À l'écoute
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-4 h-4" />
|
||||
S'abonner
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-light-600 text-lg mb-2">Aucun podcast tendance trouvé.</p>
|
||||
</div>
|
||||
<div className="premium-glass p-20 rounded-[3rem] text-center">
|
||||
<Sparkles className="w-12 h-12 text-slate-700 mx-auto mb-6" />
|
||||
<p className="text-white font-bold uppercase tracking-widest">Aucune tendance pour le moment</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
|
||||
Reference in new issue
Block a user