products
This commit is contained in:
Generated
+218
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = () => {
|
||||
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'>
|
||||
<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`}
|
||||
@@ -35,8 +35,6 @@ const ProductCard: FC = () => {
|
||||
<div className='flex justify-end'>
|
||||
<Heart size={20} color='#000' />
|
||||
</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