168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
import { useWindowWidth } from '../../hooks/useWindowWidth';
|
|
import { motion } from 'framer-motion';
|
|
import { ReactNode, useEffect, useReducer, useRef, useState } from 'react';
|
|
import { useSwipeable } from 'react-swipeable';
|
|
import { SliderControls } from './SliderControls';
|
|
|
|
export function SliderWithScaling<T extends { title: string }>({
|
|
slides,
|
|
SlideElement,
|
|
className = '',
|
|
childClassName = '',
|
|
alignItems = 'start',
|
|
slideSizes: [minWidth, minHeight, minWidthScaled, minHeightScaled],
|
|
controlsPosition,
|
|
}: {
|
|
slides: T[];
|
|
SlideElement: (_: T) => ReactNode;
|
|
className?: string;
|
|
childClassName?: string;
|
|
alignItems?: 'start' | 'center' | 'end';
|
|
slideSizes: [string, string, string, string];
|
|
controlsPosition: 'top' | 'bottom';
|
|
}) {
|
|
const width = useWindowWidth();
|
|
const baseoffset =
|
|
width >= 1024
|
|
? (-width / 1600) * 507 + 8
|
|
: (-width * +minWidth.slice(0, -2)) / 100 + 8;
|
|
|
|
const [slide, setSlide] = useState(0);
|
|
const [sliderOffset, setSliderOffset] = useState(baseoffset);
|
|
const [transiting, setTransiting] = useState(false);
|
|
const [currentSliding, setCurrentSliding] = useState<
|
|
'prev' | 'next' | null
|
|
>();
|
|
|
|
const sliderRef = useRef<HTMLDivElement>(null);
|
|
const itemRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [order, dispatch] = useReducer(
|
|
(state: typeof slides, action: 'prev' | 'next') => {
|
|
setTransiting(true);
|
|
switch (action) {
|
|
case 'prev':
|
|
setSlide(slide => (slide === 0 ? slides.length - 1 : slide - 1));
|
|
setSliderOffset(2 * baseoffset);
|
|
return [state[state.length - 3], ...state.slice(0, -1)];
|
|
case 'next':
|
|
setSlide(slide => (slide === slides.length - 1 ? 0 : slide + 1));
|
|
setSliderOffset(0);
|
|
return [...state.slice(1), state[2]];
|
|
default:
|
|
return state;
|
|
}
|
|
},
|
|
[slides[slides.length - 1], ...slides, slides[0]],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const sliderRefCurrent = sliderRef.current;
|
|
|
|
sliderRefCurrent?.addEventListener('transitionend', () => {
|
|
setTransiting(false);
|
|
setCurrentSliding(null);
|
|
});
|
|
|
|
return () => {
|
|
sliderRefCurrent?.removeEventListener('transitionend', () => {
|
|
setTransiting(false);
|
|
setCurrentSliding(null);
|
|
});
|
|
};
|
|
}, []);
|
|
|
|
function nextSlide() {
|
|
// if (!transiting) {
|
|
// }
|
|
setCurrentSliding('next');
|
|
dispatch('next');
|
|
}
|
|
|
|
function prevSlide() {
|
|
// if (!transiting) {
|
|
// }
|
|
setCurrentSliding('prev');
|
|
dispatch('prev');
|
|
}
|
|
|
|
useEffect(() => {
|
|
setSliderOffset(baseoffset);
|
|
}, [baseoffset, order, slide]);
|
|
|
|
const handlers = useSwipeable({
|
|
onSwipedLeft: nextSlide,
|
|
onSwipedRight: prevSlide,
|
|
trackMouse: true,
|
|
preventScrollOnSwipe: true,
|
|
touchEventOptions: { passive: false },
|
|
});
|
|
|
|
return (
|
|
<div className={'flex flex-col relative ' + className}>
|
|
<div className="overflow-hidden sm:-mx-6 -mx-4 h-full">
|
|
<div {...handlers} className="h-full">
|
|
<div
|
|
className={`flex items-${alignItems} gap-x-4 -mr-6 select-none`}
|
|
style={{
|
|
minHeight: minHeightScaled,
|
|
transform: `translateX(${sliderOffset}px)`,
|
|
transitionDuration: `${sliderOffset !== 0 && sliderOffset !== 2 * baseoffset ? 1 : 0}s`,
|
|
}}
|
|
ref={sliderRef}
|
|
>
|
|
{order.map((slide, index) => (
|
|
<motion.div
|
|
key={
|
|
slide.title +
|
|
(index < 1 ? '123' : index > slides.length ? '456' : '')
|
|
}
|
|
initial={
|
|
currentSliding === 'next' && index === 0
|
|
? { minWidth: minWidthScaled, minHeight: minHeightScaled }
|
|
: index === 3
|
|
? { minWidth, minHeight }
|
|
: {}
|
|
}
|
|
transition={{ duration: 1, type: 'just' }}
|
|
animate={
|
|
index === 1
|
|
? {
|
|
minWidth: minWidthScaled,
|
|
minHeight: minHeightScaled,
|
|
}
|
|
: {
|
|
minWidth,
|
|
minHeight,
|
|
transition: { duration: index === 3 ? 0.0001 : 1 },
|
|
}
|
|
}
|
|
className={'pointer-events-none ' + childClassName}
|
|
>
|
|
<SlideElement
|
|
{...slide}
|
|
ref={index === 1 && !transiting ? itemRef : null}
|
|
/>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<SliderControls
|
|
slide={slide}
|
|
onLeftClick={prevSlide}
|
|
onRightClick={nextSlide}
|
|
slidesCount={slides.length}
|
|
width={width >= 640 ? 132 : ((width - 32) / 328) * 196}
|
|
height={width >= 640 ? 66 : 58}
|
|
className={
|
|
'absolute ' +
|
|
(controlsPosition === 'top'
|
|
? 'top-[75px]'
|
|
: 'bottom-0 sm:self-end self-center')
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|