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>
  );
}

Avis

Blog Audio Player — React Blog Section — Incubator