@thorprovider/blocks 0.1.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.
Files changed (184) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/.turbo/turbo-test.log +10 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +161 -0
  6. package/components.json +33 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/contracts.d.mts +563 -0
  9. package/dist/contracts.d.ts +563 -0
  10. package/dist/contracts.js +207 -0
  11. package/dist/contracts.js.map +1 -0
  12. package/dist/contracts.mjs +168 -0
  13. package/dist/contracts.mjs.map +1 -0
  14. package/dist/index.d.mts +2408 -0
  15. package/dist/index.d.ts +2408 -0
  16. package/dist/index.js +16422 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +16352 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/docs/standards/data-contracts.md +70 -0
  21. package/docs/standards/theming.md +85 -0
  22. package/package.json +62 -0
  23. package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
  24. package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
  25. package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
  26. package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
  27. package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
  28. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
  29. package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
  30. package/src/blocks/application/verification/verify-1.tsx +307 -0
  31. package/src/blocks/application/verification/verify-confetti.tsx +192 -0
  32. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
  33. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
  34. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
  35. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
  36. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
  37. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
  38. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
  39. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
  40. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
  41. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
  42. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
  43. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
  44. package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
  45. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
  46. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
  47. package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
  48. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
  49. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
  50. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
  51. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
  52. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
  53. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
  54. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
  55. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
  56. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
  57. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
  58. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
  59. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
  60. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
  61. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
  62. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
  63. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
  67. package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
  68. package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
  69. package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
  70. package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
  71. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
  72. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
  73. package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
  74. package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
  75. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
  76. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
  77. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
  78. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
  79. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
  80. package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
  81. package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
  82. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
  83. package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
  84. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
  85. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
  86. package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
  87. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
  88. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
  89. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
  90. package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
  91. package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
  92. package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
  93. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
  94. package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
  95. package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
  96. package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
  97. package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
  98. package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
  99. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
  105. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
  106. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
  107. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
  108. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
  109. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
  110. package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
  111. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
  112. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
  113. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
  114. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
  115. package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
  116. package/src/blocks/marketing/banners/banner-1.tsx +188 -0
  117. package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
  118. package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
  119. package/src/blocks/marketing/features/feature-1.tsx +97 -0
  120. package/src/blocks/marketing/features/feature-2.tsx +82 -0
  121. package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
  122. package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
  123. package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
  124. package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
  125. package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
  126. package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
  127. package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
  128. package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
  129. package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
  130. package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
  131. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
  132. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
  133. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
  134. package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
  135. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
  136. package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
  137. package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
  138. package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
  139. package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
  140. package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
  141. package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
  142. package/src/blocks/shared/reaction-toggle.tsx +145 -0
  143. package/src/hooks/use-mobile.ts +19 -0
  144. package/src/index.ts +200 -0
  145. package/src/lib/contracts.test.ts +32 -0
  146. package/src/lib/contracts.ts +235 -0
  147. package/src/lib/types/menu.ts +43 -0
  148. package/src/lib/types.ts +4 -0
  149. package/src/lib/utils.ts +10 -0
  150. package/src/ui/accordion.tsx +65 -0
  151. package/src/ui/avatar.tsx +108 -0
  152. package/src/ui/badge.tsx +47 -0
  153. package/src/ui/breadcrumb.tsx +108 -0
  154. package/src/ui/button-group.tsx +83 -0
  155. package/src/ui/button.tsx +63 -0
  156. package/src/ui/card.tsx +91 -0
  157. package/src/ui/carousel.tsx +241 -0
  158. package/src/ui/dialog.tsx +158 -0
  159. package/src/ui/drawer.tsx +134 -0
  160. package/src/ui/dropdown-menu.tsx +256 -0
  161. package/src/ui/empty.tsx +103 -0
  162. package/src/ui/field.tsx +248 -0
  163. package/src/ui/input-group.tsx +170 -0
  164. package/src/ui/input-otp.tsx +76 -0
  165. package/src/ui/input.tsx +20 -0
  166. package/src/ui/label.tsx +23 -0
  167. package/src/ui/navigation-menu.tsx +167 -0
  168. package/src/ui/pagination.tsx +127 -0
  169. package/src/ui/progress.tsx +30 -0
  170. package/src/ui/radio-group.tsx +44 -0
  171. package/src/ui/select.tsx +189 -0
  172. package/src/ui/separator.tsx +27 -0
  173. package/src/ui/sheet.tsx +142 -0
  174. package/src/ui/sidebar.tsx +726 -0
  175. package/src/ui/skeleton.tsx +13 -0
  176. package/src/ui/table.tsx +115 -0
  177. package/src/ui/tabs.tsx +90 -0
  178. package/src/ui/textarea.tsx +17 -0
  179. package/src/ui/toggle-group.tsx +83 -0
  180. package/src/ui/toggle.tsx +46 -0
  181. package/src/ui/tooltip.tsx +56 -0
  182. package/tsconfig.json +20 -0
  183. package/tsup.config.ts +16 -0
  184. package/vitest.config.ts +9 -0
@@ -0,0 +1,260 @@
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 &quot;{searchQuery}&quot;</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
@@ -0,0 +1,179 @@
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
+ CategoryFilter2Color,
13
+ CategoryFilter2PriceRange,
14
+ } from './category-filter-2'
15
+
16
+ interface CategoryFilter2FilterPanelProps {
17
+ selectedText?: string
18
+ clearAllLabel?: string
19
+ priceRangeTitle?: string
20
+ priceRange: number[]
21
+ priceRangeConfig: CategoryFilter2PriceRange
22
+ minLabel?: string
23
+ maxLabel?: string
24
+ categoriesTitle?: string
25
+ categories: CategoryFilter2Category[]
26
+ selectedCategories: string[]
27
+ onCategoryChange: (category: string, checked: boolean) => void
28
+ onPriceChange: (range: number[]) => void
29
+ sizesTitle?: string
30
+ sizes: string[]
31
+ selectedSize: string | null
32
+ onSizeChange: (size: string | null) => void
33
+ colorsTitle?: string
34
+ colors: CategoryFilter2Color[]
35
+ selectedColor: string | null
36
+ onColorChange: (color: string | null) => void
37
+ onClearAll: () => void
38
+ }
39
+
40
+ export function CategoryFilter2FilterPanel({
41
+ selectedText = 'Filtros',
42
+ clearAllLabel = 'Restablecer',
43
+ priceRangeTitle = 'Rango de precio',
44
+ priceRange,
45
+ priceRangeConfig,
46
+ minLabel = 'Mínimo',
47
+ maxLabel = 'Máximo',
48
+ categoriesTitle = 'Categoría',
49
+ categories,
50
+ selectedCategories,
51
+ onCategoryChange,
52
+ onPriceChange,
53
+ sizesTitle = 'Tamaño',
54
+ sizes,
55
+ selectedSize,
56
+ onSizeChange,
57
+ colorsTitle = 'Color',
58
+ colors,
59
+ selectedColor,
60
+ onColorChange,
61
+ onClearAll,
62
+ }: CategoryFilter2FilterPanelProps) {
63
+ const priceInputId = useId()
64
+ const categoryInputId = useId()
65
+
66
+ return (
67
+ <>
68
+ <div className="flex flex-col gap-6">
69
+ <div className="mb-3 flex items-center justify-between">
70
+ <h1 className="font-semibold">{selectedText}</h1>
71
+ <Button variant="ghost" size="sm" className="h-8 cursor-pointer px-2 text-xs" onClick={onClearAll}>
72
+ {clearAllLabel}
73
+ </Button>
74
+ </div>
75
+ </div>
76
+
77
+ <div className="mt-4 flex flex-col gap-4">
78
+ <CardTitle className="font-semibold">{priceRangeTitle}</CardTitle>
79
+ <div className="flex items-center gap-3">
80
+ <div className="flex flex-1 flex-col gap-1">
81
+ <label htmlFor={`${priceInputId}-min`}>{minLabel}</label>
82
+ <Input
83
+ id={`${priceInputId}-min`}
84
+ type="number"
85
+ value={priceRange[0]}
86
+ onChange={e => {
87
+ const value = parseInt(e.target.value)
88
+ if (!isNaN(value) && value >= priceRangeConfig.min) {
89
+ onPriceChange([value, priceRange[1]])
90
+ }
91
+ }}
92
+ min={priceRangeConfig.min}
93
+ max={priceRange[1]}
94
+ step={priceRangeConfig.step}
95
+ className="h-9"
96
+ />
97
+ </div>
98
+ <div className="mt-5 flex-none">-</div>
99
+ <div className="flex flex-1 flex-col gap-1">
100
+ <label htmlFor={`${priceInputId}-max`}>{maxLabel}</label>
101
+ <Input
102
+ id={`${priceInputId}-max`}
103
+ type="number"
104
+ value={priceRange[1]}
105
+ onChange={e => {
106
+ const value = parseInt(e.target.value)
107
+ if (!isNaN(value) && value <= priceRangeConfig.max) {
108
+ onPriceChange([priceRange[0], value])
109
+ }
110
+ }}
111
+ min={priceRange[0]}
112
+ max={priceRangeConfig.max}
113
+ step={priceRangeConfig.step}
114
+ className="h-9"
115
+ />
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="mt-4 flex flex-col gap-4">
121
+ <CardTitle className="font-semibold">{categoriesTitle}</CardTitle>
122
+ <div className="flex flex-col gap-2">
123
+ {categories.map(category => (
124
+ <div key={category.id} className="flex items-center gap-2">
125
+ <CheckboxCheck
126
+ id={`${categoryInputId}-${category.id}`}
127
+ checked={selectedCategories.includes(category.id)}
128
+ onCheckedChange={checked => onCategoryChange(category.id, checked as boolean)}
129
+ />
130
+ <label htmlFor={`${categoryInputId}-${category.id}`} className="text-sm">
131
+ {category.label}
132
+ </label>
133
+ </div>
134
+ ))}
135
+ </div>
136
+ </div>
137
+
138
+ <div className="mt-4 flex flex-col gap-4">
139
+ <CardTitle className="font-semibold">{sizesTitle}</CardTitle>
140
+ <div className="flex flex-wrap gap-2">
141
+ {sizes.map(size => (
142
+ <Button
143
+ key={size}
144
+ variant={selectedSize === size ? 'default' : 'outline'}
145
+ size="sm"
146
+ className="h-8 cursor-pointer px-3 text-xs"
147
+ onClick={() => onSizeChange(size === selectedSize ? null : size)}
148
+ aria-pressed={selectedSize === size}
149
+ >
150
+ {size}
151
+ </Button>
152
+ ))}
153
+ </div>
154
+ </div>
155
+
156
+ <div className="mt-4 flex flex-col gap-4">
157
+ <CardTitle className="font-semibold">{colorsTitle}</CardTitle>
158
+ <div className="flex flex-wrap gap-2">
159
+ {colors.map(color => (
160
+ <Button
161
+ key={color.id}
162
+ onClick={() => onColorChange(color.id === selectedColor ? null : color.id)}
163
+ className={`relative size-8 cursor-pointer rounded-sm transition-transform duration-200 ${
164
+ selectedColor === color.id ? 'ring-primary scale-105 ring-2' : 'hover:ring-foreground/20 hover:ring-1'
165
+ } ${color.id === 'white' ? 'border-border border' : ''}`}
166
+ style={{ backgroundColor: color.color }}
167
+ aria-pressed={selectedColor === color.id}
168
+ aria-label={color.id}
169
+ >
170
+ {selectedColor === color.id && <div className="border-background absolute inset-0 rounded-sm border-2" />}
171
+ </Button>
172
+ ))}
173
+ </div>
174
+ </div>
175
+ </>
176
+ )
177
+ }
178
+
179
+ export default CategoryFilter2FilterPanel