import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Icon from '../../shared/components/icon'; export interface SliderProps { path?: string; images: string[]; mode?: 'auto' | 'manual'; intervalMs?: number; className?: string; } const DRAG_FRICTION = 0.9; const buildSrc = (path: string | undefined, img: string) => { if (/^(https?:)?\/\//i.test(img) || img.startsWith('data:') || img.startsWith('blob:')) { return img; } if (!path) return img; const p = path.endsWith('/') ? path.slice(0, -1) : path; const i = img.startsWith('/') ? img.slice(1) : img; return `${p}/${i}`; }; const Slider: React.FC = ({ path, images, mode = 'manual', intervalMs = 3500, className = '' }) => { const count = images.length; if (count === 0) return null; const renderedSlides = useMemo(() => { if (count <= 1) return images; return [images[count - 1], ...images, images[0]]; }, [images, count]); const [trackIndex, setTrackIndex] = useState(count > 1 ? 1 : 0); const [isAnimating, setIsAnimating] = useState(true); const [dragX, setDragX] = useState(0); const [isDragging, setIsDragging] = useState(false); const timerRef = useRef(null); const pointerStartX = useRef(0); const pointerIdRef = useRef(null); const viewportRef = useRef(null); const [viewportWidth, setViewportWidth] = useState(0); const transitionLockRef = useRef(false); useEffect(() => { const el = viewportRef.current; if (!el) return; const update = () => setViewportWidth(el.clientWidth); update(); const ro = new ResizeObserver(() => update()); ro.observe(el); return () => ro.disconnect(); }, []); const sliderClass = useMemo(() => ['slider', mode === 'auto' ? 'slider--auto' : 'slider--manual', className].filter(Boolean).join(' '), [mode, className]); const goNext = useCallback(() => { if (count <= 1) return; if (transitionLockRef.current) return; transitionLockRef.current = true; setIsAnimating(true); setTrackIndex((i) => i + 1); }, [count]); const goPrev = useCallback(() => { if (count <= 1) return; if (transitionLockRef.current) return; transitionLockRef.current = true; setIsAnimating(true); setTrackIndex((i) => i - 1); }, [count]); useEffect(() => { if (mode !== 'auto' || count <= 1) return; if (!isDragging) { timerRef.current = window.setInterval(goNext, intervalMs); } return () => { if (timerRef.current) { window.clearInterval(timerRef.current); timerRef.current = null; } }; }, [mode, intervalMs, count, isDragging, goNext]); const onTransitionEnd = useCallback(() => { if (count <= 1) return; transitionLockRef.current = false; if (trackIndex === renderedSlides.length - 1) { setIsAnimating(false); setTrackIndex(1); } else if (trackIndex === 0) { setIsAnimating(false); setTrackIndex(renderedSlides.length - 2); } }, [count, trackIndex, renderedSlides.length]); useEffect(() => { if (isAnimating) return; const id = window.setTimeout(() => setIsAnimating(true), 0); return () => window.clearTimeout(id); }, [isAnimating]); const activeDot = useMemo(() => { if (count <= 1) return 0; let real = trackIndex - 1; if (trackIndex === 0) real = count - 1; if (trackIndex === renderedSlides.length - 1) real = 0; return real; }, [trackIndex, count, renderedSlides.length]); const goToRealIndex = useCallback( (realIndex: number) => { if (count <= 1) return; if (transitionLockRef.current) return; transitionLockRef.current = true; setIsAnimating(true); setTrackIndex(realIndex + 1); }, [count] ); const onPointerDown = (e: React.PointerEvent) => { if (count <= 1) return; pointerIdRef.current = e.pointerId; pointerStartX.current = e.clientX; setIsDragging(true); setDragX(0); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); }; const onPointerMove = (e: React.PointerEvent) => { if (!isDragging) return; if (pointerIdRef.current !== e.pointerId) return; const delta = (e.clientX - pointerStartX.current) * DRAG_FRICTION; setDragX(delta); }; const endDrag = (e: React.PointerEvent) => { if (!isDragging) return; if (pointerIdRef.current !== e.pointerId) return; try { (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); } catch {} const delta = dragX; setIsDragging(false); setDragX(0); pointerIdRef.current = null; const threshold = viewportWidth > 0 ? viewportWidth * 0.12 : 60; if (Math.abs(delta) >= threshold) { if (delta < 0) goNext(); else goPrev(); } }; const x = -(trackIndex * viewportWidth) + (isDragging ? dragX : 0); const trackStyle: React.CSSProperties = { transform: `translate3d(${x}px, 0, 0)`, transition: isDragging || !isAnimating ? 'none' : undefined }; return (
{renderedSlides.map((img, i) => { const realIndex = count <= 1 ? 0 : i === 0 ? count - 1 : i === renderedSlides.length - 1 ? 0 : i - 1; return (
{`Slide
); })}
{mode === 'manual' && count > 1 && (
{images.map((_, i) => (
)}
); }; export default Slider;