diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2f04d15..1786c6a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,8 @@ import './styles/globals.css'; import { authService } from './services/authService'; import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; -import Discover from './pages/Discover'; +import Trending from './pages/Trending'; +import AddPodcast from './pages/AddPodcast'; import Navigation from './components/Navigation'; const queryClient = new QueryClient({ @@ -47,10 +48,18 @@ export default function App() { } /> - + + + } + /> + + } /> diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx index c7c2504..c58f81c 100644 --- a/frontend/src/components/Navigation.tsx +++ b/frontend/src/components/Navigation.tsx @@ -1,5 +1,5 @@ import { Link, useLocation } from 'react-router-dom'; -import { Home, Compass } from 'lucide-react'; +import { Home, Flame, PlusCircle } from 'lucide-react'; export default function Navigation() { const location = useLocation(); @@ -32,15 +32,27 @@ export default function Navigation() { + + Tendance + + + - - Découvrir + + Ajouter diff --git a/frontend/src/pages/AddPodcast.tsx b/frontend/src/pages/AddPodcast.tsx new file mode 100644 index 0000000..6e61de5 --- /dev/null +++ b/frontend/src/pages/AddPodcast.tsx @@ -0,0 +1,215 @@ +import { useState } from 'react'; +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 } from 'lucide-react'; +import { authService } from '../services/authService'; + +export default function AddPodcast() { + const queryClient = useQueryClient(); + const [user, setUser] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [subscribingId, setSubscribingId] = useState(null); + const [rssUrl, setRssUrl] = useState(''); + const [rssLoading, setRssLoading] = useState(false); + const [rssError, setRssError] = useState(null); + const [rssSuccess, setRssSuccess] = useState(null); + + useState(() => { + const loadUser = async () => { + try { + const response = await authService.getMe(); + setUser(response.user); + } catch (error) { + console.error('Failed to load user:', error); + } + }; + loadUser(); + }, []); + + const { data: searchResults, isLoading: isSearching } = useQuery({ + queryKey: ['discover', 'search', searchQuery], + queryFn: () => discoveryService.discoverPodcasts(searchQuery, 30), + enabled: searchQuery.length > 2, + staleTime: 5 * 60 * 1000, + }); + + const subscribeMutation = useMutation({ + mutationFn: (podcast: DiscoveryPodcast) => + discoveryService.subscribeFromDiscovery(podcast.rssUrl, { + title: podcast.title, + author: podcast.author, + imageUrl: podcast.imageUrl, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); + }, + }); + + const handleSubscribe = async (podcast: DiscoveryPodcast) => { + try { + setSubscribingId(podcast.id); + await subscribeMutation.mutateAsync(podcast); + alert(`"${podcast.title}" ajouté à vos abonnements !`); + queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] }); + } catch (error: any) { + const errorMessage = error.response?.data?.message || 'Failed to subscribe'; + alert(`Erreur : ${errorMessage}`); + } finally { + setSubscribingId(null); + } + }; + + const handleRssSubscribe = async (e: React.FormEvent) => { + e.preventDefault(); + if (!rssUrl.trim()) return; + setRssLoading(true); + setRssError(null); + 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 !'}`); + setRssUrl(''); + queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); + } catch (error: any) { + setRssError(error.response?.data?.message || 'Impossible d\'ajouter ce podcast'); + } finally { + setRssLoading(false); + } + }; + + const handleLogout = async () => { + try { + await authService.logout(); + authService.clearTokens(); + window.location.href = '/login'; + } catch (error) { + console.error('Logout failed:', error); + } + }; + + return ( +
+
+ +
+ {/* RSS URL Section */} +
+
+ +

Ajouter via URL RSS

+
+
+ { 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" + /> + +
+ {rssError &&

{rssError}

} + {rssSuccess &&

{rssSuccess}

} +
+ + {/* Search Bar */} +
+
+ +

Rechercher dans l'annuaire

+
+
+ + 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" + /> +
+
+ + {/* Results Grid */} + {isSearching ? ( +
+
+ +

Recherche en cours...

+
+
+ ) : searchResults?.podcasts && searchResults.podcasts.length > 0 ? ( +
+ {searchResults.podcasts.map((podcast) => ( +
+ {podcast.imageUrl && ( +
+ {podcast.title} +
+ )} +
+

+ {podcast.title} +

+ {podcast.author && ( +

{podcast.author}

+ )} +

+ {podcast.description} +

+
+ +
+
+
+ ))} +
+ ) : searchQuery.length >= 2 ? ( +
+

Aucun podcast trouvé.

+

Essayez de rechercher des titres, auteurs ou sujets

+
+ ) : ( +
+

Entrez au moins 2 caractères pour lancer la recherche.

+
+ )} + +
+
+ ); +} diff --git a/frontend/src/pages/Trending.tsx b/frontend/src/pages/Trending.tsx new file mode 100644 index 0000000..1bcb03a --- /dev/null +++ b/frontend/src/pages/Trending.tsx @@ -0,0 +1,143 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { discoveryService, DiscoveryPodcast } from '../services/discoveryService'; +import Header from '../components/Header'; +import { Loader, Plus, Flame } from 'lucide-react'; +import { authService } from '../services/authService'; + +export default function Trending() { + const queryClient = useQueryClient(); + const [user, setUser] = useState(null); + const [subscribingId, setSubscribingId] = useState(null); + + useState(() => { + const loadUser = async () => { + try { + const response = await authService.getMe(); + setUser(response.user); + } catch (error) { + console.error('Failed to load user:', error); + } + }; + loadUser(); + }, []); + + const { data: trendingData, isLoading } = useQuery({ + queryKey: ['discover', 'trending'], + queryFn: () => discoveryService.getTrendingPodcasts(30), + staleTime: 30 * 60 * 1000, + }); + + const subscribeMutation = useMutation({ + mutationFn: (podcast: DiscoveryPodcast) => + discoveryService.subscribeFromDiscovery(podcast.rssUrl, { + title: podcast.title, + author: podcast.author, + imageUrl: podcast.imageUrl, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['podcasts', 'subscriptions'] }); + }, + }); + + const handleSubscribe = async (podcast: DiscoveryPodcast) => { + try { + setSubscribingId(podcast.id); + await subscribeMutation.mutateAsync(podcast); + alert(`"${podcast.title}" ajouté à vos abonnements !`); + queryClient.invalidateQueries({ queryKey: ['episodes', 'latest'] }); + } catch (error: any) { + const errorMessage = error.response?.data?.message || 'Failed to subscribe'; + alert(`Erreur : ${errorMessage}`); + } finally { + setSubscribingId(null); + } + }; + + const handleLogout = async () => { + try { + await authService.logout(); + authService.clearTokens(); + window.location.href = '/login'; + } catch (error) { + console.error('Logout failed:', error); + } + }; + + return ( +
+
+ +
+
+ +

Podcasts Tendances

+
+ + {isLoading ? ( +
+
+ +

Chargement des podcasts...

+
+
+ ) : trendingData?.podcasts && trendingData.podcasts.length > 0 ? ( +
+ {trendingData.podcasts.map((podcast) => ( +
+ {podcast.imageUrl && ( +
+ {podcast.title} +
+ )} +
+

+ {podcast.title} +

+ {podcast.author && ( +

{podcast.author}

+ )} +

+ {podcast.description} +

+
+ +
+
+
+ ))} +
+ ) : ( +
+

Aucun podcast tendance trouvé.

+
+ )} +
+
+ ); +}