Traduit l'UI en français et corrige les bugs de recherche/abonnement

- Traduit toutes les pages et composants en français (Dashboard, Login, Discover, Navigation, Header, EpisodeCard, AudioPlayer)
- Corrige l'erreur 500 sur /api/podcasts/discover : z.coerce.number() au lieu de z.number() pour limit/offset (paramètres query string)
- Corrige l'abonnement manuel via URL RSS : crée le podcast depuis le flux RSS au lieu de renvoyer 400

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-04-16 11:15:42 +02:00
1 parent 00d92fc668
commit c7a5c171d1
8 files changed
+68 -51

No files matched your search

+24 -7
View File
@@ -12,8 +12,8 @@ const subscribeSchema = z.object({
const searchSchema = z.object({
q: z.string().min(2, 'Query must be at least 2 characters'),
limit: z.number().int().min(1).max(50).optional().default(20),
offset: z.number().int().min(0).optional().default(0),
limit: z.coerce.number().int().min(1).max(50).optional().default(20),
offset: z.coerce.number().int().min(0).optional().default(0),
});
export const getUserSubscriptions = async (req: Request, res: Response) => {
@@ -49,11 +49,28 @@ export const subscribe = async (req: Request, res: Response) => {
let podcast = await Podcast.findOne({ rssUrl });
if (!podcast) {
// For now, create a basic podcast entry
// In production, you'd fetch from PodcastIndex API or parse the RSS feed
return res.status(400).json({
message: 'Podcast not found. Please use a valid RSS URL.',
});
try {
const result = await rssParserService.createPodcastFromRss(rssUrl, {
title: 'Unknown Podcast',
description: '',
author: '',
imageUrl: undefined,
episodes: [],
});
podcast = result.podcast;
} catch (error: any) {
if (error.message.includes('already in database')) {
podcast = await Podcast.findOne({ rssUrl });
} else {
return res.status(400).json({
message: `Impossible de récupérer le podcast : ${error.message}`,
});
}
}
}
if (!podcast) {
return res.status(400).json({ message: 'Impossible d\'ajouter le podcast' });
}
// Check if already subscribed
+5 -5
View File
@@ -133,7 +133,7 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
{/* 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">Now Playing</h2>
<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"
@@ -163,7 +163,7 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
{/* Info */}
<div className="text-center mb-8">
<p className="text-sm font-semibold text-light-500 uppercase mb-2">
{podcast?.author || 'Unknown Podcast'}
{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>
@@ -195,7 +195,7 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
}
}}
className="p-3 hover:bg-light-100 rounded-full transition-colors"
title="Rewind 15s"
title="Reculer de 15s"
>
<SkipBack className="w-6 h-6" />
</button>
@@ -220,7 +220,7 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
}
}}
className="p-3 hover:bg-light-100 rounded-full transition-colors"
title="Skip 30s"
title="Avancer de 30s"
>
<SkipForward className="w-6 h-6" />
</button>
@@ -230,7 +230,7 @@ export default function AudioPlayer({ episode, onClose, userId }: AudioPlayerPro
<div className="grid grid-cols-2 gap-6">
{/* Playback Speed */}
<div className="card">
<h3 className="text-sm font-semibold text-light-900 mb-3">Speed</h3>
<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
+2 -2
View File
@@ -8,7 +8,7 @@ interface EpisodeCardProps {
export default function EpisodeCard({ episode, onPlay }: EpisodeCardProps) {
const podcast = typeof episode.podcastId === 'object' ? episode.podcastId : null;
const pubDate = new Date(episode.pubDate).toLocaleDateString('en-US', {
const pubDate = new Date(episode.pubDate).toLocaleDateString('fr-FR', {
month: 'short',
day: 'numeric',
});
@@ -45,7 +45,7 @@ export default function EpisodeCard({ episode, onPlay }: EpisodeCardProps) {
{/* Podcast Title */}
<p className="text-xs font-semibold text-light-500 uppercase tracking-wider mb-1">
{podcast?.author || 'Unknown'}
{podcast?.author || 'Inconnu'}
</p>
{/* Episode Title */}
+1 -1
View File
@@ -47,7 +47,7 @@ export default function Header({ title, subtitle, user, onLogout }: HeaderProps)
onClick={onLogout}
className="px-4 py-2 text-sm text-light-600 hover:text-light-900 transition-colors"
>
Logout
Déconnexion
</button>
)}
</div>
+2 -2
View File
@@ -28,7 +28,7 @@ export default function Navigation() {
}`}
>
<Home className="w-5 h-5" />
<span className="font-medium">Dashboard</span>
<span className="font-medium">Tableau de bord</span>
</Link>
<Link
@@ -40,7 +40,7 @@ export default function Navigation() {
}`}
>
<Compass className="w-5 h-5" />
<span className="font-medium">Discover</span>
<span className="font-medium">Découvrir</span>
</Link>
</div>
</div>
+8 -8
View File
@@ -50,7 +50,7 @@ export default function Dashboard() {
<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">Loading your podcasts...</p>
<p className="text-light-600">Chargement de vos podcasts...</p>
</div>
</div>
);
@@ -60,7 +60,7 @@ export default function Dashboard() {
<div className="min-h-screen bg-light-50">
<Header
title="Podcastic"
subtitle="Your podcast companion"
subtitle="Votre compagnon de podcasts"
user={user}
onLogout={handleLogout}
/>
@@ -69,10 +69,10 @@ export default function Dashboard() {
{/* Welcome Section */}
<div className="mb-12">
<h2 className="text-3xl font-bold text-light-900 mb-2">
Welcome back, {user?.username}! 👋
Bon retour, {user?.username} ! 👋
</h2>
<p className="text-light-600">
{episodesData?.count || 0} new episodes from your subscriptions
{episodesData?.count || 0} nouveaux épisodes de vos abonnements
</p>
</div>
@@ -81,12 +81,12 @@ export default function Dashboard() {
<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">Loading episodes...</p>
<p className="text-light-600">Chargement des épisodes...</p>
</div>
</div>
) : episodesData?.episodes && episodesData.episodes.length > 0 ? (
<div>
<h3 className="text-2xl font-bold text-light-900 mb-6">Latest Episodes</h3>
<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
@@ -99,9 +99,9 @@ export default function Dashboard() {
</div>
) : (
<div className="text-center py-12">
<p className="text-light-600 text-lg mb-4">No episodes yet</p>
<p className="text-light-600 text-lg mb-4">Aucun épisode pour l'instant</p>
<p className="text-light-500">
Subscribe to podcasts to see episodes in your dashboard
Abonnez-vous à des podcasts pour voir les épisodes dans votre tableau de bord
</p>
</div>
)}
+14 -14
View File
@@ -60,10 +60,10 @@ export default function Discover() {
setSubscribingId(podcast.id);
await subscribeMutation.mutateAsync(podcast);
// Show success toast (you might want to add a toast library)
alert(`Added "${podcast.title}" to your subscriptions!`);
alert(`"${podcast.title}" ajouté à vos abonnements !`);
} catch (error: any) {
const errorMessage = error.response?.data?.message || 'Failed to subscribe';
alert(`Error: ${errorMessage}`);
alert(`Erreur : ${errorMessage}`);
} finally {
setSubscribingId(null);
}
@@ -86,7 +86,7 @@ export default function Discover() {
<div className="min-h-screen bg-light-50">
<Header
title="Discover"
subtitle="Find and subscribe to podcasts"
subtitle="Trouvez et abonnez-vous à des podcasts"
user={user}
onLogout={handleLogout}
/>
@@ -98,7 +98,7 @@ export default function Discover() {
<Search className="absolute left-4 top-3.5 w-5 h-5 text-light-400" />
<input
type="text"
placeholder="Search podcasts, authors, topics..."
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"
@@ -116,7 +116,7 @@ export default function Discover() {
: 'bg-light-200 text-light-700 hover:bg-light-300'
}`}
>
🔥 Trending
🔥 Tendances
</button>
<button
onClick={() => setSelectedTab('search')}
@@ -127,7 +127,7 @@ export default function Discover() {
: 'bg-light-200 text-light-700 hover:bg-light-300'
}`}
>
🔍 Search Results
🔍 Résultats de recherche
</button>
</div>
@@ -136,7 +136,7 @@ export default function Discover() {
<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">Loading podcasts...</p>
<p className="text-light-600">Chargement des podcasts...</p>
</div>
</div>
) : displayData && displayData.length > 0 ? (
@@ -171,7 +171,7 @@ export default function Discover() {
<div className="flex items-center justify-between">
<div className="flex gap-2 text-xs text-light-500">
{podcast.episodeCount && (
<span>{podcast.episodeCount} episodes</span>
<span>{podcast.episodeCount} épisodes</span>
)}
{podcast.language && <span>{podcast.language}</span>}
</div>
@@ -184,12 +184,12 @@ export default function Discover() {
{subscribingId === podcast.id ? (
<>
<Loader className="w-4 h-4 animate-spin" />
Adding...
Ajout...
</>
) : (
<>
<Plus className="w-4 h-4" />
Add
Ajouter
</>
)}
</button>
@@ -202,12 +202,12 @@ export default function Discover() {
<div className="text-center py-20">
<p className="text-light-600 text-lg mb-2">
{selectedTab === 'search' && searchQuery.length < 2
? 'Type at least 2 characters to search'
: 'No podcasts found'}
? 'Tapez au moins 2 caractères pour rechercher'
: 'Aucun podcast trouvé'}
</p>
{selectedTab === 'search' && (
<p className="text-light-500">
Try searching for podcast titles, authors, or topics
Essayez de rechercher des titres, auteurs ou sujets
</p>
)}
</div>
@@ -217,7 +217,7 @@ export default function Discover() {
{(searchResults || trendingData) && (
<div className="mt-8 text-center text-sm text-light-500">
<p>
Powered by{' '}
Propulsé par{' '}
<a
href="https://podcastindex.org"
target="_blank"
+12 -12
View File
@@ -39,7 +39,7 @@ export default function Login() {
navigate('/dashboard');
}
} catch (err: any) {
setError(err.response?.data?.message || 'An error occurred');
setError(err.response?.data?.message || 'Une erreur s\'est produite');
} finally {
setLoading(false);
}
@@ -54,7 +54,7 @@ export default function Login() {
🎙️
</div>
<h1 className="text-4xl font-bold text-light-900 mb-2">Podcastic</h1>
<p className="text-light-600">Your podcast streaming companion</p>
<p className="text-light-600">Votre compagnon de streaming de podcasts</p>
</div>
{/* Form Card */}
@@ -78,7 +78,7 @@ export default function Login() {
}`}
onClick={() => setIsLogin(true)}
>
Sign In
Se connecter
</button>
<button
className={`flex-1 py-2 rounded-lg font-medium transition-colors ${
@@ -88,7 +88,7 @@ export default function Login() {
}`}
onClick={() => setIsLogin(false)}
>
Create Account
Créer un compte
</button>
</div>
</button>
@@ -99,7 +99,7 @@ export default function Login() {
<input
type="text"
name="username"
placeholder="Username"
placeholder="Nom d'utilisateur"
value={formData.username}
onChange={handleChange}
required={!isLogin}
@@ -110,7 +110,7 @@ export default function Login() {
<input
type="email"
name="email"
placeholder="Email address"
placeholder="Adresse e-mail"
value={formData.email}
onChange={handleChange}
required
@@ -120,7 +120,7 @@ export default function Login() {
<input
type="password"
name="password"
placeholder="Password"
placeholder="Mot de passe"
value={formData.password}
onChange={handleChange}
required
@@ -141,12 +141,12 @@ export default function Login() {
{loading ? (
<div className="flex items-center justify-center gap-2">
<div className="spinner border-white"></div>
{isLogin ? 'Signing in...' : 'Creating account...'}
{isLogin ? 'Connexion en cours...' : 'Création du compte...'}
</div>
) : isLogin ? (
'Sign In'
'Se connecter'
) : (
'Create Account'
'Créer un compte'
)}
</button>
</form>
@@ -155,8 +155,8 @@ export default function Login() {
{/* Footer */}
<p className="text-center text-sm text-light-600">
{isLogin
? "Don't have an account? Click Create Account above"
: 'Already have an account? Click Sign In above'}
? "Pas de compte ? Cliquez sur Créer un compte ci-dessus"
: 'Déjà un compte ? Cliquez sur Se connecter ci-dessus'}
</p>
</div>
</div>