fix(andreknie.de): keep testimonial carousel centered
This commit is contained in:
@@ -35,7 +35,8 @@
|
|||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
padding-bottom: 32px; /* Space for scrollbar and shadow */
|
padding: 0 calc((100% - min(100%, var(--container-max))) / 2) 32px;
|
||||||
|
scroll-padding-inline: calc((100% - 88%) / 2);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
.testimonial-card {
|
.testimonial-card {
|
||||||
/* On mobile, show ~85% width so the next card peeks in */
|
/* On mobile, show ~85% width so the next card peeks in */
|
||||||
flex: 0 0 calc(88% - 24px);
|
flex: 0 0 calc(88% - 24px);
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: center;
|
||||||
|
|
||||||
/* Equal heights trick: cards stretch to the tallest sibling */
|
/* Equal heights trick: cards stretch to the tallest sibling */
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -70,6 +71,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
.testimonial-scroll-container {
|
||||||
|
scroll-padding-inline: calc((100% - 60%) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
.testimonial-card {
|
.testimonial-card {
|
||||||
/* On tablet, show ~60% width */
|
/* On tablet, show ~60% width */
|
||||||
flex: 0 0 calc(60% - 24px);
|
flex: 0 0 calc(60% - 24px);
|
||||||
@@ -77,6 +82,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
|
.testimonial-scroll-container {
|
||||||
|
scroll-padding-inline: calc((100% - 45%) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
.testimonial-card {
|
.testimonial-card {
|
||||||
/* On desktop, show ~45% width */
|
/* On desktop, show ~45% width */
|
||||||
flex: 0 0 calc(45% - 24px);
|
flex: 0 0 calc(45% - 24px);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||||
import './TestimonialSlider.css'
|
import './TestimonialSlider.css'
|
||||||
|
|
||||||
|
const LOOP_SETS = 9
|
||||||
|
const MIDDLE_SET = Math.floor(LOOP_SETS / 2)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manual testimonial slider (carousel).
|
* Manual testimonial slider (carousel).
|
||||||
* Equal heights, horizontal scroll, no autoplay.
|
* Equal heights, horizontal scroll, no autoplay.
|
||||||
@@ -11,49 +14,80 @@ export default function TestimonialSlider({ testimonials = [] }) {
|
|||||||
const dragStateRef = useRef({ isDragging: false, startX: 0, scrollLeft: 0 })
|
const dragStateRef = useRef({ isDragging: false, startX: 0, scrollLeft: 0 })
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const canLoop = testimonials.length > 1
|
const canLoop = testimonials.length > 1
|
||||||
const visibleTestimonials = canLoop ? [...testimonials, ...testimonials, ...testimonials] : testimonials
|
const visibleTestimonials = canLoop
|
||||||
|
? Array.from({ length: LOOP_SETS }, () => testimonials).flat()
|
||||||
|
: testimonials
|
||||||
|
|
||||||
const getCardStep = () => {
|
const getCenteredScrollLeft = (cardIndex) => {
|
||||||
const scroller = scrollerRef.current
|
const scroller = scrollerRef.current
|
||||||
const firstCard = scroller?.querySelector('.testimonial-card')
|
const card = scroller?.querySelectorAll('.testimonial-card')[cardIndex]
|
||||||
if (!scroller || !firstCard) return 0
|
if (!scroller || !card) return 0
|
||||||
|
|
||||||
const gap = parseFloat(window.getComputedStyle(scroller).columnGap || '0')
|
return card.offsetLeft - (scroller.clientWidth - card.offsetWidth) / 2
|
||||||
return firstCard.offsetWidth + gap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const getClosestCardIndex = () => {
|
||||||
const scroller = scrollerRef.current
|
const scroller = scrollerRef.current
|
||||||
if (!canLoop || !scroller) return
|
const cards = Array.from(scroller?.querySelectorAll('.testimonial-card') || [])
|
||||||
|
if (!scroller || cards.length === 0) return 0
|
||||||
|
|
||||||
const cardStep = getCardStep()
|
const scrollerCenter = scroller.scrollLeft + scroller.clientWidth / 2
|
||||||
if (!cardStep) return
|
return cards.reduce((closestIndex, card, index) => {
|
||||||
|
const cardCenter = card.offsetLeft + card.offsetWidth / 2
|
||||||
|
const closestCard = cards[closestIndex]
|
||||||
|
const closestCenter = closestCard.offsetLeft + closestCard.offsetWidth / 2
|
||||||
|
|
||||||
scroller.scrollLeft = cardStep * testimonials.length
|
return Math.abs(cardCenter - scrollerCenter) < Math.abs(closestCenter - scrollerCenter)
|
||||||
|
? index
|
||||||
|
: closestIndex
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerCard = (cardIndex, behavior = 'auto') => {
|
||||||
|
const scroller = scrollerRef.current
|
||||||
|
if (!scroller) return
|
||||||
|
|
||||||
|
scroller.scrollTo({
|
||||||
|
left: getCenteredScrollLeft(cardIndex),
|
||||||
|
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])
|
}, [canLoop, testimonials.length])
|
||||||
|
|
||||||
const scrollByCard = (direction) => {
|
const scrollByCard = (direction) => {
|
||||||
const scroller = scrollerRef.current
|
const scroller = scrollerRef.current
|
||||||
const cardStep = getCardStep()
|
if (!scroller) return
|
||||||
if (!scroller || !cardStep) return
|
|
||||||
|
|
||||||
scroller.scrollBy({
|
const targetIndex = getClosestCardIndex() + direction
|
||||||
left: direction * cardStep,
|
centerCard(targetIndex, 'smooth')
|
||||||
behavior: 'smooth',
|
|
||||||
})
|
window.setTimeout(recenterIfNeeded, 450)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keepLoopCentered = () => {
|
const recenterIfNeeded = () => {
|
||||||
const scroller = scrollerRef.current
|
const scroller = scrollerRef.current
|
||||||
const cardStep = getCardStep()
|
if (!canLoop || !scroller) return
|
||||||
if (!canLoop || !scroller || !cardStep) return
|
|
||||||
|
|
||||||
const segmentWidth = cardStep * testimonials.length
|
const currentIndex = getClosestCardIndex()
|
||||||
if (scroller.scrollLeft < segmentWidth * 0.5) {
|
const lowBoundary = testimonials.length
|
||||||
scroller.scrollLeft += segmentWidth
|
const highBoundary = testimonials.length * (LOOP_SETS - 1)
|
||||||
} else if (scroller.scrollLeft > segmentWidth * 1.5) {
|
if (currentIndex >= lowBoundary && currentIndex < highBoundary) return
|
||||||
scroller.scrollLeft -= segmentWidth
|
|
||||||
}
|
const normalizedIndex = ((currentIndex % testimonials.length) + testimonials.length) % testimonials.length
|
||||||
|
centerCard(testimonials.length * MIDDLE_SET + normalizedIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerDown = (event) => {
|
const handlePointerDown = (event) => {
|
||||||
@@ -82,7 +116,7 @@ export default function TestimonialSlider({ testimonials = [] }) {
|
|||||||
const scroller = scrollerRef.current
|
const scroller = scrollerRef.current
|
||||||
dragStateRef.current.isDragging = false
|
dragStateRef.current.isDragging = false
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
keepLoopCentered()
|
recenterIfNeeded()
|
||||||
|
|
||||||
if (scroller?.hasPointerCapture?.(event.pointerId)) {
|
if (scroller?.hasPointerCapture?.(event.pointerId)) {
|
||||||
scroller.releasePointerCapture(event.pointerId)
|
scroller.releasePointerCapture(event.pointerId)
|
||||||
@@ -108,7 +142,6 @@ export default function TestimonialSlider({ testimonials = [] }) {
|
|||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={stopDragging}
|
onPointerUp={stopDragging}
|
||||||
onPointerCancel={stopDragging}
|
onPointerCancel={stopDragging}
|
||||||
onScroll={keepLoopCentered}
|
|
||||||
onPointerLeave={(event) => {
|
onPointerLeave={(event) => {
|
||||||
if (dragStateRef.current.isDragging) stopDragging(event)
|
if (dragStateRef.current.isDragging) stopDragging(event)
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user