feat(andreknie.de): loop testimonial slider
This commit is contained in:
@@ -2,6 +2,33 @@
|
||||
padding: var(--section-padding) 20px;
|
||||
}
|
||||
|
||||
.testimonial-slider-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.testimonial-nav-button {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 50%;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-primary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.testimonial-nav-button:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.testimonial-scroll-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
@@ -9,11 +36,20 @@
|
||||
scroll-behavior: smooth;
|
||||
gap: 24px;
|
||||
padding-bottom: 32px; /* Space for scrollbar and shadow */
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
|
||||
/* Hide scrollbar for cleaner look, or keep it subtle */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
.testimonial-scroll-container.dragging {
|
||||
cursor: grabbing;
|
||||
scroll-snap-type: none;
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
.testimonial-scroll-container::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user