@@ -32,41 +32,111 @@ export default function BannerCarousel({
3232 category : CategoryKey ;
3333} ) {
3434 const banners = category === "beauty" ? beautyBanners : fashionBanners ;
35+ const displayBanners = [ banners [ banners . length - 1 ] , ...banners , banners [ 0 ] ] ;
3536
36- const [ current , setCurrent ] = useState ( 0 ) ;
37+ const [ current , setCurrent ] = useState ( 1 ) ;
38+ const [ isDragging , setIsDragging ] = useState ( false ) ;
39+ const [ isSilentJumping , setIsSilentJumping ] = useState ( false ) ;
40+ const [ startX , setStartX ] = useState ( 0 ) ;
41+ const [ dragOffset , setDragOffset ] = useState ( 0 ) ;
3742 const timerRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
3843
39- const start = useCallback ( ( ) => {
40- timerRef . current = setInterval ( ( ) => {
41- setCurrent ( ( prev ) => ( prev + 1 ) % banners . length ) ;
42- } , INTERVAL ) ;
43- } , [ banners . length ] ) ;
44-
4544 const stop = useCallback ( ( ) => {
4645 if ( timerRef . current ) {
4746 clearInterval ( timerRef . current ) ;
4847 timerRef . current = null ;
4948 }
5049 } , [ ] ) ;
5150
51+ const start = useCallback ( ( ) => {
52+ stop ( ) ;
53+ timerRef . current = setInterval ( ( ) => {
54+ setCurrent ( ( prev ) => prev + 1 ) ;
55+ } , INTERVAL ) ;
56+ } , [ stop ] ) ;
57+
5258 useEffect ( ( ) => {
5359 start ( ) ;
5460 return stop ;
5561 } , [ start , stop ] ) ;
5662
63+ const handleTransitionEnd = ( ) => {
64+ if ( current === 0 ) {
65+ setIsSilentJumping ( true ) ;
66+ setCurrent ( banners . length ) ;
67+ } else if ( current === banners . length + 1 ) {
68+ setIsSilentJumping ( true ) ;
69+ setCurrent ( 1 ) ;
70+ }
71+ } ;
72+
73+ useEffect ( ( ) => {
74+ if ( isSilentJumping ) {
75+ const timeout = setTimeout ( ( ) => {
76+ setIsSilentJumping ( false ) ;
77+ } , 50 ) ;
78+ return ( ) => clearTimeout ( timeout ) ;
79+ }
80+ } , [ isSilentJumping ] ) ;
81+
82+ const handleStart = ( clientX : number ) => {
83+ stop ( ) ;
84+ setIsDragging ( true ) ;
85+ setStartX ( clientX ) ;
86+ setDragOffset ( 0 ) ;
87+ } ;
88+
89+ const handleMove = ( clientX : number ) => {
90+ if ( ! isDragging ) return ;
91+ const offset = clientX - startX ;
92+ setDragOffset ( offset ) ;
93+ } ;
94+
95+ const handleEnd = ( ) => {
96+ if ( ! isDragging ) return ;
97+
98+ const threshold = 50 ;
99+ if ( dragOffset > threshold ) {
100+ setCurrent ( ( prev ) => prev - 1 ) ;
101+ } else if ( dragOffset < - threshold ) {
102+ setCurrent ( ( prev ) => prev + 1 ) ;
103+ }
104+
105+ setIsDragging ( false ) ;
106+ setDragOffset ( 0 ) ;
107+ start ( ) ;
108+ } ;
109+
110+ const activeDotIndex = ( current - 1 + banners . length ) % banners . length ;
111+
57112 return (
58113 < div className = "-mx-5" >
59- < div className = "relative overflow-hidden" >
114+ < div
115+ className = "relative overflow-hidden cursor-grab active:cursor-grabbing touch-pan-y"
116+ onMouseDown = { ( e ) => handleStart ( e . clientX ) }
117+ onMouseMove = { ( e ) => handleMove ( e . clientX ) }
118+ onMouseUp = { handleEnd }
119+ onMouseLeave = { handleEnd }
120+ onTouchStart = { ( e ) => handleStart ( e . touches [ 0 ] . clientX ) }
121+ onTouchMove = { ( e ) => handleMove ( e . touches [ 0 ] . clientX ) }
122+ onTouchEnd = { handleEnd }
123+ >
60124 < div
61- className = "flex transition-transform duration-500 ease-in-out"
62- style = { { transform : `translateX(-${ current * 100 } %)` } }
125+ className = { `flex ${ isDragging || isSilentJumping ? "" : "transition-transform duration-500 ease-in-out" } ` }
126+ style = { {
127+ transform : `translateX(calc(-${ current * 100 } % + ${ dragOffset } px))` ,
128+ } }
129+ onTransitionEnd = { handleTransitionEnd }
63130 >
64- { banners . map ( ( banner , i ) => (
65- < div key = { `${ category } -${ i } ` } className = "w-full shrink-0" >
131+ { displayBanners . map ( ( banner , i ) => (
132+ < div
133+ key = { `${ category } -${ i } ` }
134+ className = "w-full shrink-0 select-none"
135+ >
66136 < img
67137 src = { banner . src }
68138 alt = { banner . alt }
69- className = "h-62.5 w-full object-cover"
139+ className = "h-62.5 w-full object-cover pointer-events-none "
70140 />
71141 </ div >
72142 ) ) }
@@ -77,14 +147,14 @@ export default function BannerCarousel({
77147 < button
78148 key = { i }
79149 type = "button"
80- onClick = { ( ) => {
150+ onClick = { ( e ) => {
151+ e . stopPropagation ( ) ;
81152 stop ( ) ;
82- setCurrent ( i ) ;
153+ setCurrent ( i + 1 ) ;
83154 start ( ) ;
84155 } }
85- className = { `h-1.5 w-1.5 rounded-full transition-colors ${
86- i === current ? "bg-white" : "bg-white/50"
87- } `}
156+ className = { `h-1.5 w-1.5 rounded-full transition-colors ${ i === activeDotIndex ? "bg-white" : "bg-white/50"
157+ } `}
88158 />
89159 ) ) }
90160 </ div >
0 commit comments