mirror of
https://github.com/R0m1k3/podcastic.git
synced 2026-10-23 06:58:03 +02:00
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:
1 parent
00d92fc668
commit
c7a5c171d1
8 files changed
+68
-51
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user