Files
itinova-podcasts/client/src/components/PodcastCard.tsx
manus-admin aab11c8308 Initial commit: itinova-podcasts v1
Stack: Node.js/Express + React/Vite + tRPC + MySQL (Drizzle ORM)
Features: Gestion de podcasts, établissements, mots-clés, upload audio S3
Migrations: 0000-0002 (users, etablissements, mots_cles, podcasts, podcast_mots_cles)
2026-04-12 18:34:56 -04:00

136 lines
4.8 KiB
TypeScript

import { Play, Pause, Clock, Building2 } from "lucide-react";
import type { PodcastWithRelations } from "../../../server/db";
interface Props {
podcast: PodcastWithRelations;
isPlaying: boolean;
onPlay: () => void;
}
// Palette de couleurs pour les artworks (basée sur l'ID du podcast)
const ARTWORK_COLORS = [
"from-blue-500 to-indigo-600",
"from-violet-500 to-purple-700",
"from-emerald-500 to-teal-600",
"from-orange-500 to-red-600",
"from-cyan-500 to-blue-600",
"from-rose-500 to-pink-600",
"from-amber-500 to-orange-600",
"from-indigo-500 to-blue-700",
];
function WaveformIcon() {
return (
<div className="flex items-center gap-[3px] h-5">
{[3, 5, 4, 6, 3, 5, 4].map((h, i) => (
<span
key={i}
className="waveform-bar w-[3px] bg-white"
style={{ height: `${h * 3}px`, animationDelay: `${i * 0.1}s` }}
/>
))}
</div>
);
}
function formatDuration(s?: number | null): string | null {
if (!s) return null;
const m = Math.floor(s / 60);
const sec = s % 60;
return `${m}:${sec.toString().padStart(2, "0")}`;
}
export default function PodcastCard({ podcast, isPlaying, onPlay }: Props) {
const duration = formatDuration(podcast.dureeSecondes);
const colorClass = ARTWORK_COLORS[podcast.id % ARTWORK_COLORS.length];
return (
<div
className={`group relative bg-card rounded-2xl overflow-hidden transition-all duration-300 cursor-pointer
${isPlaying
? "shadow-xl shadow-primary/20 ring-2 ring-primary/40 -translate-y-0.5"
: "shadow-sm hover:shadow-lg hover:-translate-y-1 border border-border hover:border-primary/20"
}`}
onClick={onPlay}
>
{/* Barre colorée en haut */}
<div className={`h-1 w-full bg-gradient-to-r ${colorClass}`} />
<div className="p-4 flex items-start gap-4">
{/* Artwork */}
<div className={`relative w-16 h-16 rounded-xl bg-gradient-to-br ${colorClass} flex items-center justify-center shrink-0 shadow-md`}>
{isPlaying ? (
<WaveformIcon />
) : (
<svg viewBox="0 0 24 24" fill="white" className="w-7 h-7 opacity-90">
<path d="M12 3a9 9 0 100 18A9 9 0 0012 3zm-1 13V8l6 4-6 4z" />
</svg>
)}
{/* Indicateur "en cours" */}
{isPlaying && (
<span className="absolute -top-1 -right-1 w-3.5 h-3.5 bg-emerald-500 rounded-full border-2 border-white animate-pulse" />
)}
</div>
{/* Contenu */}
<div className="flex-1 min-w-0">
<h3 className={`font-semibold text-sm leading-snug line-clamp-2 mb-1 ${isPlaying ? "text-primary" : "text-foreground group-hover:text-primary transition-colors"}`}>
{podcast.titre}
</h3>
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed mb-2">
{podcast.resume}
</p>
{/* Meta row */}
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground bg-muted/60 px-2 py-0.5 rounded-full">
<Building2 className="w-3 h-3 shrink-0" />
<span className="truncate max-w-[110px]">{podcast.etablissementNom}</span>
</span>
{duration && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="w-3 h-3" />
{duration}
</span>
)}
</div>
{/* Mots-clés */}
{podcast.motsCles.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{podcast.motsCles.slice(0, 3).map((mk) => (
<span
key={mk.id}
className="text-xs bg-primary/8 text-primary px-2 py-0.5 rounded-full font-medium border border-primary/15"
>
{mk.label}
</span>
))}
{podcast.motsCles.length > 3 && (
<span className="text-xs text-muted-foreground px-1">+{podcast.motsCles.length - 3}</span>
)}
</div>
)}
</div>
{/* Bouton play */}
{podcast.audioUrl && (
<button
onClick={(e) => { e.stopPropagation(); onPlay(); }}
className={`w-11 h-11 rounded-full flex items-center justify-center shrink-0 transition-all duration-200 shadow-md
${isPlaying
? "bg-primary text-white scale-110 shadow-primary/40"
: "bg-primary/10 text-primary hover:bg-primary hover:text-white hover:scale-110 hover:shadow-primary/30"
}`}
>
{isPlaying
? <Pause className="w-4 h-4" />
: <Play className="w-4 h-4 ml-0.5" />
}
</button>
)}
</div>
</div>
);
}