mirror of
https://github.com/R0m1k3/podcastic.git
synced 2026-10-11 17:26:20 +02:00
feat: composant PodcastCard unifié pour Library, Trending, AddPodcast
Crée PodcastCard réutilisable avec le même design qu'EpisodeCard (image carrée, auteur, titre, footer avec actions). Remplace les 3 designs inline différents par un seul composant cohérent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
00df4a39ff
commit
77f2a71efd
4 files changed
+210
-181
No files matched your search
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import { Play, Plus, Check, Loader, Trash2, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface PodcastCardProps {
|
||||
title: string;
|
||||
author?: string;
|
||||
imageUrl?: string;
|
||||
rank?: number;
|
||||
episodeCount?: number;
|
||||
language?: string;
|
||||
lastEpisodeTitle?: string;
|
||||
lastEpisodeDate?: string;
|
||||
// Subscribe action
|
||||
onSubscribe?: () => void;
|
||||
isSubscribed?: boolean;
|
||||
isSubscribing?: boolean;
|
||||
// Library actions
|
||||
onSync?: () => void;
|
||||
isSyncing?: boolean;
|
||||
onUnsubscribe?: () => void;
|
||||
confirmDelete?: boolean;
|
||||
onConfirmDelete?: () => void;
|
||||
onCancelDelete?: () => void;
|
||||
}
|
||||
|
||||
export default function PodcastCard({
|
||||
title,
|
||||
author,
|
||||
imageUrl,
|
||||
rank,
|
||||
episodeCount,
|
||||
language,
|
||||
lastEpisodeTitle,
|
||||
lastEpisodeDate,
|
||||
onSubscribe,
|
||||
isSubscribed,
|
||||
isSubscribing,
|
||||
onSync,
|
||||
isSyncing,
|
||||
onUnsubscribe,
|
||||
confirmDelete,
|
||||
onConfirmDelete,
|
||||
onCancelDelete,
|
||||
}: PodcastCardProps) {
|
||||
return (
|
||||
<div className="group premium-card premium-glass rounded-[var(--radius-card)] p-4 hover:bg-[var(--bg-secondary)] transition-all duration-500 border border-[var(--border-color)] hover:border-[var(--accent-primary)]/30 flex flex-col h-full relative">
|
||||
{/* Image */}
|
||||
<div className="relative aspect-square rounded-xl overflow-hidden mb-4 shadow-xl border border-[var(--border-color)]">
|
||||
{imageUrl ? (
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={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-[var(--accent-primary)] to-[var(--accent-secondary)] flex items-center justify-center text-4xl">🎙️</div>
|
||||
)}
|
||||
|
||||
{rank && (
|
||||
<div className="absolute top-3 left-3 px-2.5 py-1 rounded-lg bg-black/60 backdrop-blur-md border border-white/10 text-[9px] font-black text-white uppercase tracking-widest shadow-lg z-10">
|
||||
#{rank}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSubscribed && (
|
||||
<div className="absolute top-3 right-3 px-2.5 py-1 rounded-lg bg-emerald-500/90 backdrop-blur-md border border-emerald-300/30 text-[9px] font-black text-white uppercase tracking-widest flex items-center gap-1 shadow-lg z-10">
|
||||
<Check className="w-3 h-3" />
|
||||
Abonné
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hover play overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center backdrop-blur-[2px]">
|
||||
<div className="w-16 h-16 rounded-full bg-white text-obsidian flex items-center justify-center shadow-glow-indigo transform scale-90 group-hover:scale-100 transition-transform duration-500">
|
||||
<Play className="w-6 h-6 fill-current ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(episodeCount !== undefined || language) && (
|
||||
<div className="absolute bottom-3 left-3 flex items-center gap-1.5 z-10">
|
||||
{episodeCount !== undefined && (
|
||||
<div className="px-2 py-1 rounded-lg bg-black/60 backdrop-blur-md border border-white/10 text-[9px] font-black text-white uppercase tracking-widest">
|
||||
{episodeCount} ép.
|
||||
</div>
|
||||
)}
|
||||
{language && (
|
||||
<div className="px-2 py-1 rounded-lg bg-black/60 backdrop-blur-md border border-white/10 text-[9px] font-black text-white uppercase tracking-widest">
|
||||
{language}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{author && (
|
||||
<span className="text-[9px] font-black text-[var(--accent-primary)] uppercase tracking-[0.2em] truncate mb-1">
|
||||
{author}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<h3 className="text-sm font-bold line-clamp-2 leading-relaxed group-hover:text-[var(--accent-primary)] transition-colors flex-1 mb-4">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{lastEpisodeTitle && (
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-3 mb-4">
|
||||
<p className="text-[9px] font-black text-[var(--accent-primary)] uppercase tracking-[0.15em] mb-1">Dernier épisode</p>
|
||||
<p className="text-xs font-bold line-clamp-1">{lastEpisodeTitle}</p>
|
||||
{lastEpisodeDate && (
|
||||
<p className="text-[9px] text-[var(--text-secondary)] mt-0.5">
|
||||
{new Date(lastEpisodeDate).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-3 border-t border-[var(--border-color)] mt-auto">
|
||||
{confirmDelete ? (
|
||||
<div className="flex items-center gap-2 animate-fade-in">
|
||||
<button onClick={onConfirmDelete} className="flex-1 py-2 bg-accent-rose text-white rounded-xl text-[10px] font-black uppercase hover:bg-accent-rose/80 transition-all">Confirmer</button>
|
||||
<button onClick={onCancelDelete} className="flex-1 py-2 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-xl text-[10px] font-black uppercase hover:bg-[var(--bg-primary)] transition-all">Annuler</button>
|
||||
</div>
|
||||
) : onUnsubscribe ? (
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
{onSync && (
|
||||
<button
|
||||
onClick={onSync}
|
||||
disabled={isSyncing}
|
||||
className="flex items-center gap-1.5 text-[10px] font-bold text-[var(--text-secondary)] hover:text-[var(--accent-primary)] transition-all uppercase tracking-widest disabled:opacity-50"
|
||||
>
|
||||
{isSyncing ? <Loader className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
||||
Resync
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onUnsubscribe}
|
||||
className="flex items-center gap-1.5 text-[10px] font-bold text-[var(--text-secondary)] hover:text-accent-rose transition-all uppercase tracking-widest"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Désabonner
|
||||
</button>
|
||||
</div>
|
||||
) : onSubscribe ? (
|
||||
<button
|
||||
onClick={onSubscribe}
|
||||
disabled={isSubscribing || isSubscribed}
|
||||
className={`w-full py-2.5 rounded-xl font-bold uppercase tracking-widest text-[10px] transition-all flex items-center justify-center gap-2 ${
|
||||
isSubscribed
|
||||
? 'bg-[var(--accent-secondary)]/10 text-[var(--accent-secondary)] border border-[var(--accent-secondary)]/20'
|
||||
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:bg-[var(--accent-primary)] hover:text-white active:scale-95'
|
||||
}`}
|
||||
>
|
||||
{isSubscribing ? (
|
||||
<Loader className="w-3.5 h-3.5 animate-spin" />
|
||||
) : isSubscribed ? (
|
||||
<><Check className="w-3.5 h-3.5" /> Abonné</>
|
||||
) : (
|
||||
<><Plus className="w-3.5 h-3.5" /> S'abonner</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import EpisodeCard from '../components/EpisodeCard';
|
||||
import EpisodeDetails from '../components/EpisodeDetails';
|
||||
import { useAudio } from '../context/AudioContext';
|
||||
import AlertModal, { AlertType } from '../components/AlertModal';
|
||||
import { Search, Plus, Loader, Rss, Check, Globe, Sparkles, PlusCircle, LayoutGrid, ListMusic } from 'lucide-react';
|
||||
import { Search, Loader, Rss, Globe, Sparkles, PlusCircle, LayoutGrid, ListMusic } from 'lucide-react';
|
||||
import PodcastCard from '../components/PodcastCard';
|
||||
import { authService } from '../services/authService';
|
||||
import SuccessModal from '../components/SuccessModal';
|
||||
|
||||
@@ -237,33 +238,15 @@ export default function AddPodcast() {
|
||||
) : searchType === 'podcasts' && (searchQuery.length > 0 ? searchResults?.podcasts : trendingResults?.podcasts) ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 pb-32">
|
||||
{(searchQuery.length > 0 ? searchResults?.podcasts : trendingResults?.podcasts).map((podcast: any) => (
|
||||
<div key={podcast.id} className="group premium-card premium-glass rounded-[var(--radius-card)] overflow-hidden flex flex-col hover:bg-[var(--bg-secondary)] transition-all duration-500 border-[var(--border-color)]">
|
||||
<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-[var(--bg-primary)] via-transparent to-transparent opacity-60" />
|
||||
</div>
|
||||
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
<h3 className="text-lg font-bold mb-2 line-clamp-2 leading-tight group-hover:text-[var(--accent-primary)] transition-colors">{podcast.title}</h3>
|
||||
<p className="text-[10px] font-bold text-[var(--text-secondary)] 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-[var(--accent-secondary)]/10 text-[var(--accent-secondary)] border border-[var(--accent-secondary)]/20'
|
||||
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:bg-[var(--accent-primary)] 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>
|
||||
<PodcastCard
|
||||
key={podcast.id}
|
||||
title={podcast.title}
|
||||
author={podcast.author}
|
||||
imageUrl={podcast.imageUrl}
|
||||
onSubscribe={() => handleSubscribe(podcast)}
|
||||
isSubscribed={isSubscribed(podcast.rssUrl)}
|
||||
isSubscribing={subscribingId === podcast.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : searchType === 'episodes' && episodeResults?.episodes ? (
|
||||
|
||||
+18
-102
@@ -3,7 +3,8 @@ 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, Play, RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import { BookOpen, Rss } from 'lucide-react';
|
||||
import PodcastCard from '../components/PodcastCard';
|
||||
import { Link } from 'react-router-dom';
|
||||
import AlertModal, { AlertType } from '../components/AlertModal';
|
||||
|
||||
@@ -149,107 +150,22 @@ export default function Library() {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 pb-10">
|
||||
{podcasts.map((podcast) => (
|
||||
<div key={podcast._id} className="group premium-card premium-glass rounded-[var(--radius-card)] p-6 hover:bg-[var(--bg-secondary)] transition-all duration-500 flex flex-col h-full border-[var(--border-color)]">
|
||||
<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-[var(--border-color)] 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-[var(--accent-primary)] to-[var(--accent-secondary)] flex items-center justify-center text-3xl">🎙️</div>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-lg font-bold truncate leading-tight group-hover:text-[var(--accent-primary)] transition-colors">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[10px] font-bold text-[var(--text-secondary)] 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-[var(--bg-secondary)] border border-[var(--border-color)] text-[9px] font-black text-[var(--text-secondary)] uppercase tracking-tighter">
|
||||
{podcast.episodeCount} EPISODES
|
||||
</span>
|
||||
{podcast.language && (
|
||||
<span className="px-2.5 py-1 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)] text-[9px] font-black text-[var(--text-secondary)] uppercase">
|
||||
{podcast.language}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
)}
|
||||
Synchroniser maintenant
|
||||
</button>
|
||||
</div>
|
||||
) : podcast.lastEpisodeTitle && (
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-2xl p-4 mb-6 group-hover:bg-[var(--accent-primary)]/5 transition-colors">
|
||||
<p className="text-[10px] font-bold text-[var(--accent-primary)] uppercase tracking-[0.2em] mb-2">Dernier épisode</p>
|
||||
<p className="text-sm font-bold line-clamp-1 mb-1">{podcast.lastEpisodeTitle}</p>
|
||||
<p className="text-[10px] font-medium text-[var(--text-secondary)] italic">
|
||||
{new Date(podcast.lastEpisodeDate!).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions Footer */}
|
||||
<div className="flex items-center justify-between pt-6 border-t border-[var(--border-color)] 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-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-xl text-[11px] font-black uppercase hover:bg-[var(--bg-primary)]">Annuler</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4 ml-auto">
|
||||
<button
|
||||
onClick={() => syncMutation.mutate(podcast._id)}
|
||||
disabled={syncMutation.isPending && syncMutation.variables === podcast._id}
|
||||
className="flex items-center gap-2 text-[10px] font-bold text-[var(--text-secondary)] hover:text-[var(--accent-primary)] transition-all uppercase tracking-widest disabled:opacity-50"
|
||||
title="Resynchroniser les métadonnées et épisodes"
|
||||
>
|
||||
{syncMutation.isPending && syncMutation.variables === podcast._id ? (
|
||||
<Loader className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
)}
|
||||
Resync
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDeleteId(podcast.subscriptionId || podcast._id)}
|
||||
className="flex items-center gap-2 text-[10px] font-bold text-[var(--text-secondary)] hover:text-accent-rose transition-all uppercase tracking-widest"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Désabonner
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PodcastCard
|
||||
key={podcast._id}
|
||||
title={podcast.title}
|
||||
author={podcast.author}
|
||||
imageUrl={podcast.imageUrl}
|
||||
episodeCount={podcast.episodeCount}
|
||||
language={podcast.language}
|
||||
lastEpisodeTitle={podcast.lastEpisodeTitle}
|
||||
lastEpisodeDate={podcast.lastEpisodeDate}
|
||||
onSync={() => syncMutation.mutate(podcast._id)}
|
||||
isSyncing={syncMutation.isPending && syncMutation.variables === podcast._id}
|
||||
onUnsubscribe={() => setConfirmDeleteId(podcast.subscriptionId || podcast._id)}
|
||||
confirmDelete={confirmDeleteId === (podcast.subscriptionId || podcast._id)}
|
||||
onConfirmDelete={handleConfirmUnsubscribe}
|
||||
onCancelDelete={() => setConfirmDeleteId(null)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,8 @@ 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, Sparkles, TrendingUp } from 'lucide-react';
|
||||
import { Loader, Flame, Sparkles, TrendingUp } from 'lucide-react';
|
||||
import PodcastCard from '../components/PodcastCard';
|
||||
import { authService } from '../services/authService';
|
||||
import SuccessModal from '../components/SuccessModal';
|
||||
import AlertModal, { AlertType } from '../components/AlertModal';
|
||||
@@ -201,56 +202,16 @@ export default function Trending() {
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 mb-24 stagger-grid">
|
||||
{podcasts.map((podcast, index) => (
|
||||
<div key={`${skip}-${podcast.id}`} className="group premium-card premium-glass rounded-[var(--radius-card)] overflow-hidden flex flex-col hover:bg-[var(--bg-secondary)] transition-all duration-500 border border-[var(--border-color)]">
|
||||
<div className="relative aspect-square overflow-hidden bg-black/5">
|
||||
{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-[var(--accent-primary)] to-[var(--accent-secondary)] flex items-center justify-center text-4xl">🎙️</div>
|
||||
)}
|
||||
<div className="absolute top-4 left-4 px-3 py-1 rounded-full bg-black/60 backdrop-blur-md border border-white/10 text-[10px] font-black text-white uppercase tracking-widest shadow-lg z-10">
|
||||
Rank #{index + 1}
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
|
||||
<div className="p-5 flex-1 flex flex-col">
|
||||
<h3 className="text-sm font-bold mb-2 line-clamp-2 leading-tight group-hover:text-[var(--accent-primary)] transition-colors min-h-[40px]">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
{podcast.author && (
|
||||
<p className="text-[10px] font-bold text-[var(--text-secondary)] uppercase tracking-widest mb-4 truncate">{podcast.author}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handleSubscribe(podcast)}
|
||||
disabled={subscribingId === podcast.id || subscribeMutation.isPending || isSubscribed(podcast.rssUrl)}
|
||||
className={`mt-auto w-full py-3 rounded-xl font-bold uppercase tracking-widest text-[9px] transition-all duration-300 flex items-center justify-center gap-2 ${
|
||||
isSubscribed(podcast.rssUrl)
|
||||
? 'bg-[var(--accent-secondary)]/10 text-[var(--accent-secondary)] border border-[var(--accent-secondary)]/20'
|
||||
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:bg-[var(--accent-primary)] hover:text-white shadow-lg active:scale-95'
|
||||
}`}
|
||||
>
|
||||
{subscribingId === podcast.id ? (
|
||||
<Loader className="w-3.5 h-3.5 animate-spin" />
|
||||
) : isSubscribed(podcast.rssUrl) ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
Abonné
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
S'abonner
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PodcastCard
|
||||
key={`${skip}-${podcast.id}`}
|
||||
title={podcast.title}
|
||||
author={podcast.author}
|
||||
imageUrl={podcast.imageUrl}
|
||||
rank={index + 1}
|
||||
onSubscribe={() => handleSubscribe(podcast)}
|
||||
isSubscribed={isSubscribed(podcast.rssUrl)}
|
||||
isSubscribing={subscribingId === podcast.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user