diff --git a/app/components/GridWrapper.tsx b/app/components/GridWrapper.tsx new file mode 100644 index 0000000..67618f3 --- /dev/null +++ b/app/components/GridWrapper.tsx @@ -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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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
{children}
; +} diff --git a/app/home/components/CategoryCard.tsx b/app/home/components/CategoryCard.tsx new file mode 100644 index 0000000..337b836 --- /dev/null +++ b/app/home/components/CategoryCard.tsx @@ -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 = ({ title }) => { + return ( +
+
+
{title}
+
+ +
+
+
+ {title} +
+
+ ); +}; + +export default CategoryCard; diff --git a/app/home/components/CategorySection.tsx b/app/home/components/CategorySection.tsx index e9870c5..da89d30 100644 --- a/app/home/components/CategorySection.tsx +++ b/app/home/components/CategorySection.tsx @@ -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 (
دسته بندی های محصولات
-
+
-
+ +
+
-
+
+
+ {categories.map((title) => ( +
+ +
+ ))} +
+
); }; diff --git a/assets/images/category.jpg b/assets/images/category.jpg new file mode 100644 index 0000000..9202784 Binary files /dev/null and b/assets/images/category.jpg differ diff --git a/package-lock.json b/package-lock.json index 91a3a1e..8c4a5c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "mehraein", "version": "0.1.0", "dependencies": { + "embla-carousel-react": "^8.6.0", "iconsax-reactjs": "^0.0.8", "next": "16.2.10", "react": "19.2.4", @@ -2231,6 +2232,20 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", @@ -2992,6 +3007,34 @@ "dev": true, "license": "ISC" }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -6611,10 +6654,6 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, - "node_modules/unrs-resolver/node_modules/@unrs/resolver-binding-openharmony-arm64": { - "dev": true, - "optional": true - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", diff --git a/package.json b/package.json index 0149c9d..e9e98c2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "embla-carousel-react": "^8.6.0", "iconsax-reactjs": "^0.0.8", "next": "16.2.10", "react": "19.2.4",