@thorprovider/blocks 0.1.3 → 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.
Files changed (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. 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
- }