@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,57 +0,0 @@
|
|
|
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
|
|
@@ -1,124 +0,0 @@
|
|
|
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
|
|
@@ -1,170 +0,0 @@
|
|
|
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
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
CategoryFilter4Facets,
|
|
5
|
-
CategoryFilter4FilterState,
|
|
6
|
-
} from './category-filter-4'
|
|
7
|
-
import { CategoryFilter4Fields } from './category-filter-4-fields'
|
|
8
|
-
|
|
9
|
-
interface CategoryFilter4SidebarProps {
|
|
10
|
-
filters: CategoryFilter4FilterState
|
|
11
|
-
onChange: (next: CategoryFilter4FilterState) => void
|
|
12
|
-
countWith: (partial: Partial<CategoryFilter4FilterState>) => number
|
|
13
|
-
facets: CategoryFilter4Facets
|
|
14
|
-
colourNames: Record<string, string>
|
|
15
|
-
modelTypeLabel?: string
|
|
16
|
-
rangeLabel?: string
|
|
17
|
-
frameSizeLabel?: string
|
|
18
|
-
colourLabel?: string
|
|
19
|
-
featuresLabel?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function CategoryFilter4Sidebar({
|
|
23
|
-
filters,
|
|
24
|
-
onChange,
|
|
25
|
-
countWith,
|
|
26
|
-
facets,
|
|
27
|
-
colourNames,
|
|
28
|
-
modelTypeLabel,
|
|
29
|
-
rangeLabel,
|
|
30
|
-
frameSizeLabel,
|
|
31
|
-
colourLabel,
|
|
32
|
-
featuresLabel,
|
|
33
|
-
}: CategoryFilter4SidebarProps) {
|
|
34
|
-
return (
|
|
35
|
-
<aside className="w-70 shrink-0">
|
|
36
|
-
<CategoryFilter4Fields
|
|
37
|
-
filters={filters}
|
|
38
|
-
onChange={onChange}
|
|
39
|
-
countWith={countWith}
|
|
40
|
-
facets={facets}
|
|
41
|
-
colourNames={colourNames}
|
|
42
|
-
modelTypeLabel={modelTypeLabel}
|
|
43
|
-
rangeLabel={rangeLabel}
|
|
44
|
-
frameSizeLabel={frameSizeLabel}
|
|
45
|
-
colourLabel={colourLabel}
|
|
46
|
-
featuresLabel={featuresLabel}
|
|
47
|
-
/>
|
|
48
|
-
</aside>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default CategoryFilter4Sidebar
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '../../../ui/drawer'
|
|
4
|
-
import { Label } from '../../../ui/label'
|
|
5
|
-
import { RadioGroup, RadioGroupItem } from '../../../ui/radio-group'
|
|
6
|
-
import type { CategoryFilter4SortOption } from './category-filter-4'
|
|
7
|
-
|
|
8
|
-
interface CategoryFilter4SortSheetProps {
|
|
9
|
-
open: boolean
|
|
10
|
-
onOpenChange: (open: boolean) => void
|
|
11
|
-
sort: string
|
|
12
|
-
onSortChange: (value: string) => void
|
|
13
|
-
sortOptions: CategoryFilter4SortOption[]
|
|
14
|
-
sortByLabel?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function CategoryFilter4SortSheet({
|
|
18
|
-
open,
|
|
19
|
-
onOpenChange,
|
|
20
|
-
sort,
|
|
21
|
-
onSortChange,
|
|
22
|
-
sortOptions,
|
|
23
|
-
sortByLabel = 'Ordenar por',
|
|
24
|
-
}: CategoryFilter4SortSheetProps) {
|
|
25
|
-
return (
|
|
26
|
-
<Drawer open={open} onOpenChange={onOpenChange}>
|
|
27
|
-
<DrawerContent>
|
|
28
|
-
<DrawerHeader>
|
|
29
|
-
<DrawerTitle>{sortByLabel}</DrawerTitle>
|
|
30
|
-
</DrawerHeader>
|
|
31
|
-
<div className="px-4 pb-6">
|
|
32
|
-
<RadioGroup
|
|
33
|
-
value={sort}
|
|
34
|
-
onValueChange={value => {
|
|
35
|
-
if (typeof value === 'string') {
|
|
36
|
-
onSortChange(value)
|
|
37
|
-
onOpenChange(false)
|
|
38
|
-
}
|
|
39
|
-
}}
|
|
40
|
-
className="gap-3"
|
|
41
|
-
>
|
|
42
|
-
{sortOptions.map(option => (
|
|
43
|
-
<div key={option.value} className="flex items-center gap-2">
|
|
44
|
-
<RadioGroupItem value={option.value} id={`sort-${option.value}`} />
|
|
45
|
-
<Label htmlFor={`sort-${option.value}`} className="min-h-11 font-normal">
|
|
46
|
-
{option.label}
|
|
47
|
-
</Label>
|
|
48
|
-
</div>
|
|
49
|
-
))}
|
|
50
|
-
</RadioGroup>
|
|
51
|
-
</div>
|
|
52
|
-
</DrawerContent>
|
|
53
|
-
</Drawer>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export default CategoryFilter4SortSheet
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useMemo, useState } from 'react'
|
|
4
|
-
import type {
|
|
5
|
-
CategoryFilter4Facets,
|
|
6
|
-
CategoryFilter4FilterState,
|
|
7
|
-
CategoryFilter4Product,
|
|
8
|
-
CategoryFilter4RangeBand,
|
|
9
|
-
} from './category-filter-4'
|
|
10
|
-
|
|
11
|
-
export function emptyFilters(maxPrice: number): CategoryFilter4FilterState {
|
|
12
|
-
return {
|
|
13
|
-
modelTypes: [],
|
|
14
|
-
priceMax: maxPrice,
|
|
15
|
-
rangeBands: [],
|
|
16
|
-
frameSizes: [],
|
|
17
|
-
colourways: [],
|
|
18
|
-
features: [],
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function matchesFilters(
|
|
23
|
-
product: CategoryFilter4Product,
|
|
24
|
-
filters: CategoryFilter4FilterState,
|
|
25
|
-
rangeBands: CategoryFilter4RangeBand[]
|
|
26
|
-
): boolean {
|
|
27
|
-
if (filters.modelTypes.length > 0 && !filters.modelTypes.includes(product.modelType)) return false
|
|
28
|
-
if (product.price > filters.priceMax) return false
|
|
29
|
-
if (filters.rangeBands.length > 0) {
|
|
30
|
-
const inBand = filters.rangeBands.some(value => {
|
|
31
|
-
const band = rangeBands.find(b => b.value === value)
|
|
32
|
-
return band && product.rangeKm >= band.min && product.rangeKm < band.max
|
|
33
|
-
})
|
|
34
|
-
if (!inBand) return false
|
|
35
|
-
}
|
|
36
|
-
if (filters.frameSizes.length > 0 && !filters.frameSizes.includes(product.frameSize)) return false
|
|
37
|
-
if (filters.colourways.length > 0 && !filters.colourways.includes(product.colourway)) return false
|
|
38
|
-
if (filters.features.length > 0 && !filters.features.every(f => product.features.includes(f))) return false
|
|
39
|
-
return true
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The shared filter hook — counts are derived from the catalogue by this
|
|
43
|
-
* predicate, never typed, so an option count can never disagree with the grid. */
|
|
44
|
-
export function useCategoryFilter4State(
|
|
45
|
-
products: CategoryFilter4Product[],
|
|
46
|
-
facets: CategoryFilter4Facets,
|
|
47
|
-
maxPrice: number
|
|
48
|
-
) {
|
|
49
|
-
const [filters, setFilters] = useState<CategoryFilter4FilterState>(() => emptyFilters(maxPrice))
|
|
50
|
-
const [sort, setSort] = useState(() => facets.sortOptions[0]?.value ?? 'newest')
|
|
51
|
-
|
|
52
|
-
const filtered = useMemo(() => {
|
|
53
|
-
const matches = products.filter(product => matchesFilters(product, filters, facets.rangeBands))
|
|
54
|
-
const sorted = [...matches]
|
|
55
|
-
if (sort === 'price-asc') sorted.sort((a, b) => a.price - b.price)
|
|
56
|
-
if (sort === 'price-desc') sorted.sort((a, b) => b.price - a.price)
|
|
57
|
-
if (sort === 'range') sorted.sort((a, b) => b.rangeKm - a.rangeKm)
|
|
58
|
-
if (sort === 'rating') sorted.sort((a, b) => b.rating - a.rating)
|
|
59
|
-
return sorted
|
|
60
|
-
}, [products, filters, sort, facets.rangeBands])
|
|
61
|
-
|
|
62
|
-
function countWith(partial: Partial<CategoryFilter4FilterState>): number {
|
|
63
|
-
const next = { ...filters, ...partial }
|
|
64
|
-
return products.filter(product => matchesFilters(product, next, facets.rangeBands)).length
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return { filters, setFilters, sort, setSort, filtered, countWith }
|
|
68
|
-
}
|