feat(andreknie.de): loop testimonial slider

This commit is contained in:
2026-07-27 19:31:48 +02:00
parent b8c6079e3a
commit cfbd77a1a4
3 changed files with 145 additions and 7 deletions
@@ -1,4 +1,5 @@
import React from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import './TestimonialSlider.css'
/**
@@ -6,13 +7,114 @@ import './TestimonialSlider.css'
* 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 [isDragging, setIsDragging] = useState(false)
const canLoop = testimonials.length > 1
const visibleTestimonials = canLoop ? [...testimonials, ...testimonials, ...testimonials] : testimonials
const getCardStep = () => {
const scroller = scrollerRef.current
const firstCard = scroller?.querySelector('.testimonial-card')
if (!scroller || !firstCard) return 0
const gap = parseFloat(window.getComputedStyle(scroller).columnGap || '0')
return firstCard.offsetWidth + gap
}
useEffect(() => {
const scroller = scrollerRef.current
if (!canLoop || !scroller) return
const cardStep = getCardStep()
if (!cardStep) return
scroller.scrollLeft = cardStep * testimonials.length
}, [canLoop, testimonials.length])
const scrollByCard = (direction) => {
const scroller = scrollerRef.current
const cardStep = getCardStep()
if (!scroller || !cardStep) return
scroller.scrollBy({
left: direction * cardStep,
behavior: 'smooth',
})
}
const keepLoopCentered = () => {
const scroller = scrollerRef.current
const cardStep = getCardStep()
if (!canLoop || !scroller || !cardStep) return
const segmentWidth = cardStep * testimonials.length
if (scroller.scrollLeft < segmentWidth * 0.5) {
scroller.scrollLeft += segmentWidth
} else if (scroller.scrollLeft > segmentWidth * 1.5) {
scroller.scrollLeft -= segmentWidth
}
}
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)
keepLoopCentered()
if (scroller?.hasPointerCapture?.(event.pointerId)) {
scroller.releasePointerCapture(event.pointerId)
}
}
if (testimonials.length === 0) return null
return (
<section className="testimonials-section app-container">
<div className="testimonial-scroll-container">
{testimonials.map((current, i) => (
<div className="testimonial-card glass-panel" key={i}>
<div className="testimonial-slider-controls" aria-label="Testimonials wechseln">
<button type="button" className="testimonial-nav-button" onClick={() => scrollByCard(-1)} aria-label="Vorheriges Testimonial">
<ChevronLeft size={22} aria-hidden="true" />
</button>
<button type="button" className="testimonial-nav-button" onClick={() => scrollByCard(1)} aria-label="Nächstes Testimonial">
<ChevronRight size={22} aria-hidden="true" />
</button>
</div>
<div
className={`testimonial-scroll-container${isDragging ? ' dragging' : ''}`}
ref={scrollerRef}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={stopDragging}
onPointerCancel={stopDragging}
onScroll={keepLoopCentered}
onPointerLeave={(event) => {
if (dragStateRef.current.isDragging) stopDragging(event)
}}
>
{visibleTestimonials.map((current, i) => (
<div className="testimonial-card glass-panel" key={`${current.name}-${i}`}>
<div className="testimonial-quote">
<span className="quote-mark">"</span>
<p>{current.quote}</p>