@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.
- package/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { SlidersHorizontal } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import { Button } from '../../../ui/button'
|
|
7
|
+
import {
|
|
8
|
+
Pagination,
|
|
9
|
+
PaginationContent,
|
|
10
|
+
PaginationEllipsis,
|
|
11
|
+
PaginationItem,
|
|
12
|
+
PaginationLink,
|
|
13
|
+
} from '../../../ui/pagination'
|
|
14
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select'
|
|
15
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '../../../ui/sheet'
|
|
16
|
+
import { cn } from '../../../lib/utils'
|
|
17
|
+
|
|
18
|
+
import { CategoryFilter2FilterPanel } from './category-filter-2-filter-panel'
|
|
19
|
+
|
|
20
|
+
export interface FilterProduct {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
price: string
|
|
24
|
+
image: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CategoryFilter2Category {
|
|
28
|
+
id: string
|
|
29
|
+
label: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CategoryFilter2Color {
|
|
33
|
+
id: string
|
|
34
|
+
color: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CategoryFilter2PriceRange {
|
|
38
|
+
min: number
|
|
39
|
+
max: number
|
|
40
|
+
step: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CategoryFilter2SortOption {
|
|
44
|
+
value: string
|
|
45
|
+
label: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CategoryFilter2Filters {
|
|
49
|
+
categories: string[]
|
|
50
|
+
priceRange: number[]
|
|
51
|
+
size: string | null
|
|
52
|
+
color: string | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CategoryFilter2Props {
|
|
56
|
+
products: FilterProduct[]
|
|
57
|
+
categories?: CategoryFilter2Category[]
|
|
58
|
+
sizes?: string[]
|
|
59
|
+
colors?: CategoryFilter2Color[]
|
|
60
|
+
priceRange?: CategoryFilter2PriceRange
|
|
61
|
+
viewOptions?: (number | string)[]
|
|
62
|
+
sortOptions?: CategoryFilter2SortOption[]
|
|
63
|
+
pagination?: (number | string)[]
|
|
64
|
+
filtersTitle?: string
|
|
65
|
+
selectedText?: string
|
|
66
|
+
clearAllLabel?: string
|
|
67
|
+
priceRangeTitle?: string
|
|
68
|
+
minLabel?: string
|
|
69
|
+
maxLabel?: string
|
|
70
|
+
categoriesTitle?: string
|
|
71
|
+
sizesTitle?: string
|
|
72
|
+
colorsTitle?: string
|
|
73
|
+
viewLabel?: string
|
|
74
|
+
viewPlaceholder?: string
|
|
75
|
+
sortPlaceholder?: string
|
|
76
|
+
productsTitle?: string
|
|
77
|
+
currentLabel?: string
|
|
78
|
+
onProductClick?: (product: FilterProduct) => void
|
|
79
|
+
onPageChange?: (page: number | string) => void
|
|
80
|
+
onViewChange?: (view: number | string) => void
|
|
81
|
+
onSortChange?: (sort: string) => void
|
|
82
|
+
onFilterChange?: (filters: CategoryFilter2Filters) => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DEFAULT_VIEW_OPTIONS: (number | string)[] = [10, 25, 50, 100]
|
|
86
|
+
const DEFAULT_SORT_OPTIONS: CategoryFilter2SortOption[] = [
|
|
87
|
+
{ value: 'newest', label: 'Más recientes' },
|
|
88
|
+
{ value: 'price-asc', label: 'Precio: de menor a mayor' },
|
|
89
|
+
{ value: 'price-desc', label: 'Precio: de mayor a menor' },
|
|
90
|
+
]
|
|
91
|
+
const DEFAULT_PRICE_RANGE: CategoryFilter2PriceRange = { min: 0, max: 1000, step: 10 }
|
|
92
|
+
|
|
93
|
+
export function CategoryFilter2({
|
|
94
|
+
products,
|
|
95
|
+
categories = [],
|
|
96
|
+
sizes = [],
|
|
97
|
+
colors = [],
|
|
98
|
+
priceRange: priceRangeConfig = DEFAULT_PRICE_RANGE,
|
|
99
|
+
viewOptions = DEFAULT_VIEW_OPTIONS,
|
|
100
|
+
sortOptions = DEFAULT_SORT_OPTIONS,
|
|
101
|
+
pagination = [],
|
|
102
|
+
filtersTitle = 'Filtros',
|
|
103
|
+
selectedText = 'Filtros',
|
|
104
|
+
clearAllLabel = 'Restablecer',
|
|
105
|
+
priceRangeTitle = 'Rango de precio',
|
|
106
|
+
minLabel = 'Mínimo',
|
|
107
|
+
maxLabel = 'Máximo',
|
|
108
|
+
categoriesTitle = 'Categoría',
|
|
109
|
+
sizesTitle = 'Tamaño',
|
|
110
|
+
colorsTitle = 'Color',
|
|
111
|
+
viewLabel = 'Ver',
|
|
112
|
+
viewPlaceholder = 'Seleccionar',
|
|
113
|
+
sortPlaceholder = 'Ordenar por',
|
|
114
|
+
productsTitle = 'Productos',
|
|
115
|
+
currentLabel = '(actual)',
|
|
116
|
+
onProductClick,
|
|
117
|
+
onPageChange,
|
|
118
|
+
onViewChange,
|
|
119
|
+
onSortChange,
|
|
120
|
+
onFilterChange,
|
|
121
|
+
}: CategoryFilter2Props) {
|
|
122
|
+
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
|
123
|
+
const [priceRange, setPriceRange] = useState<number[]>([priceRangeConfig.min, priceRangeConfig.max])
|
|
124
|
+
const [selectedSize, setSelectedSize] = useState<string | null>(null)
|
|
125
|
+
const [selectedColor, setSelectedColor] = useState<string | null>(null)
|
|
126
|
+
const [selectedView, setSelectedView] = useState<number | string>(viewOptions[0] ?? 10)
|
|
127
|
+
const [selectedSort, setSelectedSort] = useState<string>(sortOptions[0]?.value ?? 'newest')
|
|
128
|
+
const [activePage, setActivePage] = useState<number | string | null>(pagination.length > 0 ? pagination[0] : null)
|
|
129
|
+
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
|
130
|
+
|
|
131
|
+
// Close sheet when screen size becomes large using CSS media query
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const mediaQuery = window.matchMedia('(min-width: 1024px)')
|
|
134
|
+
|
|
135
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
136
|
+
if (e.matches) {
|
|
137
|
+
setIsSheetOpen(false)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
mediaQuery.addEventListener('change', handleChange)
|
|
142
|
+
return () => mediaQuery.removeEventListener('change', handleChange)
|
|
143
|
+
}, [])
|
|
144
|
+
|
|
145
|
+
const handleCategoryChange = (category: string, checked: boolean) => {
|
|
146
|
+
const next = checked ? [...selectedCategories, category] : selectedCategories.filter(c => c !== category)
|
|
147
|
+
setSelectedCategories(next)
|
|
148
|
+
onFilterChange?.({ categories: next, priceRange, size: selectedSize, color: selectedColor })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const handlePriceChange = (next: number[]) => {
|
|
152
|
+
setPriceRange(next)
|
|
153
|
+
onFilterChange?.({ categories: selectedCategories, priceRange: next, size: selectedSize, color: selectedColor })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const handleSizeChange = (size: string | null) => {
|
|
157
|
+
setSelectedSize(size)
|
|
158
|
+
onFilterChange?.({ categories: selectedCategories, priceRange, size, color: selectedColor })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const handleColorChange = (color: string | null) => {
|
|
162
|
+
setSelectedColor(color)
|
|
163
|
+
onFilterChange?.({ categories: selectedCategories, priceRange, size: selectedSize, color })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const handleClearAll = () => {
|
|
167
|
+
setSelectedCategories([])
|
|
168
|
+
setSelectedSize(null)
|
|
169
|
+
setSelectedColor(null)
|
|
170
|
+
setPriceRange([priceRangeConfig.min, priceRangeConfig.max])
|
|
171
|
+
onFilterChange?.({ categories: [], priceRange: [priceRangeConfig.min, priceRangeConfig.max], size: null, color: null })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleViewChange = (view: number | string) => {
|
|
175
|
+
setSelectedView(view)
|
|
176
|
+
onViewChange?.(view)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const handleSortChange = (sort: string) => {
|
|
180
|
+
setSelectedSort(sort)
|
|
181
|
+
onSortChange?.(sort)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const handlePageChange = (page: number | string) => {
|
|
185
|
+
if (page === '...') return
|
|
186
|
+
setActivePage(page)
|
|
187
|
+
onPageChange?.(page)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const filterPanelProps = {
|
|
191
|
+
selectedText,
|
|
192
|
+
clearAllLabel,
|
|
193
|
+
priceRangeTitle,
|
|
194
|
+
priceRange,
|
|
195
|
+
priceRangeConfig,
|
|
196
|
+
minLabel,
|
|
197
|
+
maxLabel,
|
|
198
|
+
categoriesTitle,
|
|
199
|
+
categories,
|
|
200
|
+
selectedCategories,
|
|
201
|
+
onCategoryChange: handleCategoryChange,
|
|
202
|
+
onPriceChange: handlePriceChange,
|
|
203
|
+
sizesTitle,
|
|
204
|
+
sizes,
|
|
205
|
+
selectedSize,
|
|
206
|
+
onSizeChange: handleSizeChange,
|
|
207
|
+
colorsTitle,
|
|
208
|
+
colors,
|
|
209
|
+
selectedColor,
|
|
210
|
+
onColorChange: handleColorChange,
|
|
211
|
+
onClearAll: handleClearAll,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div className="mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
216
|
+
<div className="flex flex-col gap-8 lg:flex-row">
|
|
217
|
+
<aside className="hidden w-full flex-none lg:block lg:w-72">
|
|
218
|
+
<div className="max-h-100vh sticky top-4 overflow-y-auto">
|
|
219
|
+
<div className="flex flex-col gap-6 pb-6">
|
|
220
|
+
<CategoryFilter2FilterPanel {...filterPanelProps} />
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</aside>
|
|
224
|
+
|
|
225
|
+
<main className="flex min-w-0 flex-1 flex-col gap-6">
|
|
226
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
227
|
+
<div className="flex items-center gap-4">
|
|
228
|
+
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
|
229
|
+
<Button
|
|
230
|
+
variant="outline"
|
|
231
|
+
size="sm"
|
|
232
|
+
className="h-8 cursor-pointer px-3 text-xs lg:hidden"
|
|
233
|
+
onClick={() => setIsSheetOpen(true)}
|
|
234
|
+
>
|
|
235
|
+
<SlidersHorizontal />
|
|
236
|
+
{filtersTitle}
|
|
237
|
+
</Button>
|
|
238
|
+
<SheetContent side="left" className="w-full p-0 sm:w-[380px] lg:hidden">
|
|
239
|
+
<SheetHeader className="border-b px-6 py-4">
|
|
240
|
+
<SheetTitle className="text-lg">{filtersTitle}</SheetTitle>
|
|
241
|
+
</SheetHeader>
|
|
242
|
+
<div className="h-[calc(100vh-4rem)] overflow-y-auto p-6">
|
|
243
|
+
<div className="flex flex-col gap-6">
|
|
244
|
+
<CategoryFilter2FilterPanel {...filterPanelProps} />
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</SheetContent>
|
|
248
|
+
</Sheet>
|
|
249
|
+
<div className="hidden items-center gap-2 sm:flex">
|
|
250
|
+
<span className="text-sm">{viewLabel}</span>
|
|
251
|
+
<Select
|
|
252
|
+
value={String(selectedView)}
|
|
253
|
+
onValueChange={value => handleViewChange(viewOptions.find(option => String(option) === value) ?? value)}
|
|
254
|
+
>
|
|
255
|
+
<SelectTrigger className="h-8 w-[100px]" aria-label={viewLabel}>
|
|
256
|
+
<SelectValue placeholder={viewPlaceholder} />
|
|
257
|
+
</SelectTrigger>
|
|
258
|
+
<SelectContent>
|
|
259
|
+
{viewOptions.map(option => (
|
|
260
|
+
<SelectItem key={option} value={String(option)}>
|
|
261
|
+
{option}
|
|
262
|
+
</SelectItem>
|
|
263
|
+
))}
|
|
264
|
+
</SelectContent>
|
|
265
|
+
</Select>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="flex items-center gap-4">
|
|
269
|
+
<Select value={selectedSort} onValueChange={handleSortChange}>
|
|
270
|
+
<SelectTrigger className="h-8 w-[180px]" aria-label={sortPlaceholder}>
|
|
271
|
+
<SelectValue placeholder={sortPlaceholder} />
|
|
272
|
+
</SelectTrigger>
|
|
273
|
+
<SelectContent>
|
|
274
|
+
{sortOptions.map(option => (
|
|
275
|
+
<SelectItem key={option.value} value={option.value}>
|
|
276
|
+
{option.label}
|
|
277
|
+
</SelectItem>
|
|
278
|
+
))}
|
|
279
|
+
</SelectContent>
|
|
280
|
+
</Select>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<section aria-labelledby="products-heading" className="flex flex-col gap-6">
|
|
285
|
+
<h2 id="products-heading" className="sr-only">
|
|
286
|
+
{productsTitle}
|
|
287
|
+
</h2>
|
|
288
|
+
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3">
|
|
289
|
+
{products.map(product => (
|
|
290
|
+
<li key={product.id} className="group">
|
|
291
|
+
<article
|
|
292
|
+
className={cn('flex h-full flex-col gap-2', onProductClick && 'cursor-pointer')}
|
|
293
|
+
onClick={onProductClick ? () => onProductClick(product) : undefined}
|
|
294
|
+
>
|
|
295
|
+
<div className="relative aspect-3/4 overflow-hidden rounded-lg">
|
|
296
|
+
<img
|
|
297
|
+
src={product.image || undefined}
|
|
298
|
+
alt={product.name}
|
|
299
|
+
className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
300
|
+
width={300}
|
|
301
|
+
height={400}
|
|
302
|
+
loading="lazy"
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
<h3 className="text-sm font-medium text-balance">{product.name}</h3>
|
|
306
|
+
<p className="text-sm font-semibold">{product.price}</p>
|
|
307
|
+
</article>
|
|
308
|
+
</li>
|
|
309
|
+
))}
|
|
310
|
+
</ul>
|
|
311
|
+
|
|
312
|
+
{pagination.length > 0 ? (
|
|
313
|
+
<Pagination aria-label="Paginación">
|
|
314
|
+
<PaginationContent>
|
|
315
|
+
{pagination.map((page, index) =>
|
|
316
|
+
page === '...' ? (
|
|
317
|
+
<PaginationEllipsis key={`${page}-${index}`} />
|
|
318
|
+
) : (
|
|
319
|
+
<PaginationItem key={page}>
|
|
320
|
+
<PaginationLink
|
|
321
|
+
isActive={activePage === page}
|
|
322
|
+
className="size-8 cursor-pointer p-0"
|
|
323
|
+
onClick={() => handlePageChange(page)}
|
|
324
|
+
>
|
|
325
|
+
{page}
|
|
326
|
+
{activePage === page && <span className="sr-only">{currentLabel}</span>}
|
|
327
|
+
</PaginationLink>
|
|
328
|
+
</PaginationItem>
|
|
329
|
+
)
|
|
330
|
+
)}
|
|
331
|
+
</PaginationContent>
|
|
332
|
+
</Pagination>
|
|
333
|
+
) : null}
|
|
334
|
+
</section>
|
|
335
|
+
</main>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export default CategoryFilter2
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { X } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import { Badge } from '../../../ui/badge'
|
|
6
|
+
import { Button } from '../../../ui/button'
|
|
7
|
+
import type { CategoryFilter4FilterState } from './category-filter-4'
|
|
8
|
+
|
|
9
|
+
interface CategoryFilter4ChipRailProps {
|
|
10
|
+
filters: CategoryFilter4FilterState
|
|
11
|
+
onChange: (next: CategoryFilter4FilterState) => void
|
|
12
|
+
empty: CategoryFilter4FilterState
|
|
13
|
+
clearAllLabel?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function CategoryFilter4ChipRail({
|
|
17
|
+
filters,
|
|
18
|
+
onChange,
|
|
19
|
+
empty,
|
|
20
|
+
clearAllLabel = 'Borrar todos',
|
|
21
|
+
}: CategoryFilter4ChipRailProps) {
|
|
22
|
+
type Chip = { label: string; clear: () => void }
|
|
23
|
+
const chips: Chip[] = [
|
|
24
|
+
...filters.modelTypes.map(v => ({ label: v, clear: () => onChange({ ...filters, modelTypes: filters.modelTypes.filter(x => x !== v) }) })),
|
|
25
|
+
...filters.rangeBands.map(v => ({ label: v, clear: () => onChange({ ...filters, rangeBands: filters.rangeBands.filter(x => x !== v) }) })),
|
|
26
|
+
...filters.frameSizes.map(v => ({ label: `Talla ${v}`, clear: () => onChange({ ...filters, frameSizes: filters.frameSizes.filter(x => x !== v) }) })),
|
|
27
|
+
...filters.colourways.map(v => ({ label: v, clear: () => onChange({ ...filters, colourways: filters.colourways.filter(x => x !== v) }) })),
|
|
28
|
+
...filters.features.map(v => ({ label: v, clear: () => onChange({ ...filters, features: filters.features.filter(x => x !== v) }) })),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
if (filters.priceMax < empty.priceMax) {
|
|
32
|
+
chips.push({
|
|
33
|
+
label: `Menos de $${filters.priceMax.toLocaleString()}`,
|
|
34
|
+
clear: () => onChange({ ...filters, priceMax: empty.priceMax }),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (chips.length === 0) return null
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex items-center gap-1.5 overflow-x-auto pb-1" role="group" aria-label="Filtros aplicados">
|
|
42
|
+
{chips.map(chip => (
|
|
43
|
+
<Badge key={chip.label} variant="secondary" className="shrink-0 gap-1">
|
|
44
|
+
{chip.label}
|
|
45
|
+
<button type="button" aria-label={`Eliminar filtro: ${chip.label}`} onClick={chip.clear}>
|
|
46
|
+
<X className="size-3" />
|
|
47
|
+
</button>
|
|
48
|
+
</Badge>
|
|
49
|
+
))}
|
|
50
|
+
<Button variant="ghost" size="sm" className="shrink-0" onClick={() => onChange(empty)}>
|
|
51
|
+
{clearAllLabel}
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default CategoryFilter4ChipRail
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { animate, useReducedMotion } from 'motion/react'
|
|
5
|
+
|
|
6
|
+
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '../../../ui/drawer'
|
|
7
|
+
import { Button } from '../../../ui/button'
|
|
8
|
+
import type {
|
|
9
|
+
CategoryFilter4Facets,
|
|
10
|
+
CategoryFilter4FilterState,
|
|
11
|
+
} from './category-filter-4'
|
|
12
|
+
import { CategoryFilter4Fields } from './category-filter-4-fields'
|
|
13
|
+
|
|
14
|
+
interface CategoryFilter4DrawerProps {
|
|
15
|
+
open: boolean
|
|
16
|
+
onOpenChange: (open: boolean) => void
|
|
17
|
+
filters: CategoryFilter4FilterState
|
|
18
|
+
onChange: (next: CategoryFilter4FilterState) => void
|
|
19
|
+
countWith: (partial: Partial<CategoryFilter4FilterState>) => number
|
|
20
|
+
resultCount: number
|
|
21
|
+
empty: CategoryFilter4FilterState
|
|
22
|
+
facets: CategoryFilter4Facets
|
|
23
|
+
colourNames: Record<string, string>
|
|
24
|
+
filtersLabel?: string
|
|
25
|
+
modelTypeLabel?: string
|
|
26
|
+
rangeLabel?: string
|
|
27
|
+
frameSizeLabel?: string
|
|
28
|
+
colourLabel?: string
|
|
29
|
+
featuresLabel?: string
|
|
30
|
+
clearAllLabel?: string
|
|
31
|
+
showResultsLabel?: (count: number) => string
|
|
32
|
+
noResultsLabel?: string
|
|
33
|
+
noResultsDescription?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function CategoryFilter4Drawer({
|
|
37
|
+
open,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
filters,
|
|
40
|
+
onChange,
|
|
41
|
+
countWith,
|
|
42
|
+
resultCount,
|
|
43
|
+
empty,
|
|
44
|
+
facets,
|
|
45
|
+
colourNames,
|
|
46
|
+
filtersLabel = 'Filtros',
|
|
47
|
+
modelTypeLabel,
|
|
48
|
+
rangeLabel,
|
|
49
|
+
frameSizeLabel,
|
|
50
|
+
colourLabel,
|
|
51
|
+
featuresLabel,
|
|
52
|
+
clearAllLabel = 'Borrar todos',
|
|
53
|
+
showResultsLabel = count => `Mostrar ${count} ${count === 1 ? 'producto' : 'productos'}`,
|
|
54
|
+
noResultsLabel = 'Sin coincidencias',
|
|
55
|
+
noResultsDescription = 'Prueba a relajar alguno de los filtros aplicados.',
|
|
56
|
+
}: CategoryFilter4DrawerProps) {
|
|
57
|
+
const reduceMotion = useReducedMotion()
|
|
58
|
+
const [displayCount, setDisplayCount] = useState(resultCount)
|
|
59
|
+
const prevRef = useRef(resultCount)
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (reduceMotion) {
|
|
63
|
+
setDisplayCount(resultCount)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const controls = animate(prevRef.current, resultCount, { duration: 0.3, onUpdate: v => setDisplayCount(Math.round(v)) })
|
|
67
|
+
prevRef.current = resultCount
|
|
68
|
+
return () => controls.stop()
|
|
69
|
+
}, [resultCount, reduceMotion])
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
73
|
+
{/*
|
|
74
|
+
No height override. `DrawerContent` already ships `h-auto` with a `max-h-[80vh]` cap for
|
|
75
|
+
a bottom drawer, which is exactly the behaviour wanted: grow to the filters, stop before
|
|
76
|
+
swallowing the screen. Forcing `h-[88vh]` made it that tall whatever it held, so a short
|
|
77
|
+
filter set left a half-screen of blank between the last checkbox and the footer.
|
|
78
|
+
|
|
79
|
+
`min-h-0` on the scroller because a flex child defaults to `min-height: auto` — it
|
|
80
|
+
refuses to shrink below its content, so without it a tall filter set pushes the footer
|
|
81
|
+
off the bottom instead of scrolling inside.
|
|
82
|
+
*/}
|
|
83
|
+
<DrawerContent>
|
|
84
|
+
<DrawerHeader>
|
|
85
|
+
<DrawerTitle>{filtersLabel}</DrawerTitle>
|
|
86
|
+
</DrawerHeader>
|
|
87
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-4">
|
|
88
|
+
<CategoryFilter4Fields
|
|
89
|
+
filters={filters}
|
|
90
|
+
onChange={onChange}
|
|
91
|
+
countWith={countWith}
|
|
92
|
+
facets={facets}
|
|
93
|
+
colourNames={colourNames}
|
|
94
|
+
modelTypeLabel={modelTypeLabel}
|
|
95
|
+
rangeLabel={rangeLabel}
|
|
96
|
+
frameSizeLabel={frameSizeLabel}
|
|
97
|
+
colourLabel={colourLabel}
|
|
98
|
+
featuresLabel={featuresLabel}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="flex items-center gap-2 border-t p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
|
102
|
+
<Button variant="ghost" onClick={() => onChange(empty)}>
|
|
103
|
+
{clearAllLabel}
|
|
104
|
+
</Button>
|
|
105
|
+
<Button
|
|
106
|
+
className="flex-1"
|
|
107
|
+
disabled={resultCount === 0}
|
|
108
|
+
aria-describedby={resultCount === 0 ? 'category-filter-4-zero-reason' : undefined}
|
|
109
|
+
onClick={() => onOpenChange(false)}
|
|
110
|
+
>
|
|
111
|
+
{resultCount === 0 ? noResultsLabel : showResultsLabel(displayCount)}
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
{resultCount === 0 ? (
|
|
115
|
+
<p id="category-filter-4-zero-reason" className="text-destructive px-4 pb-2 text-xs">
|
|
116
|
+
{noResultsDescription}
|
|
117
|
+
</p>
|
|
118
|
+
) : null}
|
|
119
|
+
</DrawerContent>
|
|
120
|
+
</Drawer>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default CategoryFilter4Drawer
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Label } from '../../../ui/label'
|
|
4
|
+
import { CheckboxCheck } from '../checkout-forms/checkbox-check'
|
|
5
|
+
import type {
|
|
6
|
+
CategoryFilter4Facets,
|
|
7
|
+
CategoryFilter4FilterState,
|
|
8
|
+
} from './category-filter-4'
|
|
9
|
+
|
|
10
|
+
interface CategoryFilter4FieldsProps {
|
|
11
|
+
filters: CategoryFilter4FilterState
|
|
12
|
+
onChange: (next: CategoryFilter4FilterState) => void
|
|
13
|
+
countWith: (partial: Partial<CategoryFilter4FilterState>) => number
|
|
14
|
+
facets: CategoryFilter4Facets
|
|
15
|
+
colourNames: Record<string, string>
|
|
16
|
+
modelTypeLabel?: string
|
|
17
|
+
rangeLabel?: string
|
|
18
|
+
frameSizeLabel?: string
|
|
19
|
+
colourLabel?: string
|
|
20
|
+
featuresLabel?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CategoryFilter4Fields({
|
|
24
|
+
filters,
|
|
25
|
+
onChange,
|
|
26
|
+
countWith,
|
|
27
|
+
facets,
|
|
28
|
+
colourNames,
|
|
29
|
+
modelTypeLabel = 'Tipo de modelo',
|
|
30
|
+
rangeLabel = 'Autonomía',
|
|
31
|
+
frameSizeLabel = 'Talla de cuadro',
|
|
32
|
+
colourLabel = 'Color',
|
|
33
|
+
featuresLabel = 'Características',
|
|
34
|
+
}: CategoryFilter4FieldsProps) {
|
|
35
|
+
function toggle<K extends 'modelTypes' | 'rangeBands' | 'frameSizes' | 'colourways' | 'features'>(
|
|
36
|
+
key: K,
|
|
37
|
+
value: string
|
|
38
|
+
) {
|
|
39
|
+
const list = filters[key] as string[]
|
|
40
|
+
const next = list.includes(value) ? list.filter(v => v !== value) : [...list, value]
|
|
41
|
+
onChange({ ...filters, [key]: next })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const colourways = Object.keys(colourNames)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex flex-col gap-6">
|
|
48
|
+
<fieldset>
|
|
49
|
+
<legend className="mb-2 text-sm font-medium">{modelTypeLabel}</legend>
|
|
50
|
+
<div className="flex flex-col gap-2">
|
|
51
|
+
{facets.modelTypes.map(option => {
|
|
52
|
+
const count = countWith({
|
|
53
|
+
modelTypes: filters.modelTypes.includes(option.value)
|
|
54
|
+
? filters.modelTypes
|
|
55
|
+
: [...filters.modelTypes, option.value],
|
|
56
|
+
})
|
|
57
|
+
const zero = count === 0 && !filters.modelTypes.includes(option.value)
|
|
58
|
+
return (
|
|
59
|
+
<div key={option.value} className="flex items-center justify-between">
|
|
60
|
+
<div className="flex items-center gap-2">
|
|
61
|
+
<CheckboxCheck
|
|
62
|
+
id={`model-${option.value}`}
|
|
63
|
+
checked={filters.modelTypes.includes(option.value)}
|
|
64
|
+
disabled={zero}
|
|
65
|
+
onCheckedChange={() => toggle('modelTypes', option.value)}
|
|
66
|
+
/>
|
|
67
|
+
<Label htmlFor={`model-${option.value}`} className="font-normal">
|
|
68
|
+
{option.label}
|
|
69
|
+
</Label>
|
|
70
|
+
</div>
|
|
71
|
+
<span className="text-muted-foreground text-xs tabular-nums">{count}</span>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
</fieldset>
|
|
77
|
+
|
|
78
|
+
<fieldset>
|
|
79
|
+
<legend className="mb-2 text-sm font-medium">{rangeLabel}</legend>
|
|
80
|
+
<div className="flex flex-col gap-2">
|
|
81
|
+
{facets.rangeBands.map(band => {
|
|
82
|
+
const count = countWith({
|
|
83
|
+
rangeBands: filters.rangeBands.includes(band.value)
|
|
84
|
+
? filters.rangeBands
|
|
85
|
+
: [...filters.rangeBands, band.value],
|
|
86
|
+
})
|
|
87
|
+
const zero = count === 0 && !filters.rangeBands.includes(band.value)
|
|
88
|
+
return (
|
|
89
|
+
<div key={band.value} className="flex items-center justify-between">
|
|
90
|
+
<div className="flex items-center gap-2">
|
|
91
|
+
<CheckboxCheck
|
|
92
|
+
id={`range-${band.value}`}
|
|
93
|
+
checked={filters.rangeBands.includes(band.value)}
|
|
94
|
+
disabled={zero}
|
|
95
|
+
onCheckedChange={() => toggle('rangeBands', band.value)}
|
|
96
|
+
/>
|
|
97
|
+
<Label htmlFor={`range-${band.value}`} className="font-normal">
|
|
98
|
+
{band.label}
|
|
99
|
+
</Label>
|
|
100
|
+
</div>
|
|
101
|
+
<span className="text-muted-foreground text-xs tabular-nums">{count}</span>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
</fieldset>
|
|
107
|
+
|
|
108
|
+
<fieldset>
|
|
109
|
+
<legend className="mb-2 text-sm font-medium">{frameSizeLabel}</legend>
|
|
110
|
+
<div className="flex flex-wrap gap-2">
|
|
111
|
+
{facets.frameSizes.map(size => {
|
|
112
|
+
const active = filters.frameSizes.includes(size)
|
|
113
|
+
return (
|
|
114
|
+
<button
|
|
115
|
+
key={size}
|
|
116
|
+
type="button"
|
|
117
|
+
aria-pressed={active}
|
|
118
|
+
onClick={() => toggle('frameSizes', size)}
|
|
119
|
+
className="focus-visible:ring-ring/50 min-h-11 min-w-11 rounded-full border px-3 text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none aria-pressed:border-primary aria-pressed:bg-primary aria-pressed:text-primary-foreground border-input hover:bg-accent/50"
|
|
120
|
+
>
|
|
121
|
+
{size}
|
|
122
|
+
</button>
|
|
123
|
+
)
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
</fieldset>
|
|
127
|
+
|
|
128
|
+
<fieldset>
|
|
129
|
+
<legend className="mb-2 text-sm font-medium">{colourLabel}</legend>
|
|
130
|
+
<div className="flex flex-wrap gap-2">
|
|
131
|
+
{colourways.map(colour => {
|
|
132
|
+
const active = filters.colourways.includes(colour)
|
|
133
|
+
return (
|
|
134
|
+
<button
|
|
135
|
+
key={colour}
|
|
136
|
+
type="button"
|
|
137
|
+
aria-pressed={active}
|
|
138
|
+
onClick={() => toggle('colourways', colour)}
|
|
139
|
+
className="focus-visible:ring-ring/50 flex min-h-11 items-center gap-1.5 rounded-full border px-3 text-sm transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none aria-pressed:border-primary aria-pressed:bg-primary/10 border-input hover:bg-accent/50"
|
|
140
|
+
>
|
|
141
|
+
<span aria-hidden="true" className="bg-muted-foreground/30 size-3 rounded-full" />
|
|
142
|
+
{colourNames[colour]}
|
|
143
|
+
</button>
|
|
144
|
+
)
|
|
145
|
+
})}
|
|
146
|
+
</div>
|
|
147
|
+
</fieldset>
|
|
148
|
+
|
|
149
|
+
<fieldset>
|
|
150
|
+
<legend className="mb-2 text-sm font-medium">{featuresLabel}</legend>
|
|
151
|
+
<div className="flex flex-col gap-2">
|
|
152
|
+
{facets.featureOptions.map(option => (
|
|
153
|
+
<div key={option.value} className="flex items-center gap-2">
|
|
154
|
+
<CheckboxCheck
|
|
155
|
+
id={`feature-${option.value}`}
|
|
156
|
+
checked={filters.features.includes(option.value)}
|
|
157
|
+
onCheckedChange={() => toggle('features', option.value)}
|
|
158
|
+
/>
|
|
159
|
+
<Label htmlFor={`feature-${option.value}`} className="font-normal">
|
|
160
|
+
{option.label}
|
|
161
|
+
</Label>
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
</fieldset>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default CategoryFilter4Fields
|