"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Image from "next/image"; type Banner = { id: string; title: string; imageUrl: string; linkUrl?: string | null }; export function HeroBanner() { const [banners, setBanners] = useState([]); const [activeIndex, setActiveIndex] = useState(0); const [isHovered, setIsHovered] = useState(false); const [progress, setProgress] = useState(0); // 0..1 const rotationMs = 5000; const rafIdRef = useRef(null); const startedAtRef = useRef(0); useEffect(() => { fetch("/api/banners").then((r) => r.json()).then((d) => setBanners(d.banners ?? [])); }, []); const numSlides = banners.length; const canAutoPlay = numSlides > 1 && !isHovered; const startTicker = useCallback(() => { if (!canAutoPlay) return; startedAtRef.current = performance.now(); const tick = () => { const now = performance.now(); const elapsed = now - startedAtRef.current; const nextProgress = Math.min(1, elapsed / rotationMs); setProgress(nextProgress); if (nextProgress >= 1) { setActiveIndex((i) => (i + 1) % Math.max(1, numSlides)); startedAtRef.current = performance.now(); setProgress(0); } rafIdRef.current = window.requestAnimationFrame(tick); }; rafIdRef.current = window.requestAnimationFrame(tick); }, [canAutoPlay, numSlides, rotationMs]); const stopTicker = useCallback(() => { if (rafIdRef.current) { window.cancelAnimationFrame(rafIdRef.current); rafIdRef.current = null; } }, []); useEffect(() => { stopTicker(); setProgress(0); startTicker(); return () => stopTicker(); }, [startTicker, stopTicker, activeIndex, isHovered, numSlides]); const goTo = useCallback((index: number) => { if (numSlides === 0) return; const next = (index + numSlides) % numSlides; setActiveIndex(next); setProgress(0); startedAtRef.current = performance.now(); }, [numSlides]); const goPrev = useCallback(() => goTo(activeIndex - 1), [activeIndex, goTo]); const goNext = useCallback(() => goTo(activeIndex + 1), [activeIndex, goTo]); const translatePercent = useMemo(() => (numSlides > 0 ? -(activeIndex * 100) : 0), [activeIndex, numSlides]); if (numSlides === 0) return null; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} aria-roledescription="carousel" >
{banners.map((banner) => (
{banner.linkUrl ? ( {banner.title} ) : ( {banner.title} )}

{banner.title}

))}
{/* Controls */} {numSlides > 1 && ( <> )}
{/* Progress bar {numSlides > 1 && (
)} */} {/* Pagination */} {numSlides > 1 && (
{banners.map((_, i) => (
)}
); }