108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { Children, useRef, useState } from "react";
|
|
import { Swiper, SwiperSlide } from "swiper/react";
|
|
import { FreeMode } from "swiper/modules";
|
|
import type { Swiper as SwiperClass } from "swiper";
|
|
import "swiper/css";
|
|
import "swiper/css/free-mode";
|
|
|
|
type CarouselProps = {
|
|
children: React.ReactNode;
|
|
ariaLabel: string;
|
|
slideClassName?: string;
|
|
};
|
|
|
|
function ChevronLeftIcon() {
|
|
return (
|
|
<svg viewBox="0 0 24 24" fill="none" className="h-5 w-5" aria-hidden>
|
|
<path
|
|
d="M14.5 6.5 9 12l5.5 5.5"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function ChevronRightIcon() {
|
|
return (
|
|
<svg viewBox="0 0 24 24" fill="none" className="h-5 w-5" aria-hidden>
|
|
<path
|
|
d="M9.5 6.5 15 12l-5.5 5.5"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function Carousel({ children, ariaLabel, slideClassName }: CarouselProps) {
|
|
const swiperRef = useRef<SwiperClass | null>(null);
|
|
const [isLocked, setIsLocked] = useState(false);
|
|
|
|
const btnClass =
|
|
"absolute top-1/2 z-10 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/40 bg-white/40 text-red shadow-sm backdrop-blur-md transition-colors hover:border-red/40 hover:bg-white/60 hover:text-red-dark disabled:pointer-events-none disabled:opacity-0";
|
|
|
|
return (
|
|
<div className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => swiperRef.current?.slidePrev()}
|
|
aria-label="قبلی"
|
|
disabled={isLocked}
|
|
className={`${btnClass} right-1`}
|
|
>
|
|
<ChevronRightIcon />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => swiperRef.current?.slideNext()}
|
|
aria-label="بعدی"
|
|
disabled={isLocked}
|
|
className={`${btnClass} left-1`}
|
|
>
|
|
<ChevronLeftIcon />
|
|
</button>
|
|
<Swiper
|
|
modules={[FreeMode]}
|
|
onSwiper={(swiper) => {
|
|
swiperRef.current = swiper;
|
|
setIsLocked(swiper.isLocked);
|
|
}}
|
|
onLock={(swiper) => {
|
|
swiper.slideTo(0, 0);
|
|
setIsLocked(true);
|
|
}}
|
|
onUnlock={() => setIsLocked(false)}
|
|
dir="rtl"
|
|
slidesPerView="auto"
|
|
spaceBetween={16}
|
|
watchOverflow
|
|
freeMode={{ enabled: true, momentum: true, sticky: true }}
|
|
grabCursor
|
|
role="region"
|
|
aria-label={ariaLabel}
|
|
className="!px-1 !py-2"
|
|
>
|
|
{Children.map(children, (child) => (
|
|
// Swiper's own CSS pins slide height to 100%, which — with no definite
|
|
// wrapper height — resolves per-slide instead of stretching to match
|
|
// the tallest sibling. height:auto (inline, so it wins) lets normal
|
|
// flex stretch equalize card heights across a row.
|
|
<SwiperSlide
|
|
className={slideClassName}
|
|
style={slideClassName ? { height: "auto" } : { width: "auto", height: "auto" }}
|
|
>
|
|
{child}
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
</div>
|
|
);
|
|
}
|