diff --git a/package-lock.json b/package-lock.json
index da0fde6..8a8e6f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,12 @@
"version": "0.1.0",
"dependencies": {
"@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.84.1",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "embla-carousel-react": "^8.6.0",
"iconsax-react": "^0.0.8",
"lucide-react": "^0.536.0",
"next": "15.4.5",
@@ -2721,6 +2723,34 @@
"node": ">= 0.4"
}
},
+ "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",
diff --git a/package.json b/package.json
index 085f4e3..6cabd37 100644
--- a/package.json
+++ b/package.json
@@ -10,10 +10,12 @@
},
"dependencies": {
"@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.84.1",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "embla-carousel-react": "^8.6.0",
"iconsax-react": "^0.0.8",
"lucide-react": "^0.536.0",
"next": "15.4.5",
diff --git a/src/app/home/components/Banners3.tsx b/src/app/home/components/Banners3.tsx
new file mode 100644
index 0000000..f0bfec8
--- /dev/null
+++ b/src/app/home/components/Banners3.tsx
@@ -0,0 +1,18 @@
+import { FC } from 'react'
+
+const Banners3: FC = () => {
+ return (
+
+ )
+}
+
+export default Banners3
\ No newline at end of file
diff --git a/src/app/home/components/BlogCard.tsx b/src/app/home/components/BlogCard.tsx
new file mode 100644
index 0000000..12c4f1d
--- /dev/null
+++ b/src/app/home/components/BlogCard.tsx
@@ -0,0 +1,14 @@
+import { FC } from 'react'
+
+const BlogCard: FC = () => {
+ return (
+
+
+ بررسی
+ گوشی گیمینگ ایسوس
+
+
+ )
+}
+
+export default BlogCard
\ No newline at end of file
diff --git a/src/app/home/components/Blogs.tsx b/src/app/home/components/Blogs.tsx
new file mode 100644
index 0000000..e06ca45
--- /dev/null
+++ b/src/app/home/components/Blogs.tsx
@@ -0,0 +1,17 @@
+import { FC } from 'react'
+import BlogCard from './BlogCard'
+import GridWrapper from '@/components/GridWrapper'
+
+const Blogs: FC = () => {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export default Blogs
\ No newline at end of file
diff --git a/src/app/home/components/Categories.tsx b/src/app/home/components/Categories.tsx
new file mode 100644
index 0000000..11b9c88
--- /dev/null
+++ b/src/app/home/components/Categories.tsx
@@ -0,0 +1,28 @@
+'use client'
+import React from 'react'
+import CategoryItem from './CategoryItem'
+import { useGetCategories } from '@/share/hooks/useShareData'
+import { Category } from '@/share/types/SharedTypes'
+
+const Categories = () => {
+
+ const { data } = useGetCategories()
+ const parentCategories = data?.results?.data?.filter(cat => !cat.parent) || []
+
+ return (
+
+ {
+ parentCategories?.map((item: Category) => {
+ return (
+
+
+ )
+ })
+ }
+
+
+
+ )
+}
+
+export default Categories
\ No newline at end of file
diff --git a/src/app/home/components/CategoryItem.tsx b/src/app/home/components/CategoryItem.tsx
new file mode 100644
index 0000000..9e6ae9b
--- /dev/null
+++ b/src/app/home/components/CategoryItem.tsx
@@ -0,0 +1,22 @@
+import Image from 'next/image';
+import React, { FC } from 'react'
+
+type Props = {
+ image: string;
+ title: string;
+}
+
+const CategoryItem: FC = (props) => {
+ return (
+
+
+
+
+
+ {props.title}
+
+
+ )
+}
+
+export default CategoryItem
\ No newline at end of file
diff --git a/src/app/home/components/NewestProducts.tsx b/src/app/home/components/NewestProducts.tsx
new file mode 100644
index 0000000..ca444ad
--- /dev/null
+++ b/src/app/home/components/NewestProducts.tsx
@@ -0,0 +1,43 @@
+import { FC } from 'react'
+import {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselNext,
+ CarouselPrevious,
+} from "@/components/ui/carousel"
+import ProductCard from '@/components/ProductCard'
+
+
+const NewestProducts: FC = () => {
+ return (
+
+
جدیدترین محصولات
+
+ برای مشاهده ی جدیدترین محصولات اینجا کلیک کنید
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+export default NewestProducts
\ No newline at end of file
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 0fc1dc8..3044078 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,6 +2,10 @@ import { NextPage } from 'next'
import withLayout from '@/hoc/withLayout'
import Carousel from './home/components/Carousel'
import HotOffer from './home/components/HotOffer'
+import Categories from './home/components/Categories'
+import Banners3 from './home/components/Banners3'
+import Blogs from './home/components/Blogs'
+import NewestProducts from './home/components/NewestProducts'
const Home: NextPage = () => {
return (
@@ -9,6 +13,20 @@ const Home: NextPage = () => {
+
+
+
)
diff --git a/src/components/GridWrapper.tsx b/src/components/GridWrapper.tsx
new file mode 100644
index 0000000..da18013
--- /dev/null
+++ b/src/components/GridWrapper.tsx
@@ -0,0 +1,122 @@
+import React from "react";
+import { cn } from "@/lib/utils";
+
+type GridWrapperProps = {
+ children: React.ReactNode;
+ desktop: number; // تعداد ستونها در دسکتاپ
+ mobile: number; // تعداد ستونها در موبایل
+ gapDesktop?: number; // فاصله بین آیتمها در دسکتاپ (بر اساس scale پیشفرض Tailwind)
+ gapMobile?: number; // فاصله بین آیتمها در موبایل
+ 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 mapping (scale رایج Tailwind)
+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;
+ const safe = clampToRange(value, 0, 12);
+ return GAP_CLASS[safe];
+}
+
+function getLgGapClass(value?: number): string | undefined {
+ if (value === undefined) return undefined;
+ const safe = clampToRange(value, 0, 12);
+ 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/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..0e05a77
--- /dev/null
+++ b/src/components/ui/carousel.tsx
@@ -0,0 +1,241 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+function Carousel({
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) return
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) return
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/src/share/components/Menu.tsx b/src/share/components/Menu.tsx
index 9e7b180..b201fbd 100644
--- a/src/share/components/Menu.tsx
+++ b/src/share/components/Menu.tsx
@@ -2,13 +2,13 @@
import { HambergerMenu, ArrowLeft2 } from 'iconsax-react'
import { FC, useState, useRef, useEffect } from 'react'
import Image from 'next/image'
-import { useCategories } from '../hooks/useShareData'
+import { useGetCategories } from '../hooks/useShareData'
import { Category } from '../types/SharedTypes'
const Menu: FC = () => {
const [isOpen, setIsOpen] = useState(false)
const [hoveredPath, setHoveredPath] = useState([])
- const { data, isLoading } = useCategories()
+ const { data, isLoading } = useGetCategories()
const menuRef = useRef(null)
useEffect(() => {
diff --git a/src/share/hooks/useShareData.ts b/src/share/hooks/useShareData.ts
index a28a450..c7fa513 100644
--- a/src/share/hooks/useShareData.ts
+++ b/src/share/hooks/useShareData.ts
@@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { getCategories } from "../service/ShareService";
-export const useCategories = () => {
+export const useGetCategories = () => {
return useQuery({
queryKey: ["categories"],
queryFn: getCategories,