Retour au catalogue
Blog Audio Player
Blog format podcast avec barres de forme d'onde audio, lecture/pause et liste d'episodes.
blogmedium Both Responsive a11y
boldplayfuluniversaleducationsaasstacked
Theme
"use client";
import { useState, useMemo } from "react";
import { motion } from "framer-motion";
import { Play, Pause, Headphones, Clock, Mic } from "lucide-react";
interface FeaturedEpisode {
title: string;
description: string;
duration: string;
date: string;
guest: string;
}
interface Episode {
title: string;
duration: string;
date: string;
}
interface BlogAudioPlayerProps {
podcastName?: string;
podcastDescription?: string;
featuredEpisode?: FeaturedEpisode;
episodes?: Episode[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
function WaveformBars({ playing, barCount = 40 }: { playing: boolean; barCount?: number }) {
const heights = useMemo(
() => Array.from({ length: barCount }, () => 20 + Math.random() * 80),
[barCount]
);
return (
<div style={{ display: "flex", alignItems: "end", gap: 2, height: 48, flex: 1 }}>
{heights.map((h, i) => (
<motion.div
key={i}
animate={playing ? { height: [h * 0.3, h * 0.01 * 48, h * 0.5 * 0.01 * 48] } : { height: h * 0.01 * 48 }}
transition={playing ? { duration: 0.4 + Math.random() * 0.4, repeat: Infinity, repeatType: "reverse" } : { duration: 0.3 }}
style={{ flex: 1, minWidth: 2, maxWidth: 6, borderRadius: 2, background: i < barCount * 0.4 ? "var(--color-accent)" : "var(--color-border)" }}
/>
))}
</div>
);
}
export default function BlogAudioPlayer({
podcastName = "Le Podcast Tech",
podcastDescription = "Conversations approfondies sur le design, le code et l'innovation.",
featuredEpisode,
episodes = [],
}: BlogAudioPlayerProps) {
const [playing, setPlaying] = useState(false);
const feat = featuredEpisode ?? {
title: "Episode en vedette",
description: "Description de l'episode.",
duration: "45:00",
date: "10 mars 2026",
guest: "Invite",
};
return (
<section style={{ paddingTop: "var(--section-padding-y)", paddingBottom: "var(--section-padding-y)", background: "var(--color-background)" }}>
<div style={{ maxWidth: 780, margin: "0 auto", padding: "0 var(--container-padding-x)" }}>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: EASE }}
style={{ textAlign: "center", marginBottom: "2.5rem" }}
>
<div style={{ display: "inline-flex", alignItems: "center", gap: "0.5rem", marginBottom: "1rem", padding: "0.4rem 1rem", borderRadius: "var(--radius-full)", background: "var(--color-accent-subtle)" }}>
<Headphones style={{ width: 16, height: 16, color: "var(--color-accent)" }} />
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-accent)" }}>Podcast</span>
</div>
<h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.75rem, 3vw, 2.5rem)", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.5rem" }}>{podcastName}</h2>
<p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)" }}>{podcastDescription}</p>
</motion.div>
{/* Featured episode player */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.08, ease: EASE }}
style={{ padding: "2rem", borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", marginBottom: "2rem" }}
>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.75rem", fontSize: "0.75rem", color: "var(--color-foreground-muted)" }}>
<Mic style={{ width: 12, height: 12 }} />
<span>Invite : {feat.guest}</span>
<span style={{ margin: "0 0.25rem" }}>·</span>
<span>{feat.date}</span>
</div>
<h3 style={{ fontFamily: "var(--font-sans)", fontSize: "1.25rem", fontWeight: 700, color: "var(--color-foreground)", lineHeight: 1.3, marginBottom: "0.5rem" }}>{feat.title}</h3>
<p style={{ fontSize: "0.9375rem", color: "var(--color-foreground-muted)", lineHeight: 1.6, marginBottom: "1.5rem" }}>{feat.description}</p>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<button
onClick={() => setPlaying(!playing)}
aria-label={playing ? "Pause" : "Lecture"}
style={{ width: 48, height: 48, borderRadius: "var(--radius-full)", background: "var(--color-accent)", border: "none", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", flexShrink: 0 }}
>
{playing ? <Pause style={{ width: 20, height: 20, color: "var(--color-background)" }} /> : <Play style={{ width: 20, height: 20, color: "var(--color-background)", marginLeft: 2 }} />}
</button>
<WaveformBars playing={playing} />
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-foreground-muted)", flexShrink: 0 }}>{feat.duration}</span>
</div>
</motion.div>
{/* Episode list */}
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
{episodes.map((ep, i) => (
<motion.button
key={i}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.12 + i * 0.05, ease: EASE }}
style={{ display: "flex", alignItems: "center", gap: "1rem", padding: "1rem 1.25rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-background)", cursor: "pointer", textAlign: "left", width: "100%" }}
>
<div style={{ width: 36, height: 36, borderRadius: "var(--radius-full)", background: "var(--color-background-alt)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
<Play style={{ width: 14, height: 14, color: "var(--color-foreground-muted)", marginLeft: 1 }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h4 style={{ fontFamily: "var(--font-sans)", fontSize: "0.9375rem", fontWeight: 600, color: "var(--color-foreground)", lineHeight: 1.35 }}>{ep.title}</h4>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", fontSize: "0.75rem", color: "var(--color-foreground-muted)", flexShrink: 0 }}>
<span style={{ display: "flex", alignItems: "center", gap: "0.2rem" }}><Clock style={{ width: 11, height: 11 }} /> {ep.duration}</span>
<span>{ep.date}</span>
</div>
</motion.button>
))}
</div>
</div>
</section>
);
}