From c9127c70c1d0297858d4df72fba2db012a1cf1e9 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 16 Apr 2026 15:27:00 +0200 Subject: [PATCH] feat: implement trending podcasts discovery page with genre filtering and subscription capabilities --- frontend/src/components/AlertModal.tsx | 96 ++++++++++++++++++++++++++ frontend/src/components/Navigation.tsx | 8 +-- frontend/src/pages/AddPodcast.tsx | 37 ++++++++-- frontend/src/pages/Library.tsx | 39 +++++++++-- frontend/src/pages/Trending.tsx | 38 +++++++++- frontend/src/styles/globals.css | 4 +- 6 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/AlertModal.tsx diff --git a/frontend/src/components/AlertModal.tsx b/frontend/src/components/AlertModal.tsx new file mode 100644 index 0000000..8184f15 --- /dev/null +++ b/frontend/src/components/AlertModal.tsx @@ -0,0 +1,96 @@ +import { X, AlertCircle, Info, CheckCircle2, AlertTriangle } from 'lucide-react'; + +export type AlertType = 'error' | 'info' | 'success' | 'warning'; + +interface AlertModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + message: string; + type?: AlertType; +} + +export default function AlertModal({ + isOpen, + onClose, + title, + message, + type = 'error' +}: AlertModalProps) { + if (!isOpen) return null; + + const config = { + error: { + icon: AlertCircle, + color: 'text-accent-rose', + bg: 'bg-accent-rose/10', + border: 'border-accent-rose/20', + glow: 'shadow-[0_0_30px_rgba(244,63,94,0.3)]' + }, + warning: { + icon: AlertTriangle, + color: 'text-amber-500', + bg: 'bg-amber-500/10', + border: 'border-amber-500/20', + glow: 'shadow-[0_0_30px_rgba(245,158,11,0.3)]' + }, + success: { + icon: CheckCircle2, + color: 'text-accent-cyan', + bg: 'bg-accent-cyan/10', + border: 'border-accent-cyan/20', + glow: 'shadow-[0_0_30_rgba(6,182,212,0.3)]' + }, + info: { + icon: Info, + color: 'text-accent-indigo', + bg: 'bg-accent-indigo/10', + border: 'border-accent-indigo/20', + glow: 'shadow-[0_0_30_rgba(99,102,241,0.3)]' + } + }; + + const { icon: StatusIcon, color, bg, border, glow } = config[type]; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal Content */} +
+
+ {/* Header Actions */} + + + {/* Icon */} +
+ +
+ +

+ {title} +

+

+ {message} +

+ + +
+
+
+ ); +} diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx index 69e4c2b..dd125a7 100644 --- a/frontend/src/components/Navigation.tsx +++ b/frontend/src/components/Navigation.tsx @@ -7,10 +7,10 @@ export default function Navigation() { const isActive = (path: string) => location.pathname === path; const navItems = [ - { path: '/dashboard', label: 'Dashboard', icon: Home }, - { path: '/trending', label: 'Trending', icon: Flame }, - { path: '/library', label: 'Library', icon: Library }, - { path: '/add', label: 'Add Podcast', icon: PlusCircle }, + { path: '/dashboard', label: 'Tableau de bord', icon: Home }, + { path: '/trending', label: 'Tendances', icon: Flame }, + { path: '/library', label: 'Bibliothèque', icon: Library }, + { path: '/add', label: 'Ajouter', icon: PlusCircle }, ]; return ( diff --git a/frontend/src/pages/AddPodcast.tsx b/frontend/src/pages/AddPodcast.tsx index 1a6b845..880365a 100644 --- a/frontend/src/pages/AddPodcast.tsx +++ b/frontend/src/pages/AddPodcast.tsx @@ -7,6 +7,7 @@ import Header from '../components/Header'; import EpisodeCard from '../components/EpisodeCard'; import EpisodeDetails from '../components/EpisodeDetails'; import AudioPlayer from '../components/AudioPlayer'; +import AlertModal, { AlertType } from '../components/AlertModal'; import { Search, Plus, Loader, Rss, Check, Globe, Sparkles, PlusCircle, LayoutGrid, ListMusic } from 'lucide-react'; import { authService } from '../services/authService'; import SuccessModal from '../components/SuccessModal'; @@ -19,7 +20,20 @@ export default function AddPodcast() { const [subscribingId, setSubscribingId] = useState(null); const [rssUrl, setRssUrl] = useState(''); const [rssLoading, setRssLoading] = useState(false); - const [rssError, setRssError] = useState(null); + + // Alert states + const [alertConfig, setAlertConfig] = useState<{ + isOpen: boolean; + title: string; + message: string; + type: AlertType; + }>({ + isOpen: false, + title: '', + message: '', + type: 'error' + }); + const [rssSuccess, setRssSuccess] = useState(null); const [selectedEpisode, setSelectedEpisode] = useState(null); const [detailsEpisode, setDetailsEpisode] = useState(null); @@ -101,7 +115,6 @@ export default function AddPodcast() { e.preventDefault(); if (!rssUrl.trim()) return; setRssLoading(true); - setRssError(null); setRssSuccess(null); try { const result = await podcastService.subscribe(rssUrl.trim()); @@ -109,7 +122,12 @@ export default function AddPodcast() { setRssUrl(''); queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); } catch (error: any) { - setRssError(error.response?.data?.message || 'Impossible d\'ajouter ce podcast'); + setAlertConfig({ + isOpen: true, + title: "Échec de l'ajout", + message: error.response?.data?.message || "Impossible d'ajouter ce podcast. Vérifiez que l'URL du flux RSS est correcte et accessible.", + type: 'error' + }); } finally { setRssLoading(false); } @@ -141,7 +159,7 @@ export default function AddPodcast() { type="url" placeholder="URL du flux RSS (ex: https://feed.podbean.com/...)" value={rssUrl} - onChange={(e) => { setRssUrl(e.target.value); setRssError(null); setRssSuccess(null); }} + onChange={(e) => { setRssUrl(e.target.value); setRssSuccess(null); }} className="input-premium flex-1" />
@@ -270,6 +287,14 @@ export default function AddPodcast() { onPlay={setSelectedEpisode} /> + setAlertConfig(prev => ({ ...prev, isOpen: false }))} + /> + {selectedEpisode && ( (null); const [confirmDeleteId, setConfirmDeleteId] = useState(null); + + // Alert states + const [alertConfig, setAlertConfig] = useState<{ + isOpen: boolean; + title: string; + message: string; + type: AlertType; + }>({ + isOpen: false, + title: '', + message: '', + type: 'error' + }); useEffect(() => { const loadUser = async () => { @@ -51,7 +65,12 @@ export default function Library() { }, onError: (error: any) => { queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); - alert('Erreur lors du désabonnement'); + setAlertConfig({ + isOpen: true, + title: "Erreur de désabonnement", + message: "Une erreur est survenue lors de la tentative de désabonnement. Veuillez réessayer plus tard.", + type: 'error' + }); }, }); @@ -92,7 +111,7 @@ export default function Library() { {/* Stats */}
-
+
@@ -101,7 +120,7 @@ export default function Library() {

{podcasts.length}

- +
@@ -118,8 +137,8 @@ export default function Library() {

Chargement de votre univers...

) : podcasts.length === 0 ? ( -
-
🕳️
+
+
🕳️

Bibliothèque vide

Commencez votre voyage audio en ajoutant vos podcasts préférés dès maintenant.

@@ -130,7 +149,7 @@ export default function Library() { ) : (
{podcasts.map((podcast) => ( -
+
@@ -219,6 +238,14 @@ export default function Library() { ))}
)} + + setAlertConfig(prev => ({ ...prev, isOpen: false }))} + />
); } diff --git a/frontend/src/pages/Trending.tsx b/frontend/src/pages/Trending.tsx index df79dfb..f5dc5d6 100644 --- a/frontend/src/pages/Trending.tsx +++ b/frontend/src/pages/Trending.tsx @@ -6,6 +6,7 @@ import Header from '../components/Header'; import { Loader, Plus, Flame, Check, Sparkles, TrendingUp } from 'lucide-react'; import { authService } from '../services/authService'; import SuccessModal from '../components/SuccessModal'; +import AlertModal, { AlertType } from '../components/AlertModal'; export default function Trending() { const queryClient = useQueryClient(); @@ -17,6 +18,19 @@ export default function Trending() { const [showSuccessModal, setShowSuccessModal] = useState(false); const [successPodcast, setSuccessPodcast] = useState(null); + // Alert state + const [alertConfig, setAlertConfig] = useState<{ + isOpen: boolean; + title: string; + message: string; + type: AlertType; + }>({ + isOpen: false, + title: '', + message: '', + type: 'error' + }); + const GENRES = [ { id: '', label: 'Tout' }, { id: '1318', label: 'Technologie' }, @@ -98,7 +112,16 @@ export default function Trending() { }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); + queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] }); }, + onError: (error: any) => { + setAlertConfig({ + isOpen: true, + title: "Échec de l'abonnement", + message: error.response?.data?.message || "Une erreur est survenue. Le flux de ce podcast est peut-être temporairement inaccessible.", + type: 'error' + }); + } }); const handleSubscribe = async (podcast: DiscoveryPodcast) => { @@ -109,7 +132,12 @@ export default function Trending() { setShowSuccessModal(true); queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] }); } catch (error: any) { - console.error(error); + setAlertConfig({ + isOpen: true, + title: "Action impossible", + message: "Une erreur critique est survenue lors de la tentative d'abonnement.", + type: 'error' + }); } finally { setSubscribingId(null); } @@ -257,6 +285,14 @@ export default function Trending() { podcastAuthor={successPodcast?.author || ""} podcastImage={successPodcast?.imageUrl || ""} /> + + setAlertConfig(prev => ({ ...prev, isOpen: false }))} + />
); } diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 03f5f91..9451020 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -14,7 +14,7 @@ --accent-glow: rgba(99, 102, 241, 0.35); --border-color: rgba(255, 255, 255, 0.07); --glass-blur: blur(24px) saturate(180%); - --shadow-premium: 0 8px 40px rgba(0, 0, 0, 0.4); + --shadow-premium: 0 25px 60px rgba(0, 0, 0, 0.6), 0 0 1px rgba(255, 255, 255, 0.1); --input-bg: rgba(255, 255, 255, 0.03); --radius-card: 1.5rem; /* 24px */ --radius-panel: 2rem; /* 32px */ @@ -31,7 +31,7 @@ --accent-glow: rgba(197, 160, 40, 0.2); --border-color: rgba(197, 160, 40, 0.15); --glass-blur: blur(20px) saturate(120%); - --shadow-premium: 0 12px 48px rgba(0, 0, 0, 0.08); + --shadow-premium: 0 20px 50px rgba(197, 160, 40, 0.15), 0 10px 30px rgba(0, 0, 0, 0.06); --radius-card: 1.5rem; --radius-panel: 2rem; --input-bg: #ffffff;