products
This commit is contained in:
Generated
+218
@@ -8,6 +8,7 @@
|
|||||||
"name": "shop",
|
"name": "shop",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tanstack/react-query": "^5.84.1",
|
"@tanstack/react-query": "^5.84.1",
|
||||||
@@ -979,6 +980,99 @@
|
|||||||
"node": ">=12.4.0"
|
"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": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
"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": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tanstack/react-query": "^5.84.1",
|
"@tanstack/react-query": "^5.84.1",
|
||||||
|
|||||||
@@ -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<Props> = ({ categories, selectedId, onSelect }) => {
|
||||||
|
const roots = useMemo(() => categories || [], [categories])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-0'>
|
||||||
|
<Row
|
||||||
|
label='همه کالا ها'
|
||||||
|
isLeaf
|
||||||
|
isSelected={selectedId === null}
|
||||||
|
onClick={() => onSelect(null)}
|
||||||
|
/>
|
||||||
|
{roots.map((cat) => (
|
||||||
|
<Node key={cat._id} node={cat} selectedId={selectedId} onSelect={onSelect} depth={0} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeProps = {
|
||||||
|
node: Category
|
||||||
|
selectedId: string | null
|
||||||
|
onSelect: (id: string | null) => void
|
||||||
|
depth: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node: FC<NodeProps> = ({ node, selectedId, onSelect, depth }) => {
|
||||||
|
const [open, setOpen] = useState<boolean>(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 (
|
||||||
|
<div>
|
||||||
|
<Row
|
||||||
|
label={node.title_fa}
|
||||||
|
isLeaf={!hasChildren}
|
||||||
|
isSelected={selectedId === node._id}
|
||||||
|
onClick={toggle}
|
||||||
|
open={open}
|
||||||
|
depth={depth}
|
||||||
|
/>
|
||||||
|
{hasChildren && open && (
|
||||||
|
<div className='mt-1.5 space-y-1.5'>
|
||||||
|
{node.children.map((child) => (
|
||||||
|
<Node
|
||||||
|
key={child._id}
|
||||||
|
node={child}
|
||||||
|
selectedId={selectedId}
|
||||||
|
onSelect={onSelect}
|
||||||
|
depth={depth + 1}
|
||||||
|
/>)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowProps = {
|
||||||
|
label: string
|
||||||
|
isLeaf?: boolean
|
||||||
|
isSelected?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
open?: boolean
|
||||||
|
depth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row: FC<RowProps> = ({ label, isLeaf, isSelected, onClick, open, depth = 0 }) => {
|
||||||
|
return (
|
||||||
|
<button type='button' onClick={onClick} className='w-full'>
|
||||||
|
<div
|
||||||
|
className='flex items-center py-2 border-b border-border hover:bg-accent/40 rounded-md'
|
||||||
|
style={{ paddingInlineStart: `${depth * 16}px` }}
|
||||||
|
>
|
||||||
|
<div className='ml-auto flex items-center gap-2.5'>
|
||||||
|
{!isLeaf && (
|
||||||
|
<ArrowDown2
|
||||||
|
size={16}
|
||||||
|
color='#6B7280'
|
||||||
|
className={`transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className={`text-[14px] leading-[22px] ${isSelected ? 'text-primary font-medium' : 'text-[#333333]'}`}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryTree
|
||||||
|
|
||||||
|
|
||||||
@@ -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<string[]>([])
|
||||||
|
const [minPrice, setMinPrice] = useState<string>('')
|
||||||
|
const [maxPrice, setMaxPrice] = useState<string>('')
|
||||||
|
const [inStockOnly, setInStockOnly] = useState<boolean>(false)
|
||||||
|
const [rating, setRating] = useState<number | null>(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 (
|
||||||
|
<div className='w-[270px] border border-border rounded-[10px] p-5'>
|
||||||
|
<div className='flex items-center gap-2.5'>
|
||||||
|
<Setting5 size={24} color='#666666' />
|
||||||
|
<div className='text-[#666666]'>فیلترها</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-3.5'>
|
||||||
|
<Accordion type='multiple' className='w-full'>
|
||||||
|
<AccordionItem value='category'>
|
||||||
|
<AccordionTrigger className='text-[#333333]'>دستهبندی ها</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<CategoryTree
|
||||||
|
categories={categories}
|
||||||
|
selectedId={selectedCategories[0] ?? null}
|
||||||
|
onSelect={(id) => setSelectedCategories(id ? [id] : [])}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value='price'>
|
||||||
|
<AccordionTrigger className='text-[#333333]'>قیمت</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className='grid grid-cols-2 gap-2.5'>
|
||||||
|
<Input
|
||||||
|
label='حداقل'
|
||||||
|
value={minPrice}
|
||||||
|
onChange={(e) => setMinPrice(e.target.value)}
|
||||||
|
placeholder='0'
|
||||||
|
seprator
|
||||||
|
inputMode='numeric'
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label='حداکثر'
|
||||||
|
value={maxPrice}
|
||||||
|
onChange={(e) => setMaxPrice(e.target.value)}
|
||||||
|
placeholder='0'
|
||||||
|
seprator
|
||||||
|
inputMode='numeric'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value='status'>
|
||||||
|
<AccordionTrigger className='text-[#333333]'>وضعیت موجودی</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<label className='flex items-center gap-2.5 text-sm text-[#4A4A4A]'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='size-4 rounded border-border'
|
||||||
|
checked={inStockOnly}
|
||||||
|
onChange={(e) => setInStockOnly(e.target.checked)}
|
||||||
|
/>
|
||||||
|
فقط کالاهای موجود
|
||||||
|
</label>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem value='rating'>
|
||||||
|
<AccordionTrigger className='text-[#333333]'>امتیاز خریداران</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className='flex flex-col gap-2.5 text-sm text-[#4A4A4A]'>
|
||||||
|
{[4, 3, 2].map((r) => (
|
||||||
|
<label key={r} className='flex items-center gap-2.5'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='rating'
|
||||||
|
className='size-4 border-border'
|
||||||
|
checked={rating === r}
|
||||||
|
onChange={() => setRating(r)}
|
||||||
|
/>
|
||||||
|
{r} ستاره و بالاتر
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => setRating(null)}
|
||||||
|
className='text-xs text-muted-foreground hover:text-foreground text-right mt-1'
|
||||||
|
>
|
||||||
|
حذف امتیاز
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 grid grid-cols-2 gap-2.5'>
|
||||||
|
<Button variant='outline' onClick={clearFilters}>حذف</Button>
|
||||||
|
<Button onClick={applyFilters}>اعمال</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Filters
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Sort } from 'iconsax-react'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
const Sorts: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-between items-center text-[#999999] text-sm border-b border-border pb-5'>
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<Sort size={24} color='#333333' />
|
||||||
|
<div className='text-sm text-[#333333]'>مرتب سازی:</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='cursor-pointer'>
|
||||||
|
مرتبط ترین
|
||||||
|
</div>
|
||||||
|
<div className='cursor-pointer'>
|
||||||
|
پربازدیدترین
|
||||||
|
</div>
|
||||||
|
<div className='text-primary'>
|
||||||
|
جدیدترین
|
||||||
|
</div>
|
||||||
|
<div className='cursor-pointer'>
|
||||||
|
پرفروش ترین
|
||||||
|
</div>
|
||||||
|
<div className='cursor-pointer'>
|
||||||
|
ارزانترین
|
||||||
|
</div>
|
||||||
|
<div className='cursor-pointer'>
|
||||||
|
گرانترین
|
||||||
|
</div>
|
||||||
|
<div className='cursor-pointer'>
|
||||||
|
پیشنهاد خریداران
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
100 کالا
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sorts
|
||||||
@@ -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 (
|
||||||
|
<div className="mt-24 px-20">
|
||||||
|
<Breadcrumb aria-label="breadcrumb">
|
||||||
|
<BreadcrumbList className="text-sm text-muted-foreground">
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink href="/">فروشگاه آناهیتا</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator className="text-muted-foreground select-none">/</BreadcrumbSeparator>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink href="/products">بهداشت خانگی</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator className="text-muted-foreground select-none">/</BreadcrumbSeparator>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage className="text-foreground">مواد شوینده</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
|
||||||
|
<div className='mt-14 flex gap-6'>
|
||||||
|
<Filters />
|
||||||
|
<div className='flex-1'>
|
||||||
|
<Sorts />
|
||||||
|
|
||||||
|
<div className='mt-10'>
|
||||||
|
<GridWrapper desktop={4} mobile={1}>
|
||||||
|
<ProductCard />
|
||||||
|
<ProductCard />
|
||||||
|
<ProductCard />
|
||||||
|
<ProductCard />
|
||||||
|
</GridWrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withLayout(Products)
|
||||||
@@ -4,7 +4,7 @@ import { FC } from 'react'
|
|||||||
|
|
||||||
const ProductCard: FC = () => {
|
const ProductCard: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className='bg-white rounded-[40px] p-6 w-full min-w-[280px] max-w-[280px] mx-auto'>
|
<div className='bg-white rounded-[40px] p-6 w-full max-w-[280px] mx-auto shadow'>
|
||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<Image
|
<Image
|
||||||
src={`https://dkstatics-public.digikala.com/digikala-products/40026b18c2b053ac4a68c3288556dc899a77aecd_1727277566.jpg?x-oss-process=image/resize,m_lfit,h_300,w_300/format,webp/quality,q_80`}
|
src={`https://dkstatics-public.digikala.com/digikala-products/40026b18c2b053ac4a68c3288556dc899a77aecd_1727277566.jpg?x-oss-process=image/resize,m_lfit,h_300,w_300/format,webp/quality,q_80`}
|
||||||
@@ -35,8 +35,6 @@ const ProductCard: FC = () => {
|
|||||||
<div className='flex justify-end'>
|
<div className='flex justify-end'>
|
||||||
<Heart size={20} color='#000' />
|
<Heart size={20} color='#000' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
@@ -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 <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbLink({
|
||||||
|
asChild,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="breadcrumb-link"
|
||||||
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("text-foreground font-normal", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user