@@ -0,0 +1,218 @@
import { PropsWithChildren , useEffect , useState } from "react" ;
import { Container } from "~/components/container" ;
import { ArrowLeft , ArrowRight } from "lucide-react" ;
import { range } from "~/utils/range" ;
const IMAGES = [
{
id : 1 ,
url : "https://images.pexels.com/photos/1366919/pexels-photo-1366919.jpeg" ,
} ,
{
id : 2 ,
url : "https://images.pexels.com/photos/2662116/pexels-photo-2662116.jpeg" ,
} ,
{
id : 3 ,
url : "https://images.pexels.com/photos/1379636/pexels-photo-1379636.jpeg" ,
} ,
{
id : 4 ,
url : "https://images.pexels.com/photos/1470405/pexels-photo-1470405.jpeg" ,
} ,
{
id : 5 ,
url : "https://images.pexels.com/photos/3225517/pexels-photo-3225517.jpeg" ,
} ,
{
id : 6 ,
url : "https://images.pexels.com/photos/1133957/pexels-photo-1133957.jpeg" ,
} ,
{
id : 7 ,
url : "https://images.pexels.com/photos/1486974/pexels-photo-1486974.jpeg" ,
} ,
{
id : 8 ,
url : "https://images.pexels.com/photos/2662116/pexels-photo-2662116.jpeg" ,
} ,
{
id : 9 ,
url : "https://images.pexels.com/photos/1366630/pexels-photo-1366630.jpeg" ,
} ,
{
id : 10 ,
url : "https://images.pexels.com/photos/2486168/pexels-photo-2486168.jpeg" ,
} ,
] ;
const MAX_ITEMS = 4 ;
export default function Carousel ( ) {
const [ images , setImages ] = useState ( IMAGES ) ;
const [ activeItemWidth , setActiveItemWidth ] = useState < number > ( 60 ) ;
const [ currentIndex , setCurrentIndex ] = useState ( MAX_ITEMS ) ;
const scaleStep = 1 / MAX_ITEMS ;
const offsetStep = range ( - 3 , MAX_ITEMS * 2 - 1 ) . reduce (
( acc : Record < number , number > , currentValue ) => {
acc [ currentValue ] =
Math . sign ( currentValue ) * MAX_ITEMS - Math . abs ( currentValue ) ;
return acc ;
} ,
{ }
) ;
function isBetween ( value : number , start : number , stop : number ) {
return start <= value && value >= stop ;
}
function isValidInterval ( position : number ) {
return isBetween ( position , - 3 , - 1 ) || isBetween ( position , 1 , 3 ) ;
}
function computedOffset ( position : number ) {
if ( position !== 0 && position !== - 1 && position !== 1 ) {
return (
( Math . abs ( position ) === MAX_ITEMS - 1 ? Math . abs ( position ) : 0 ) +
range ( 1 , Math . abs ( position ) + 1 ) . reduce ( ( acc , currentValue ) => {
if ( offsetStep [ currentValue ] ) {
return acc + offsetStep [ currentValue ] ;
}
return acc ;
} , 0 )
) ;
}
return 0 ;
}
function computedWidth ( position : number ) {
return MAX_ITEMS - Math . abs ( position ) + 1 ;
}
function computedScale ( position : number ) {
if ( position === 0 ) {
return 1 ;
} else if (
isValidInterval ( position ) ||
position === - 3 ||
position === - 2
) {
return 1 - scaleStep * Math . abs ( position ) + 0.1 ;
}
return 0 ;
}
function computedOpacity ( position : number ) {
if (
position === 0 ||
position === - 3 ||
position === - 2 ||
isBetween ( position , - 3 , - 1 )
) {
return 1 ;
}
return 0 ;
}
function computedZIndex ( position : number ) {
if ( position !== 0 ) {
return images . length - Math . abs ( position ) ;
}
return images . length + 1 ;
}
function onNext ( ) {
const imagesCopy = [ ...images ] ;
const firstItem = imagesCopy . shift ( ) ;
if ( firstItem ) {
imagesCopy . push ( { id : Date . now ( ) , url : firstItem . url } ) ;
}
setImages ( imagesCopy ) ;
}
function onPrev ( ) {
const imagesCopy = [ ...images ] ;
const lastItem = imagesCopy . pop ( ) ;
if ( lastItem ) {
imagesCopy . unshift ( { id : Date . now ( ) , url : lastItem . url } ) ;
}
setImages ( imagesCopy ) ;
}
return (
< Container >
< div className = "relative h-screen w-full " >
< Button onClick = { onPrev } position = "left" >
< ArrowLeft className = "size-6 stroke-gray-900" />
</ Button >
< Button onClick = { onNext } position = "right" >
< ArrowRight className = "size-6 stroke-gray-900" />
</ Button >
{ images . map ( ( { url, id } , index ) => {
const shift = index - currentIndex ;
const width =
shift === 0
? activeItemWidth + "rem"
: Math . abs ( shift ) === MAX_ITEMS
? 0
: computedWidth ( shift ) + "rem" ;
return (
< div
key = { id }
style = {
{
"--width" : width ,
"--z-index" : computedZIndex ( shift ) ,
"--shift" : shift ,
"--scale" : computedScale ( shift ) ,
"--opacity" : computedOpacity ( shift ) ,
"--skip" :
shift === 0
? "0rem"
: Math . sign ( shift ) *
( activeItemWidth / 2 +
computedWidth ( shift ) +
computedOffset ( shift ) ) +
"rem" ,
"--height" : "30rem" ,
width : "var(--width)" ,
zIndex : "var(--z-index)" ,
transform :
"translate(calc(-50% + var(--skip)), -50%) scale(var(--scale)" ,
opacity : "var(--opacity)" ,
} as any
}
className = "inline-block flex-none w-[--width] h-[--height] inset-1/2 aspect-video absolute transition-all ease-in-out rounded-3xl overflow-hidden duration-700"
>
< img
src = { url }
className = "w-full h-full object-cover"
// loading="lazy"
/>
</ div >
) ;
} ) }
</ div >
</ Container >
) ;
}
function Button ( {
children,
position,
onClick,
} : PropsWithChildren < { position : "left" | "right" ; onClick : ( ) => void } > ) {
return (
< button
data-position = { position }
onClick = { onClick }
className = "bg-white rounded-full p-1.5 shadow-gray-800 shadow-sm absolute data-[position=left]:left-2 data-[position=right]:right-2 bottom-1/2 translate-y-1/2 z-10"
>
{ children }
</ button >
) ;
}