import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' import { ChevronLeft, ChevronRight } from 'lucide-react' import './TestimonialSlider.css' const LOOP_SETS = 9 const MIDDLE_SET = Math.floor(LOOP_SETS / 2) /** * Manual testimonial slider (carousel). * Equal heights, horizontal scroll, no autoplay. */ export default function TestimonialSlider({ testimonials = [] }) { const scrollerRef = useRef(null) const dragStateRef = useRef({ isDragging: false, startX: 0, scrollLeft: 0 }) const recenterTimerRef = useRef(null) const [isDragging, setIsDragging] = useState(false) const [activeIndex, setActiveIndex] = useState(0) const canLoop = testimonials.length > 1 const visibleTestimonials = canLoop ? Array.from({ length: LOOP_SETS }, () => testimonials).flat() : testimonials const getCenteredScrollLeft = (cardIndex) => { const scroller = scrollerRef.current const card = scroller?.querySelectorAll('.testimonial-card')[cardIndex] if (!scroller || !card) return 0 return card.offsetLeft - (scroller.clientWidth - card.offsetWidth) / 2 } const getClosestCardIndex = () => { const scroller = scrollerRef.current const cards = Array.from(scroller?.querySelectorAll('.testimonial-card') || []) if (!scroller || cards.length === 0) return 0 const scrollerCenter = scroller.scrollLeft + scroller.clientWidth / 2 return cards.reduce((closestIndex, card, index) => { const cardCenter = card.offsetLeft + card.offsetWidth / 2 const closestCard = cards[closestIndex] const closestCenter = closestCard.offsetLeft + closestCard.offsetWidth / 2 return Math.abs(cardCenter - scrollerCenter) < Math.abs(closestCenter - scrollerCenter) ? index : closestIndex }, 0) } const centerCard = (cardIndex, behavior = 'auto') => { const scroller = scrollerRef.current if (!scroller) return const normalizedIndex = ((cardIndex % testimonials.length) + testimonials.length) % testimonials.length setActiveIndex(normalizedIndex) scroller.scrollTo({ left: getCenteredScrollLeft(cardIndex), behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : behavior, }) } useLayoutEffect(() => { if (!canLoop) return undefined const frameId = window.requestAnimationFrame(() => { const scroller = scrollerRef.current const card = scroller?.querySelectorAll('.testimonial-card')[testimonials.length * MIDDLE_SET] if (!scroller || !card) return scroller.scrollLeft = card.offsetLeft - (scroller.clientWidth - card.offsetWidth) / 2 }) return () => window.cancelAnimationFrame(frameId) }, [canLoop, testimonials.length]) useEffect(() => () => window.clearTimeout(recenterTimerRef.current), []) const scrollByCard = (direction) => { const scroller = scrollerRef.current if (!scroller) return const targetIndex = getClosestCardIndex() + direction centerCard(targetIndex, 'smooth') window.clearTimeout(recenterTimerRef.current) recenterTimerRef.current = window.setTimeout(recenterIfNeeded, 450) } const recenterIfNeeded = () => { const scroller = scrollerRef.current if (!canLoop || !scroller) return const currentIndex = getClosestCardIndex() const normalizedIndex = ((currentIndex % testimonials.length) + testimonials.length) % testimonials.length setActiveIndex(normalizedIndex) const lowBoundary = testimonials.length const highBoundary = testimonials.length * (LOOP_SETS - 1) if (currentIndex >= lowBoundary && currentIndex < highBoundary) return centerCard(testimonials.length * MIDDLE_SET + normalizedIndex) } const handlePointerDown = (event) => { if (event.pointerType === 'touch') return const scroller = scrollerRef.current if (!scroller) return dragStateRef.current = { isDragging: true, startX: event.clientX, scrollLeft: scroller.scrollLeft, } setIsDragging(true) scroller.setPointerCapture(event.pointerId) } const handlePointerMove = (event) => { const scroller = scrollerRef.current const dragState = dragStateRef.current if (!scroller || !dragState.isDragging) return scroller.scrollLeft = dragState.scrollLeft - (event.clientX - dragState.startX) } const stopDragging = (event) => { const scroller = scrollerRef.current dragStateRef.current.isDragging = false setIsDragging(false) recenterIfNeeded() if (scroller?.hasPointerCapture?.(event.pointerId)) { scroller.releasePointerCapture(event.pointerId) } } const handleKeyDown = (event) => { if (event.key === 'ArrowLeft') { event.preventDefault() scrollByCard(-1) } else if (event.key === 'ArrowRight') { event.preventDefault() scrollByCard(1) } } if (testimonials.length === 0) return null return (

Testimonials

Testimonial {activeIndex + 1} von {testimonials.length}

1 ? 'Karussell' : ''}`} aria-roledescription="Karussell" tabIndex="0" onKeyDown={handleKeyDown} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={stopDragging} onPointerCancel={stopDragging} onPointerLeave={(event) => { if (dragStateRef.current.isDragging) stopDragging(event) }} > {visibleTestimonials.map((current, i) => (
"

{current.quote}

{current.photo && {current.name}}
{current.name} {current.role}
))}
) }