From 1859d0b2c98bf61451ee7fd8b29290a4b978b239 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 10 Aug 2025 13:16:56 +0330 Subject: [PATCH] products --- package-lock.json | 218 +++++++++++++++++++ package.json | 1 + src/app/products/components/CategoryTree.tsx | 109 ++++++++++ src/app/products/components/Filters.tsx | 176 +++++++++++++++ src/app/products/components/Sorts.tsx | 43 ++++ src/app/products/page.tsx | 54 +++++ src/components/ProductCard.tsx | 4 +- src/components/ui/accordion.tsx | 66 ++++++ src/components/ui/breadcrumb.tsx | 109 ++++++++++ 9 files changed, 777 insertions(+), 3 deletions(-) create mode 100644 src/app/products/components/CategoryTree.tsx create mode 100644 src/app/products/components/Filters.tsx create mode 100644 src/app/products/components/Sorts.tsx create mode 100644 src/app/products/page.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/breadcrumb.tsx diff --git a/package-lock.json b/package-lock.json index 8a8e6f6..26370c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "shop", "version": "0.1.0", "dependencies": { + "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.84.1", @@ -979,6 +980,99 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", + "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -994,6 +1088,78 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-primitive": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", @@ -1058,6 +1224,58 @@ } } }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", diff --git a/package.json b/package.json index 6cabd37..998aab8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.84.1", diff --git a/src/app/products/components/CategoryTree.tsx b/src/app/products/components/CategoryTree.tsx new file mode 100644 index 0000000..a5dcbc8 --- /dev/null +++ b/src/app/products/components/CategoryTree.tsx @@ -0,0 +1,109 @@ +"use client" + +import { FC, useCallback, useMemo, useState } from 'react' +import { ArrowDown2 } from 'iconsax-react' +import { Category } from '@/share/types/SharedTypes' + +type Props = { + categories: Category[] + selectedId: string | null + onSelect: (id: string | null) => void +} + +const CategoryTree: FC = ({ categories, selectedId, onSelect }) => { + const roots = useMemo(() => categories || [], [categories]) + + return ( +
+ onSelect(null)} + /> + {roots.map((cat) => ( + + ))} +
+ ) +} + +type NodeProps = { + node: Category + selectedId: string | null + onSelect: (id: string | null) => void + depth: number +} + +const Node: FC = ({ node, selectedId, onSelect, depth }) => { + const [open, setOpen] = useState(false) + const hasChildren = (node.children?.length ?? 0) > 0 + + const toggle = useCallback(() => { + if (hasChildren) setOpen((p) => !p) + else onSelect(node._id) + }, [hasChildren, node._id, onSelect]) + + return ( +
+ + {hasChildren && open && ( +
+ {node.children.map((child) => ( + ) + )} +
+ )} +
+ ) +} + +type RowProps = { + label: string + isLeaf?: boolean + isSelected?: boolean + onClick: () => void + open?: boolean + depth?: number +} + +const Row: FC = ({ label, isLeaf, isSelected, onClick, open, depth = 0 }) => { + return ( + + ) +} + +export default CategoryTree + + diff --git a/src/app/products/components/Filters.tsx b/src/app/products/components/Filters.tsx new file mode 100644 index 0000000..b5c47b8 --- /dev/null +++ b/src/app/products/components/Filters.tsx @@ -0,0 +1,176 @@ + +"use client" + +import { FC, useMemo, useState, useEffect } from 'react' +import { Setting5 } from 'iconsax-react' +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' +import Input from '@/components/Input' +import { Button } from '@/components/ui/button' +import { useQuery } from '@tanstack/react-query' +import { getCategories } from '@/share/service/ShareService' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import CategoryTree from './CategoryTree' +import { Category } from '@/share/types/SharedTypes' + +const Filters: FC = () => { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + + const [selectedCategories, setSelectedCategories] = useState([]) + const [minPrice, setMinPrice] = useState('') + const [maxPrice, setMaxPrice] = useState('') + const [inStockOnly, setInStockOnly] = useState(false) + const [rating, setRating] = useState(null) + + const { data: categoriesData } = useQuery({ + queryKey: ['categories'], + queryFn: getCategories, + staleTime: 1000 * 60 * 10, + }) + + const categories: Category[] = useMemo(() => { + return categoriesData?.results?.data ?? [] + }, [categoriesData]) + + useEffect(() => { + const qsCategories = searchParams.get('category')?.split(',').filter(Boolean) ?? [] + const qsMin = searchParams.get('minPrice') ?? '' + const qsMax = searchParams.get('maxPrice') ?? '' + const qsStock = searchParams.get('inStock') === '1' + const qsRating = searchParams.get('rating') + + setSelectedCategories(qsCategories) + setMinPrice(qsMin) + setMaxPrice(qsMax) + setInStockOnly(qsStock) + setRating(qsRating ? Number(qsRating) : null) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const applyFilters = () => { + const params = new URLSearchParams(searchParams.toString()) + if (selectedCategories.length > 0) { + params.set('category', selectedCategories.join(',')) + } else { + params.delete('category') + } + if (minPrice) params.set('minPrice', minPrice) + else params.delete('minPrice') + if (maxPrice) params.set('maxPrice', maxPrice) + else params.delete('maxPrice') + if (inStockOnly) params.set('inStock', '1') + else params.delete('inStock') + if (rating) params.set('rating', String(rating)) + else params.delete('rating') + router.push(`${pathname}?${params.toString()}`) + } + + const clearFilters = () => { + setSelectedCategories([]) + setMinPrice('') + setMaxPrice('') + setInStockOnly(false) + setRating(null) + const params = new URLSearchParams(searchParams.toString()) + ;['category', 'minPrice', 'maxPrice', 'inStock', 'rating'].forEach((k) => params.delete(k)) + router.push(`${pathname}?${params.toString()}`) + } + + return ( +
+
+ +
فیلترها
+
+ +
+ + + دسته‌بندی ها + + setSelectedCategories(id ? [id] : [])} + /> + + + + + قیمت + +
+ setMinPrice(e.target.value)} + placeholder='0' + seprator + inputMode='numeric' + /> + setMaxPrice(e.target.value)} + placeholder='0' + seprator + inputMode='numeric' + /> +
+
+
+ + + وضعیت موجودی + + + + + + + امتیاز خریداران + +
+ {[4, 3, 2].map((r) => ( + + ))} + +
+
+
+
+
+ +
+ + +
+
+ ) +} + +export default Filters \ No newline at end of file diff --git a/src/app/products/components/Sorts.tsx b/src/app/products/components/Sorts.tsx new file mode 100644 index 0000000..3195ba1 --- /dev/null +++ b/src/app/products/components/Sorts.tsx @@ -0,0 +1,43 @@ +import { Sort } from 'iconsax-react' +import { FC } from 'react' + +const Sorts: FC = () => { + return ( +
+
+
+ +
مرتب سازی:
+
+ +
+ مرتبط ترین +
+
+ پربازدیدترین +
+
+ جدیدترین +
+
+ پرفروش ترین +
+
+ ارزانترین +
+
+ گرانترین +
+
+ پیشنهاد خریداران +
+
+ +
+ 100 کالا +
+
+ ) +} + +export default Sorts \ No newline at end of file diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx new file mode 100644 index 0000000..eb951e7 --- /dev/null +++ b/src/app/products/page.tsx @@ -0,0 +1,54 @@ +import withLayout from '@/hoc/withLayout' +import { NextPage } from 'next' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@/components/ui/breadcrumb' +import Filters from './components/Filters' +import Sorts from './components/Sorts' +import GridWrapper from '@/components/GridWrapper' +import ProductCard from '@/components/ProductCard' + +const Products: NextPage = () => { + return ( +
+ + + + فروشگاه آناهیتا + + / + + بهداشت خانگی + + / + + مواد شوینده + + + + +
+ +
+ + +
+ + + + + + +
+
+
+
+ ) +} + +export default withLayout(Products) \ No newline at end of file diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx index 4194a75..cd8dd1f 100644 --- a/src/components/ProductCard.tsx +++ b/src/components/ProductCard.tsx @@ -4,7 +4,7 @@ import { FC } from 'react' const ProductCard: FC = () => { return ( -
+
{
- -
) } diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..171b2d3 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return