spb/phi-conseil
Public
1"use client";23import { useCallback, useEffect, useId, useState, useSyncExternalStore } from "react";4import { QuoteMark } from "@/components/ui/Icons";56type Item = { quote: string; author: string; role: string };78const INTERVAL_MS = 8000;9const RM_QUERY = "(prefers-reduced-motion: reduce)";10function subscribeReducedMotion(cb: () => void) {11 const mq = window.matchMedia(RM_QUERY);12 mq.addEventListener("change", cb);13 return () => mq.removeEventListener("change", cb);14}15function getReducedMotion() {16 return window.matchMedia(RM_QUERY).matches;17}1819export function TestimonialSlider({ items }: { items: Item[] }) {20 const [index, setIndex] = useState(0);21 const [paused, setPaused] = useState(false);22 const reduced = useSyncExternalStore(subscribeReducedMotion, getReducedMotion, () => false);23 const regionId = useId();2425 const go = useCallback(26 (n: number) => setIndex((i) => (i + n + items.length) % items.length),27 [items.length],28 );2930 useEffect(() => {31 if (items.length < 2 || paused || reduced) return;32 const t = setInterval(() => go(1), INTERVAL_MS);33 return () => clearInterval(t);34 }, [go, items.length, paused, reduced]);3536 const current = items[index];37 const multiple = items.length > 1;3839 return (40 <section41 aria-roledescription="carrousel"42 aria-label="Témoignages de clients"43 className="bg-pale-200 relative rounded-[var(--radius-xl)] px-6 py-8 sm:px-10 md:px-14 md:py-10"44 onMouseEnter={() => setPaused(true)}45 onMouseLeave={() => setPaused(false)}46 onFocusCapture={() => setPaused(true)}47 onBlurCapture={(e) => {48 if (!e.currentTarget.contains(e.relatedTarget as Node)) setPaused(false);49 }}50 onKeyDown={(e) => {51 if (!multiple) return;52 if (e.key === "ArrowRight") go(1);53 if (e.key === "ArrowLeft") go(-1);54 }}55 >56 <div className="flex flex-col gap-6 md:flex-row md:items-start md:gap-8">57 <QuoteMark className="text-gold-400 h-9 w-9 shrink-0 md:h-11 md:w-11" />58 <div id={regionId} aria-live="polite" aria-atomic="true" className="flex-1">59 <blockquote>60 <p className="text-navy-900 font-serif text-xl leading-relaxed italic md:text-[1.45rem]">61 {current.quote}62 </p>63 <footer className="text-ink-700 mt-5 text-sm">64 <span aria-hidden="true">— </span>65 <cite className="text-ink-900 font-semibold not-italic">{current.author}</cite>66 {current.role ? <span>, {current.role}</span> : null}67 </footer>68 </blockquote>69 </div>7071 {multiple ? (72 <div73 className="flex items-center gap-3 self-end md:self-center"74 role="group"75 aria-label="Pagination des témoignages"76 >77 <button78 type="button"79 onClick={() => setPaused((p) => !p)}80 aria-pressed={paused}81 className="text-ink-700 rounded-md px-2 py-1 text-xs hover:bg-white/50"82 >83 {paused ? "Reprendre" : "Pause"}84 </button>85 <ul className="flex items-center gap-2.5">86 {items.map((_, i) => (87 <li key={i}>88 <button89 type="button"90 aria-label={`Témoignage ${i + 1} sur ${items.length}`}91 aria-current={i === index ? "true" : undefined}92 aria-controls={regionId}93 onClick={() => setIndex(i)}94 className={`block h-2.5 w-2.5 rounded-full transition-colors ${95 i === index ? "bg-navy-900" : "bg-pale-400 hover:bg-slate-500"96 }`}97 />98 </li>99 ))}100 </ul>101 </div>102 ) : null}103 </div>104 </section>105 );106}107