carousel category section
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import React from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
type GridWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
desktop: number; // تعداد ستونها در دسکتاپ
|
||||
mobile: number; // تعداد ستونها در موبایل
|
||||
gapDesktop?: number; // فاصله بین آیتمها در دسکتاپ. اگر مقدار > 12 باشد به عنوان px تفسیر میشود (مثلاً 24 => 24px)
|
||||
gapMobile?: number; // فاصله بین آیتمها در موبایل. اگر مقدار > 12 باشد به عنوان px تفسیر میشود
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const GRID_COLS_CLASS: Record<number, string> = {
|
||||
1: "grid-cols-1",
|
||||
2: "grid-cols-2",
|
||||
3: "grid-cols-3",
|
||||
4: "grid-cols-4",
|
||||
5: "grid-cols-5",
|
||||
6: "grid-cols-6",
|
||||
7: "grid-cols-7",
|
||||
8: "grid-cols-8",
|
||||
9: "grid-cols-9",
|
||||
10: "grid-cols-10",
|
||||
11: "grid-cols-11",
|
||||
12: "grid-cols-12",
|
||||
};
|
||||
|
||||
const MD_GRID_COLS_CLASS: Record<number, string> = {
|
||||
1: "md:grid-cols-1",
|
||||
2: "md:grid-cols-2",
|
||||
3: "md:grid-cols-3",
|
||||
4: "md:grid-cols-4",
|
||||
5: "md:grid-cols-5",
|
||||
6: "md:grid-cols-6",
|
||||
7: "md:grid-cols-7",
|
||||
8: "md:grid-cols-8",
|
||||
9: "md:grid-cols-9",
|
||||
10: "md:grid-cols-10",
|
||||
11: "md:grid-cols-11",
|
||||
12: "md:grid-cols-12",
|
||||
};
|
||||
|
||||
// نسخهی دسکتاپ با پیشوند lg: به صورت literal تا با purge مشکلی نداشته باشد
|
||||
const LG_GRID_COLS_CLASS: Record<number, string> = {
|
||||
1: "lg:grid-cols-1",
|
||||
2: "lg:grid-cols-2",
|
||||
3: "lg:grid-cols-3",
|
||||
4: "lg:grid-cols-4",
|
||||
5: "lg:grid-cols-5",
|
||||
6: "lg:grid-cols-6",
|
||||
7: "lg:grid-cols-7",
|
||||
8: "lg:grid-cols-8",
|
||||
9: "lg:grid-cols-9",
|
||||
10: "lg:grid-cols-10",
|
||||
11: "lg:grid-cols-11",
|
||||
12: "lg:grid-cols-12",
|
||||
};
|
||||
|
||||
// نگاشت Gap بر اساس scale رایج Tailwind: gap-N که هر N معادل 4px است (N * 4px)
|
||||
const GAP_CLASS: Record<number, string> = {
|
||||
0: "gap-0",
|
||||
1: "gap-1",
|
||||
2: "gap-2",
|
||||
3: "gap-3",
|
||||
4: "gap-4",
|
||||
5: "gap-5",
|
||||
6: "gap-6",
|
||||
7: "gap-7",
|
||||
8: "gap-8",
|
||||
9: "gap-9",
|
||||
10: "gap-10",
|
||||
11: "gap-11",
|
||||
12: "gap-12",
|
||||
13: "gap-13",
|
||||
14: "gap-14",
|
||||
15: "gap-15",
|
||||
16: "gap-16",
|
||||
17: "gap-17",
|
||||
18: "gap-18",
|
||||
19: "gap-19",
|
||||
20: "gap-20",
|
||||
21: "gap-21",
|
||||
22: "gap-22",
|
||||
23: "gap-23",
|
||||
24: "gap-24",
|
||||
};
|
||||
|
||||
const LG_GAP_CLASS: Record<number, string> = {
|
||||
0: "lg:gap-0",
|
||||
1: "lg:gap-1",
|
||||
2: "lg:gap-2",
|
||||
3: "lg:gap-3",
|
||||
4: "lg:gap-4",
|
||||
5: "lg:gap-5",
|
||||
6: "lg:gap-6",
|
||||
7: "lg:gap-7",
|
||||
8: "lg:gap-8",
|
||||
9: "lg:gap-9",
|
||||
10: "lg:gap-10",
|
||||
11: "lg:gap-11",
|
||||
12: "lg:gap-12",
|
||||
13: "lg:gap-13",
|
||||
14: "lg:gap-14",
|
||||
15: "lg:gap-15",
|
||||
16: "lg:gap-16",
|
||||
17: "lg:gap-17",
|
||||
18: "lg:gap-18",
|
||||
19: "lg:gap-19",
|
||||
20: "lg:gap-20",
|
||||
21: "lg:gap-21",
|
||||
22: "lg:gap-22",
|
||||
23: "lg:gap-23",
|
||||
24: "lg:gap-24",
|
||||
};
|
||||
|
||||
function clampToRange(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function getGridColsClass(count: number): string {
|
||||
const safe = clampToRange(count, 1, 12);
|
||||
return GRID_COLS_CLASS[safe];
|
||||
}
|
||||
|
||||
function getLgGridColsClass(count: number): string {
|
||||
const safe = clampToRange(count, 1, 12);
|
||||
return LG_GRID_COLS_CLASS[safe];
|
||||
}
|
||||
|
||||
function getMdGridColsClass(count: number): string {
|
||||
const safe = clampToRange(count, 1, 12);
|
||||
return MD_GRID_COLS_CLASS[safe];
|
||||
}
|
||||
|
||||
function getGapClass(value?: number): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
// اگر کاربر مقدار را به صورت px فرستاده باشد (بزرگتر از 12)، آن را به scale تبدیل میکنیم: N = round(px/4)
|
||||
const interpretedAsScale = value > 12 ? Math.round(value / 4) : value;
|
||||
const safe = clampToRange(interpretedAsScale, 0, 24);
|
||||
return GAP_CLASS[safe];
|
||||
}
|
||||
|
||||
function getLgGapClass(value?: number): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const interpretedAsScale = value > 12 ? Math.round(value / 4) : value;
|
||||
const safe = clampToRange(interpretedAsScale, 0, 24);
|
||||
return LG_GAP_CLASS[safe];
|
||||
}
|
||||
|
||||
export default function GridWrapper(props: GridWrapperProps) {
|
||||
const { children, desktop, mobile, className, gapDesktop = 4, gapMobile = 4 } = props;
|
||||
|
||||
const mobileCols = getGridColsClass(mobile);
|
||||
const desktopColsMd = getMdGridColsClass(desktop);
|
||||
const desktopColsLg = getLgGridColsClass(desktop);
|
||||
|
||||
const mobileGap = getGapClass(gapMobile);
|
||||
const desktopGap = getLgGapClass(gapDesktop);
|
||||
|
||||
return <div className={cn("grid", mobileCols, desktopColsMd, desktopColsLg, mobileGap, desktopGap, className)}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import CategoryImage from "@/assets/images/category.jpg";
|
||||
import { ArrowLeft } from "iconsax-reactjs";
|
||||
import Image from "next/image";
|
||||
import { type FC } from "react";
|
||||
|
||||
type CategoryCardProps = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
const CategoryCard: FC<CategoryCardProps> = ({ title }) => {
|
||||
return (
|
||||
<div className="w-full bg-[#D4E3F154]/33 rounded-[40px] p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-[#21588C] font-bold">{title}</div>
|
||||
<div className="bg-white/40 flex justify-center items-center">
|
||||
<ArrowLeft size={20} color="currentColor" className="text-primary rotate-45" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Image src={CategoryImage} alt={title} width={300} height={300} className="w-full h-auto rounded-3xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryCard;
|
||||
@@ -1,21 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, ArrowRight } from "iconsax-reactjs";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import CategoryCard from "./CategoryCard";
|
||||
|
||||
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 (
|
||||
<div className="mt-[120px] px-[120px]">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="font-bold text-2xl">دسته بندی های محصولات</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="size-8 bg-primary rounded-full flex justify-center items-center">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="دستهبندیهای قبلی"
|
||||
disabled={!canScrollPrev}
|
||||
onClick={() => emblaApi?.scrollPrev()}
|
||||
className="size-8 bg-primary rounded-full flex justify-center items-center disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
<ArrowRight size={20} color="white" />
|
||||
</div>
|
||||
<div className="size-8 bg-primary rounded-full flex justify-center items-center">
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="دستهبندیهای بعدی"
|
||||
disabled={!canScrollNext}
|
||||
onClick={() => emblaApi?.scrollNext()}
|
||||
className="size-8 bg-primary rounded-full flex justify-center items-center disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
<ArrowLeft size={20} color="white" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12"></div>
|
||||
<div className="mt-12 overflow-hidden" ref={emblaRef}>
|
||||
<div className="flex touch-pan-y gap-10">
|
||||
{categories.map((title) => (
|
||||
<div
|
||||
key={title}
|
||||
className="min-w-0 shrink-0 grow-0 basis-[calc((100%-10rem)/5)]"
|
||||
>
|
||||
<CategoryCard title={title} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user