This commit is contained in:
hamid zarghami
2025-08-10 13:16:56 +03:30
parent 6fcb8a6904
commit 1859d0b2c9
9 changed files with 777 additions and 3 deletions
+218
View File
@@ -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",
+1
View File
@@ -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
+176
View File
@@ -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
+43
View File
@@ -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
+54
View File
@@ -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)
+1 -3
View File
@@ -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>
) )
} }
+66
View File
@@ -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 }
+109
View File
@@ -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,
}