Retour au catalogue
Testimonials Audio Visual
Temoignages avec visualisation de forme d'onde audio animee en CSS. Effet podcast immersif.
testimonialsmedium Both Responsive a11y
boldplayfulsaaseducationuniversalstacked
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Play, Pause, Mic } from "lucide-react";
interface Testimonial {
id: string;
content: string;
authorName: string;
authorRole: string;
duration: string;
}
interface TestimonialsAudioVisualProps {
title?: string;
subtitle?: string;
testimonials?: Testimonial[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
const BAR_COUNT = 32;
function WaveformBars({ playing }: { playing: boolean }) {
return (
<div className="flex items-center gap-[2px] h-8">
{Array.from({ length: BAR_COUNT }).map((_, i) => {
const baseHeight = 20 + Math.sin(i * 0.8) * 60 + Math.cos(i * 1.3) * 20;
return (
<motion.div
key={i}
className="w-[3px] rounded-full"
style={{ background: "var(--color-accent)" }}
animate={
playing
? { height: [`${baseHeight}%`, `${20 + Math.random() * 80}%`, `${baseHeight}%`] }
: { height: `${baseHeight}%` }
}
transition={
playing
? { duration: 0.4 + Math.random() * 0.3, repeat: Infinity, repeatType: "reverse", delay: i * 0.02 }
: { duration: 0.4 }
}
/>
);
})}
</div>
);
}
export default function TestimonialsAudioVisual({
title = "Ecoutez nos clients",
subtitle = "Temoignages",
testimonials = [],
}: TestimonialsAudioVisualProps) {
const [activeIdx, setActiveIdx] = useState<number | null>(null);
return (
<section
style={{
paddingTop: "var(--section-padding-y)",
paddingBottom: "var(--section-padding-y)",
background: "var(--color-background)",
}}
>
<div
className="mx-auto"
style={{
maxWidth: "var(--container-max-width)",
paddingLeft: "var(--container-padding-x)",
paddingRight: "var(--container-padding-x)",
}}
>
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, ease: EASE }}
className="text-center mb-14"
>
<div className="inline-flex items-center gap-2 mb-4">
<Mic className="w-4 h-4" style={{ color: "var(--color-accent)" }} />
<span className="text-xs font-semibold uppercase tracking-widest" style={{ color: "var(--color-accent)" }}>{subtitle}</span>
</div>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight" style={{ color: "var(--color-foreground)" }}>{title}</h2>
</motion.div>
<div className="flex flex-col gap-4 max-w-2xl mx-auto">
{testimonials.map((t, i) => {
const isPlaying = activeIdx === i;
return (
<motion.div
key={t.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.1, ease: EASE }}
className="rounded-xl p-5"
style={{
background: "var(--color-background-card)",
border: isPlaying ? "1px solid var(--color-accent-border)" : "1px solid var(--color-border)",
}}
>
<div className="flex items-center gap-4">
<button
onClick={() => setActiveIdx(isPlaying ? null : i)}
className="w-10 h-10 rounded-full flex items-center justify-center shrink-0 cursor-pointer"
style={{ background: "var(--color-accent)", color: "var(--color-foreground-on-dark)" }}
aria-label={isPlaying ? "Pause" : "Ecouter"}
>
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4 ml-0.5" />}
</button>
<div className="flex-1 overflow-hidden">
<WaveformBars playing={isPlaying} />
</div>
<span className="text-xs font-mono shrink-0" style={{ color: "var(--color-foreground-light)" }}>
{t.duration}
</span>
</div>
<AnimatePresence>
{isPlaying && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3, ease: EASE }}
className="overflow-hidden"
>
<p className="text-sm leading-relaxed mt-4 pt-4" style={{ color: "var(--color-foreground-muted)", borderTop: "1px solid var(--color-border)" }}>
“{t.content}”
</p>
</motion.div>
)}
</AnimatePresence>
<div className="flex items-center gap-2 mt-3">
<div className="w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-bold" style={{ background: "var(--color-accent-subtle)", color: "var(--color-accent)" }}>
{t.authorName.charAt(0)}
</div>
<span className="text-sm font-medium" style={{ color: "var(--color-foreground)" }}>{t.authorName}</span>
<span className="text-xs" style={{ color: "var(--color-foreground-light)" }}>{t.authorRole}</span>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
}