From 7bb9240f212471b7354d083612f3140456292512 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 18 Jul 2026 11:25:53 +0330 Subject: [PATCH] carousel components --- app/components/Carousel.tsx | 183 ++++++++++++++++++++++++ app/home/components/CategorySection.tsx | 74 ++-------- 2 files changed, 193 insertions(+), 64 deletions(-) create mode 100644 app/components/Carousel.tsx diff --git a/app/components/Carousel.tsx b/app/components/Carousel.tsx new file mode 100644 index 0000000..7df3fc1 --- /dev/null +++ b/app/components/Carousel.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { cn } from "@/app/lib/cn"; +import useEmblaCarousel from "embla-carousel-react"; +import { ArrowLeft, ArrowRight } from "iconsax-reactjs"; +import { + createContext, + useContext, + useEffect, + useState, + type FC, + type HTMLAttributes, + type ReactNode, +} from "react"; + +type CarouselApi = { + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; + slidesPerView: number; + gap: number; +}; + +type ViewportRef = (node: HTMLElement | null) => void; + +const CarouselContext = createContext(null); +const CarouselViewportContext = createContext(null); + +function useCarousel() { + const context = useContext(CarouselContext); + if (!context) { + throw new Error("Carousel components must be used within "); + } + return context; +} + +type CarouselProps = { + children: ReactNode; + slidesPerView?: number; + slidesToScroll?: number; + gap?: number; + direction?: "rtl" | "ltr"; + className?: string; +}; + +const Carousel: FC = ({ + children, + slidesPerView = 5, + slidesToScroll, + gap = 40, + direction = "rtl", + className, +}) => { + const [emblaRef, emblaApi] = useEmblaCarousel({ + align: "start", + direction, + slidesToScroll: slidesToScroll ?? slidesPerView, + containScroll: "trimSnaps", + }); + + const [canScrollPrev, setCanScrollPrev] = useState(false); + const [canScrollNext, setCanScrollNext] = useState(false); + + useEffect(() => { + if (!emblaApi) return; + + const updateButtons = () => { + setCanScrollPrev(emblaApi.canScrollPrev()); + setCanScrollNext(emblaApi.canScrollNext()); + }; + + updateButtons(); + emblaApi.on("select", updateButtons); + emblaApi.on("reInit", updateButtons); + + return () => { + emblaApi.off("select", updateButtons); + emblaApi.off("reInit", updateButtons); + }; + }, [emblaApi]); + + return ( + emblaApi?.scrollPrev(), + scrollNext: () => emblaApi?.scrollNext(), + canScrollPrev, + canScrollNext, + slidesPerView, + gap, + }} + > + +
{children}
+
+
+ ); +}; + +type CarouselControlsProps = { + className?: string; + prevLabel?: string; + nextLabel?: string; +}; + +const CarouselControls: FC = ({ + className, + prevLabel = "قبلی", + nextLabel = "بعدی", +}) => { + const { scrollPrev, scrollNext, canScrollPrev, canScrollNext } = useCarousel(); + + return ( +
+ + +
+ ); +}; + +type CarouselTrackProps = { + children: ReactNode; + className?: string; +}; + +const CarouselTrack: FC = ({ children, className }) => { + const emblaRef = useContext(CarouselViewportContext); + const { gap } = useCarousel(); + + if (!emblaRef) { + throw new Error("CarouselTrack must be used within "); + } + + return ( +
+
+ {children} +
+
+ ); +}; + +type CarouselSlideProps = HTMLAttributes & { + children: ReactNode; +}; + +const CarouselSlide: FC = ({ children, className, style, ...rest }) => { + const { slidesPerView, gap } = useCarousel(); + const totalGap = (slidesPerView - 1) * gap; + + return ( +
+ {children} +
+ ); +}; + +export { Carousel, CarouselControls, CarouselSlide, CarouselTrack }; +export default Carousel; diff --git a/app/home/components/CategorySection.tsx b/app/home/components/CategorySection.tsx index da89d30..7f7a89d 100644 --- a/app/home/components/CategorySection.tsx +++ b/app/home/components/CategorySection.tsx @@ -1,8 +1,4 @@ -"use client"; - -import { ArrowLeft, ArrowRight } from "iconsax-reactjs"; -import useEmblaCarousel from "embla-carousel-react"; -import { useEffect, useState } from "react"; +import { Carousel, CarouselControls, CarouselSlide, CarouselTrack } from "@/app/components/Carousel"; import CategoryCard from "./CategoryCard"; const categories = [ @@ -19,72 +15,22 @@ const categories = [ ]; const CategorySection = () => { - const [emblaRef, emblaApi] = useEmblaCarousel({ - align: "start", - direction: "rtl", - slidesToScroll: 5, - containScroll: "trimSnaps", - }); - - const [canScrollPrev, setCanScrollPrev] = useState(false); - const [canScrollNext, setCanScrollNext] = useState(false); - - useEffect(() => { - if (!emblaApi) return; - - const updateButtons = () => { - setCanScrollPrev(emblaApi.canScrollPrev()); - setCanScrollNext(emblaApi.canScrollNext()); - }; - - updateButtons(); - emblaApi.on("select", updateButtons); - emblaApi.on("reInit", updateButtons); - - return () => { - emblaApi.off("select", updateButtons); - emblaApi.off("reInit", updateButtons); - }; - }, [emblaApi]); - return (
-
-
دسته بندی های محصولات
-
- - + +
+
دسته بندی های محصولات
+
-
-
-
+ {categories.map((title) => ( -
+ -
+ ))} -
-
+ +
); };