@thorprovider/blocks 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +156 -40
- package/package.json +1 -1
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
- package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
- package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +80 -81
- package/src/lib/contracts-curated.test.ts +5 -5
- package/src/lib/contracts.test.ts +55 -0
- package/src/lib/contracts.ts +64 -2
- package/tsup.config.ts +1 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { ChevronDown, Search, SlidersHorizontal, X } from 'lucide-react'
|
|
5
|
-
|
|
6
|
-
import { Badge } from '../../../ui/badge'
|
|
7
|
-
import { Button } from '../../../ui/button'
|
|
8
|
-
import {
|
|
9
|
-
DropdownMenu,
|
|
10
|
-
DropdownMenuContent,
|
|
11
|
-
DropdownMenuItem,
|
|
12
|
-
DropdownMenuSeparator,
|
|
13
|
-
DropdownMenuTrigger,
|
|
14
|
-
} from '../../../ui/dropdown-menu'
|
|
15
|
-
import { Input } from '../../../ui/input'
|
|
16
|
-
import { cn } from '../../../lib/utils'
|
|
17
|
-
|
|
18
|
-
export interface CategoryFilter1Category {
|
|
19
|
-
id: string
|
|
20
|
-
name: string
|
|
21
|
-
count: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface CategoryFilter1PriceRange {
|
|
25
|
-
id: string
|
|
26
|
-
label: string
|
|
27
|
-
min: number
|
|
28
|
-
max: number | null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface CategoryFilter1SortOption {
|
|
32
|
-
id: string
|
|
33
|
-
label: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface CategoryFilter1Filters {
|
|
37
|
-
category: string
|
|
38
|
-
priceRange: string
|
|
39
|
-
sort: string
|
|
40
|
-
search: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface CategoryFilter1Props {
|
|
44
|
-
title?: string
|
|
45
|
-
description?: string
|
|
46
|
-
categories: CategoryFilter1Category[]
|
|
47
|
-
priceRanges: CategoryFilter1PriceRange[]
|
|
48
|
-
sortOptions: CategoryFilter1SortOption[]
|
|
49
|
-
onFilterChange?: (filters: CategoryFilter1Filters) => void
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function CategoryFilter1({
|
|
53
|
-
title,
|
|
54
|
-
description,
|
|
55
|
-
categories,
|
|
56
|
-
priceRanges,
|
|
57
|
-
sortOptions,
|
|
58
|
-
onFilterChange,
|
|
59
|
-
}: CategoryFilter1Props) {
|
|
60
|
-
const [selectedCategory, setSelectedCategory] = useState('all')
|
|
61
|
-
const [selectedPriceRange, setSelectedPriceRange] = useState('all')
|
|
62
|
-
const [selectedSort, setSelectedSort] = useState('featured')
|
|
63
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
64
|
-
|
|
65
|
-
const activeFilters: { type: string; label: string; value: string }[] = []
|
|
66
|
-
if (selectedCategory !== 'all') {
|
|
67
|
-
const category = categories.find(c => c.id === selectedCategory)
|
|
68
|
-
if (category) activeFilters.push({ type: 'category', label: category.name, value: selectedCategory })
|
|
69
|
-
}
|
|
70
|
-
if (selectedPriceRange !== 'all') {
|
|
71
|
-
const priceRange = priceRanges.find(p => p.id === selectedPriceRange)
|
|
72
|
-
if (priceRange) activeFilters.push({ type: 'price', label: priceRange.label, value: selectedPriceRange })
|
|
73
|
-
}
|
|
74
|
-
if (searchQuery) {
|
|
75
|
-
activeFilters.push({ type: 'search', label: `"${searchQuery}"`, value: searchQuery })
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const selectedCategoryCount = categories.find(c => c.id === selectedCategory)?.count ?? 0
|
|
79
|
-
|
|
80
|
-
const clearFilter = (type: string) => {
|
|
81
|
-
let nextCategory = selectedCategory
|
|
82
|
-
let nextPrice = selectedPriceRange
|
|
83
|
-
let nextSearch = searchQuery
|
|
84
|
-
if (type === 'category') nextCategory = 'all'
|
|
85
|
-
if (type === 'price') nextPrice = 'all'
|
|
86
|
-
if (type === 'search') nextSearch = ''
|
|
87
|
-
setSelectedCategory(nextCategory)
|
|
88
|
-
setSelectedPriceRange(nextPrice)
|
|
89
|
-
setSearchQuery(nextSearch)
|
|
90
|
-
onFilterChange?.({ category: nextCategory, priceRange: nextPrice, sort: selectedSort, search: nextSearch })
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const clearAllFilters = () => {
|
|
94
|
-
setSelectedCategory('all')
|
|
95
|
-
setSelectedPriceRange('all')
|
|
96
|
-
setSearchQuery('')
|
|
97
|
-
onFilterChange?.({ category: 'all', priceRange: 'all', sort: selectedSort, search: '' })
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
101
|
-
const next = e.target.value
|
|
102
|
-
setSearchQuery(next)
|
|
103
|
-
onFilterChange?.({ category: selectedCategory, priceRange: selectedPriceRange, sort: selectedSort, search: next })
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const handleSortSelect = (id: string) => {
|
|
107
|
-
setSelectedSort(id)
|
|
108
|
-
onFilterChange?.({ category: selectedCategory, priceRange: selectedPriceRange, sort: id, search: searchQuery })
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const handleCategorySelect = (id: string) => {
|
|
112
|
-
setSelectedCategory(id)
|
|
113
|
-
onFilterChange?.({ category: id, priceRange: selectedPriceRange, sort: selectedSort, search: searchQuery })
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const handlePriceRangeSelect = (id: string) => {
|
|
117
|
-
setSelectedPriceRange(id)
|
|
118
|
-
onFilterChange?.({ category: selectedCategory, priceRange: id, sort: selectedSort, search: searchQuery })
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<section className="py-8">
|
|
123
|
-
<div className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
124
|
-
<div className="mb-8">
|
|
125
|
-
{title ? <h2 className="text-3xl font-bold tracking-tight text-balance">{title}</h2> : null}
|
|
126
|
-
{description ? <p className="text-muted-foreground mt-2">{description}</p> : null}
|
|
127
|
-
{!description && title ? (
|
|
128
|
-
<p className="text-muted-foreground mt-2">
|
|
129
|
-
Browse our collection of {selectedCategoryCount} products
|
|
130
|
-
</p>
|
|
131
|
-
) : null}
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
<div className="mb-6 flex flex-col gap-4">
|
|
135
|
-
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
136
|
-
<div className="relative max-w-md flex-1">
|
|
137
|
-
<Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
|
|
138
|
-
<Input
|
|
139
|
-
placeholder="Search products..."
|
|
140
|
-
value={searchQuery}
|
|
141
|
-
onChange={handleSearchChange}
|
|
142
|
-
className="h-9 pl-10"
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<DropdownMenu>
|
|
147
|
-
<DropdownMenuTrigger asChild>
|
|
148
|
-
<Button variant="outline" className="h-9 w-full cursor-pointer px-4 py-2 sm:w-auto">
|
|
149
|
-
<SlidersHorizontal data-icon="inline-start" />
|
|
150
|
-
Sort: {sortOptions.find(s => s.id === selectedSort)?.label}
|
|
151
|
-
<ChevronDown data-icon="inline-end" />
|
|
152
|
-
</Button>
|
|
153
|
-
</DropdownMenuTrigger>
|
|
154
|
-
<DropdownMenuContent align="end" className="w-56">
|
|
155
|
-
{sortOptions.map(option => (
|
|
156
|
-
<DropdownMenuItem
|
|
157
|
-
key={option.id}
|
|
158
|
-
onClick={() => handleSortSelect(option.id)}
|
|
159
|
-
className={selectedSort === option.id ? 'bg-accent' : ''}
|
|
160
|
-
>
|
|
161
|
-
{option.label}
|
|
162
|
-
</DropdownMenuItem>
|
|
163
|
-
))}
|
|
164
|
-
</DropdownMenuContent>
|
|
165
|
-
</DropdownMenu>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<div className="flex flex-wrap gap-3">
|
|
169
|
-
<DropdownMenu>
|
|
170
|
-
<DropdownMenuTrigger asChild>
|
|
171
|
-
<Button variant="outline" size="sm" className="h-8 cursor-pointer px-3 text-xs">
|
|
172
|
-
Category: {categories.find(c => c.id === selectedCategory)?.name}
|
|
173
|
-
<ChevronDown data-icon="inline-end" />
|
|
174
|
-
</Button>
|
|
175
|
-
</DropdownMenuTrigger>
|
|
176
|
-
<DropdownMenuContent className="w-56">
|
|
177
|
-
{categories.map(category => (
|
|
178
|
-
<DropdownMenuItem
|
|
179
|
-
key={category.id}
|
|
180
|
-
onClick={() => handleCategorySelect(category.id)}
|
|
181
|
-
className={cn(selectedCategory === category.id && 'bg-accent')}
|
|
182
|
-
>
|
|
183
|
-
<div className="flex w-full items-center justify-between">
|
|
184
|
-
<span>{category.name}</span>
|
|
185
|
-
<Badge variant="secondary" className="px-2.5 py-0.5 text-xs font-semibold">
|
|
186
|
-
{category.count}
|
|
187
|
-
</Badge>
|
|
188
|
-
</div>
|
|
189
|
-
</DropdownMenuItem>
|
|
190
|
-
))}
|
|
191
|
-
</DropdownMenuContent>
|
|
192
|
-
</DropdownMenu>
|
|
193
|
-
|
|
194
|
-
<DropdownMenu>
|
|
195
|
-
<DropdownMenuTrigger asChild>
|
|
196
|
-
<Button variant="outline" size="sm" className="h-8 cursor-pointer px-3 text-xs">
|
|
197
|
-
Price: {priceRanges.find(p => p.id === selectedPriceRange)?.label}
|
|
198
|
-
<ChevronDown data-icon="inline-end" />
|
|
199
|
-
</Button>
|
|
200
|
-
</DropdownMenuTrigger>
|
|
201
|
-
<DropdownMenuContent className="w-48">
|
|
202
|
-
{priceRanges.map(range => (
|
|
203
|
-
<DropdownMenuItem
|
|
204
|
-
key={range.id}
|
|
205
|
-
onClick={() => handlePriceRangeSelect(range.id)}
|
|
206
|
-
className={cn(selectedPriceRange === range.id && 'bg-accent')}
|
|
207
|
-
>
|
|
208
|
-
{range.label}
|
|
209
|
-
</DropdownMenuItem>
|
|
210
|
-
))}
|
|
211
|
-
</DropdownMenuContent>
|
|
212
|
-
</DropdownMenu>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{activeFilters.length > 0 ? (
|
|
216
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
217
|
-
<span className="text-muted-foreground text-sm font-medium">Active filters:</span>
|
|
218
|
-
{activeFilters.map(filter => (
|
|
219
|
-
<Badge key={filter.type} variant="secondary" className="px-2.5 py-0.5 font-semibold">
|
|
220
|
-
{filter.label}
|
|
221
|
-
<Button
|
|
222
|
-
variant="ghost"
|
|
223
|
-
size="sm"
|
|
224
|
-
className="!p-1 h-auto cursor-pointer text-inherit"
|
|
225
|
-
onClick={() => clearFilter(filter.type)}
|
|
226
|
-
>
|
|
227
|
-
<X className="size-3" />
|
|
228
|
-
</Button>
|
|
229
|
-
</Badge>
|
|
230
|
-
))}
|
|
231
|
-
<DropdownMenuSeparator className="mx-2" />
|
|
232
|
-
<Button
|
|
233
|
-
variant="ghost"
|
|
234
|
-
size="sm"
|
|
235
|
-
onClick={clearAllFilters}
|
|
236
|
-
className="h-auto cursor-pointer p-1.5 text-xs text-muted-foreground"
|
|
237
|
-
>
|
|
238
|
-
Clear all
|
|
239
|
-
</Button>
|
|
240
|
-
</div>
|
|
241
|
-
) : null}
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
<div className="bg-muted/50 rounded-lg border p-4">
|
|
245
|
-
<div className="flex items-center justify-between">
|
|
246
|
-
<div className="flex items-center gap-4">
|
|
247
|
-
<span className="text-sm font-medium">Showing {selectedCategoryCount} results</span>
|
|
248
|
-
{searchQuery && <span className="text-muted-foreground text-sm">for "{searchQuery}"</span>}
|
|
249
|
-
</div>
|
|
250
|
-
<div className="text-muted-foreground text-xs">
|
|
251
|
-
Sorted by {sortOptions.find(s => s.id === selectedSort)?.label}
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
</section>
|
|
257
|
-
)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export default CategoryFilter1
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useId } from 'react'
|
|
4
|
-
|
|
5
|
-
import { Button } from '../../../ui/button'
|
|
6
|
-
import { CardTitle } from '../../../ui/card'
|
|
7
|
-
import { Input } from '../../../ui/input'
|
|
8
|
-
import { CheckboxCheck } from '../checkout-forms/checkbox-check'
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
CategoryFilter2Category,
|
|
12
|
-
CategoryFilter2Collection,
|
|
13
|
-
CategoryFilter2Color,
|
|
14
|
-
CategoryFilter2PriceRange,
|
|
15
|
-
} from './category-filter-2'
|
|
16
|
-
|
|
17
|
-
interface CategoryFilter2FilterPanelProps {
|
|
18
|
-
selectedText?: string
|
|
19
|
-
clearAllLabel?: string
|
|
20
|
-
priceRangeTitle?: string
|
|
21
|
-
priceRange: number[]
|
|
22
|
-
/**
|
|
23
|
-
* Real backend-provided range only. When absent the price section is not
|
|
24
|
-
* rendered — the block never fabricates a `0..1000` range (E2 design
|
|
25
|
-
* Decision 8 / spec "Price controls require real price metadata").
|
|
26
|
-
*/
|
|
27
|
-
priceRangeConfig?: CategoryFilter2PriceRange
|
|
28
|
-
minLabel?: string
|
|
29
|
-
maxLabel?: string
|
|
30
|
-
onPriceChange: (range: number[]) => void
|
|
31
|
-
categoriesTitle?: string
|
|
32
|
-
categories: CategoryFilter2Category[]
|
|
33
|
-
selectedCategories: string[]
|
|
34
|
-
onCategoryChange: (category: string, checked: boolean) => void
|
|
35
|
-
collectionsTitle?: string
|
|
36
|
-
collections: CategoryFilter2Collection[]
|
|
37
|
-
selectedCollections: string[]
|
|
38
|
-
onCollectionChange: (collection: string, checked: boolean) => void
|
|
39
|
-
sizesTitle?: string
|
|
40
|
-
sizes: string[]
|
|
41
|
-
selectedSize: string | null
|
|
42
|
-
onSizeChange: (size: string | null) => void
|
|
43
|
-
colorsTitle?: string
|
|
44
|
-
colors: CategoryFilter2Color[]
|
|
45
|
-
selectedColor: string | null
|
|
46
|
-
onColorChange: (color: string | null) => void
|
|
47
|
-
onClearAll: () => void
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function CategoryFilter2FilterPanel({
|
|
51
|
-
selectedText = 'Filtros',
|
|
52
|
-
clearAllLabel = 'Restablecer',
|
|
53
|
-
priceRangeTitle = 'Rango de precio',
|
|
54
|
-
priceRange,
|
|
55
|
-
priceRangeConfig,
|
|
56
|
-
minLabel = 'Mínimo',
|
|
57
|
-
maxLabel = 'Máximo',
|
|
58
|
-
onPriceChange,
|
|
59
|
-
categoriesTitle = 'Categoría',
|
|
60
|
-
categories,
|
|
61
|
-
selectedCategories,
|
|
62
|
-
onCategoryChange,
|
|
63
|
-
collectionsTitle = 'Colección',
|
|
64
|
-
collections,
|
|
65
|
-
selectedCollections,
|
|
66
|
-
onCollectionChange,
|
|
67
|
-
sizesTitle = 'Tamaño',
|
|
68
|
-
sizes,
|
|
69
|
-
selectedSize,
|
|
70
|
-
onSizeChange,
|
|
71
|
-
colorsTitle = 'Color',
|
|
72
|
-
colors,
|
|
73
|
-
selectedColor,
|
|
74
|
-
onColorChange,
|
|
75
|
-
onClearAll,
|
|
76
|
-
}: CategoryFilter2FilterPanelProps) {
|
|
77
|
-
const priceInputId = useId()
|
|
78
|
-
const categoryInputId = useId()
|
|
79
|
-
const collectionInputId = useId()
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<>
|
|
83
|
-
<div className="flex flex-col gap-6">
|
|
84
|
-
<div className="mb-3 flex items-center justify-between">
|
|
85
|
-
<h1 className="font-semibold">{selectedText}</h1>
|
|
86
|
-
<Button variant="ghost" size="sm" className="h-8 cursor-pointer px-2 text-xs" onClick={onClearAll}>
|
|
87
|
-
{clearAllLabel}
|
|
88
|
-
</Button>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
{priceRangeConfig ? (
|
|
93
|
-
<div className="mt-4 flex flex-col gap-4">
|
|
94
|
-
<CardTitle className="font-semibold">{priceRangeTitle}</CardTitle>
|
|
95
|
-
<div className="flex items-center gap-3">
|
|
96
|
-
<div className="flex flex-1 flex-col gap-1">
|
|
97
|
-
<label htmlFor={`${priceInputId}-min`}>{minLabel}</label>
|
|
98
|
-
<Input
|
|
99
|
-
id={`${priceInputId}-min`}
|
|
100
|
-
type="number"
|
|
101
|
-
value={priceRange[0] ?? priceRangeConfig.min}
|
|
102
|
-
onChange={e => {
|
|
103
|
-
const value = parseInt(e.target.value)
|
|
104
|
-
if (!isNaN(value) && value >= priceRangeConfig.min) {
|
|
105
|
-
onPriceChange([value, priceRange[1] ?? priceRangeConfig.max])
|
|
106
|
-
}
|
|
107
|
-
}}
|
|
108
|
-
min={priceRangeConfig.min}
|
|
109
|
-
max={priceRange[1] ?? priceRangeConfig.max}
|
|
110
|
-
step={priceRangeConfig.step}
|
|
111
|
-
className="h-9"
|
|
112
|
-
/>
|
|
113
|
-
</div>
|
|
114
|
-
<div className="mt-5 flex-none">-</div>
|
|
115
|
-
<div className="flex flex-1 flex-col gap-1">
|
|
116
|
-
<label htmlFor={`${priceInputId}-max`}>{maxLabel}</label>
|
|
117
|
-
<Input
|
|
118
|
-
id={`${priceInputId}-max`}
|
|
119
|
-
type="number"
|
|
120
|
-
value={priceRange[1] ?? priceRangeConfig.max}
|
|
121
|
-
onChange={e => {
|
|
122
|
-
const value = parseInt(e.target.value)
|
|
123
|
-
if (!isNaN(value) && value <= priceRangeConfig.max) {
|
|
124
|
-
onPriceChange([priceRange[0] ?? priceRangeConfig.min, value])
|
|
125
|
-
}
|
|
126
|
-
}}
|
|
127
|
-
min={priceRange[0] ?? priceRangeConfig.min}
|
|
128
|
-
max={priceRangeConfig.max}
|
|
129
|
-
step={priceRangeConfig.step}
|
|
130
|
-
className="h-9"
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
) : null}
|
|
136
|
-
|
|
137
|
-
{categories.length > 0 ? (
|
|
138
|
-
<div className="mt-4 flex flex-col gap-4">
|
|
139
|
-
<CardTitle className="font-semibold">{categoriesTitle}</CardTitle>
|
|
140
|
-
<div className="flex flex-col gap-2">
|
|
141
|
-
{categories.map(category => (
|
|
142
|
-
<div key={category.id} className="flex items-center gap-2">
|
|
143
|
-
<CheckboxCheck
|
|
144
|
-
id={`${categoryInputId}-${category.id}`}
|
|
145
|
-
checked={selectedCategories.includes(category.id)}
|
|
146
|
-
onCheckedChange={checked => onCategoryChange(category.id, checked as boolean)}
|
|
147
|
-
/>
|
|
148
|
-
<label htmlFor={`${categoryInputId}-${category.id}`} className="text-sm">
|
|
149
|
-
{category.label}
|
|
150
|
-
</label>
|
|
151
|
-
</div>
|
|
152
|
-
))}
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
) : null}
|
|
156
|
-
|
|
157
|
-
{collections.length > 0 ? (
|
|
158
|
-
<div className="mt-4 flex flex-col gap-4">
|
|
159
|
-
<CardTitle className="font-semibold">{collectionsTitle}</CardTitle>
|
|
160
|
-
<div className="flex flex-col gap-2">
|
|
161
|
-
{collections.map(collection => (
|
|
162
|
-
<div key={collection.id} className="flex items-center gap-2">
|
|
163
|
-
<CheckboxCheck
|
|
164
|
-
id={`${collectionInputId}-${collection.id}`}
|
|
165
|
-
checked={selectedCollections.includes(collection.id)}
|
|
166
|
-
onCheckedChange={checked => onCollectionChange(collection.id, checked as boolean)}
|
|
167
|
-
/>
|
|
168
|
-
<label htmlFor={`${collectionInputId}-${collection.id}`} className="text-sm">
|
|
169
|
-
{collection.label}
|
|
170
|
-
</label>
|
|
171
|
-
</div>
|
|
172
|
-
))}
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
) : null}
|
|
176
|
-
|
|
177
|
-
{sizes.length > 0 ? (
|
|
178
|
-
<div className="mt-4 flex flex-col gap-4">
|
|
179
|
-
<CardTitle className="font-semibold">{sizesTitle}</CardTitle>
|
|
180
|
-
<div className="flex flex-wrap gap-2">
|
|
181
|
-
{sizes.map(size => (
|
|
182
|
-
<Button
|
|
183
|
-
key={size}
|
|
184
|
-
variant={selectedSize === size ? 'default' : 'outline'}
|
|
185
|
-
size="sm"
|
|
186
|
-
className="h-8 cursor-pointer px-3 text-xs"
|
|
187
|
-
onClick={() => onSizeChange(size === selectedSize ? null : size)}
|
|
188
|
-
aria-pressed={selectedSize === size}
|
|
189
|
-
>
|
|
190
|
-
{size}
|
|
191
|
-
</Button>
|
|
192
|
-
))}
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
) : null}
|
|
196
|
-
|
|
197
|
-
{colors.length > 0 ? (
|
|
198
|
-
<div className="mt-4 flex flex-col gap-4">
|
|
199
|
-
<CardTitle className="font-semibold">{colorsTitle}</CardTitle>
|
|
200
|
-
<div className="flex flex-wrap gap-2">
|
|
201
|
-
{colors.map(color => (
|
|
202
|
-
<Button
|
|
203
|
-
key={color.id}
|
|
204
|
-
onClick={() => onColorChange(color.id === selectedColor ? null : color.id)}
|
|
205
|
-
className={`relative size-8 cursor-pointer rounded-sm transition-transform duration-200 ${
|
|
206
|
-
selectedColor === color.id ? 'ring-primary scale-105 ring-2' : 'hover:ring-foreground/20 hover:ring-1'
|
|
207
|
-
} ${color.id === 'white' ? 'border-border border' : ''}`}
|
|
208
|
-
style={{ backgroundColor: color.color }}
|
|
209
|
-
aria-pressed={selectedColor === color.id}
|
|
210
|
-
aria-label={color.id}
|
|
211
|
-
>
|
|
212
|
-
{selectedColor === color.id && <div className="border-background absolute inset-0 rounded-sm border-2" />}
|
|
213
|
-
</Button>
|
|
214
|
-
))}
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
) : null}
|
|
218
|
-
</>
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export default CategoryFilter2FilterPanel
|