Files
graff.event/src/components/ui/SliderWithScaling.tsx
T

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>
);
}