@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,298 +0,0 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import { SlidersHorizontal } from 'lucide-react'
5
-
6
- import { Badge } from '../../../ui/badge'
7
- import { Button } from '../../../ui/button'
8
- import {
9
- Empty,
10
- EmptyContent,
11
- EmptyDescription,
12
- EmptyHeader,
13
- EmptyTitle,
14
- } from '../../../ui/empty'
15
-
16
- import { CategoryFilter4ChipRail } from './category-filter-4-chip-rail'
17
- import { CategoryFilter4Drawer } from './category-filter-4-drawer'
18
- import { CategoryFilter4Sidebar } from './category-filter-4-sidebar'
19
- import { CategoryFilter4SortSheet } from './category-filter-4-sort-sheet'
20
- import { emptyFilters, useCategoryFilter4State } from './category-filter-4-state'
21
-
22
- export interface CategoryFilter4SortOption {
23
- value: string
24
- label: string
25
- }
26
-
27
- export interface CategoryFilter4FacetOption {
28
- value: string
29
- label: string
30
- }
31
-
32
- export interface CategoryFilter4RangeBand {
33
- value: string
34
- label: string
35
- min: number
36
- max: number
37
- }
38
-
39
- export interface CategoryFilter4Facets {
40
- sortOptions: CategoryFilter4SortOption[]
41
- frameSizes: string[]
42
- modelTypes: CategoryFilter4FacetOption[]
43
- rangeBands: CategoryFilter4RangeBand[]
44
- featureOptions: CategoryFilter4FacetOption[]
45
- }
46
-
47
- export interface CategoryFilter4FilterState {
48
- modelTypes: string[]
49
- priceMax: number
50
- rangeBands: string[]
51
- frameSizes: string[]
52
- colourways: string[]
53
- features: string[]
54
- }
55
-
56
- export interface CategoryFilter4Product {
57
- id: string
58
- name: string
59
- modelType: string
60
- price: number
61
- rangeKm: number
62
- frameSize: string
63
- colourway: string
64
- features: string[]
65
- rating: number
66
- }
67
-
68
- export interface CategoryFilter4Props {
69
- products: CategoryFilter4Product[]
70
- facets: CategoryFilter4Facets
71
- colourNames: Record<string, string>
72
- fallbackImage: string
73
- maxPrice: number
74
- filtersLabel?: string
75
- sortLabel?: string
76
- sortByLabel?: string
77
- modelTypeLabel?: string
78
- rangeLabel?: string
79
- frameSizeLabel?: string
80
- colourLabel?: string
81
- featuresLabel?: string
82
- resultsCountLabel?: (count: number) => string
83
- clearAllLabel?: string
84
- showResultsLabel?: (count: number) => string
85
- noResultsLabel?: string
86
- noResultsTitle?: string
87
- noResultsDescription?: string
88
- noResultsCtaLabel?: string
89
- onFiltersChange?: (filters: CategoryFilter4FilterState) => void
90
- onSortChange?: (sort: string) => void
91
- onProductSelect?: (product: CategoryFilter4Product) => void
92
- }
93
-
94
- function activeFilterCount(
95
- filters: CategoryFilter4FilterState,
96
- empty: CategoryFilter4FilterState
97
- ): number {
98
- return (
99
- filters.modelTypes.length +
100
- filters.rangeBands.length +
101
- filters.frameSizes.length +
102
- filters.colourways.length +
103
- filters.features.length +
104
- (filters.priceMax < empty.priceMax ? 1 : 0)
105
- )
106
- }
107
-
108
- export function CategoryFilter4({
109
- products,
110
- facets,
111
- colourNames,
112
- fallbackImage,
113
- maxPrice,
114
- filtersLabel = 'Filtros',
115
- sortLabel = 'Ordenar',
116
- sortByLabel = 'Ordenar por',
117
- modelTypeLabel = 'Tipo de modelo',
118
- rangeLabel = 'Autonomía',
119
- frameSizeLabel = 'Talla de cuadro',
120
- colourLabel = 'Color',
121
- featuresLabel = 'Características',
122
- resultsCountLabel = count => `${count} ${count === 1 ? 'producto' : 'productos'}`,
123
- clearAllLabel = 'Borrar todos',
124
- showResultsLabel = count => `Mostrar ${count} ${count === 1 ? 'producto' : 'productos'}`,
125
- noResultsLabel = 'Sin coincidencias',
126
- noResultsTitle = 'No hay productos que coincidan con estos filtros',
127
- noResultsDescription = 'Prueba a relajar alguno de los filtros aplicados.',
128
- noResultsCtaLabel = 'Borrar todos los filtros',
129
- onFiltersChange,
130
- onSortChange,
131
- onProductSelect,
132
- }: CategoryFilter4Props) {
133
- const empty = emptyFilters(maxPrice)
134
- const { filters, setFilters, sort, setSort, filtered, countWith } = useCategoryFilter4State(
135
- products,
136
- facets,
137
- maxPrice
138
- )
139
- const [drawerOpen, setDrawerOpen] = useState(false)
140
- const [sortOpen, setSortOpen] = useState(false)
141
-
142
- const handleFiltersChange = (next: CategoryFilter4FilterState) => {
143
- setFilters(next)
144
- onFiltersChange?.(next)
145
- }
146
-
147
- const handleSortChange = (next: string) => {
148
- setSort(next)
149
- onSortChange?.(next)
150
- }
151
-
152
- const count = activeFilterCount(filters, empty)
153
- const selectedSortLabel = facets.sortOptions.find(option => option.value === sort)?.label ?? sortByLabel
154
-
155
- return (
156
- <div className="w-full">
157
- <div className="bg-background/95 sticky top-0 z-10 flex items-center justify-between gap-2 border-b px-4 py-2 backdrop-blur sm:px-6 lg:hidden">
158
- <Button variant="outline" size="sm" onClick={() => setDrawerOpen(true)}>
159
- <SlidersHorizontal className="size-3.5" />
160
- {filtersLabel}
161
- {count > 0 ? <Badge className="ml-1 size-4 justify-center rounded-full p-0 text-[10px]">{count}</Badge> : null}
162
- </Button>
163
- <Button variant="ghost" size="sm" onClick={() => setSortOpen(true)}>
164
- {sortLabel}: {selectedSortLabel}
165
- </Button>
166
- <span aria-live="polite" className="text-muted-foreground text-xs tabular-nums">
167
- {resultsCountLabel(filtered.length)}
168
- </span>
169
- </div>
170
-
171
- <div className="px-4 py-2 sm:px-6 lg:hidden">
172
- <CategoryFilter4ChipRail
173
- filters={filters}
174
- onChange={handleFiltersChange}
175
- empty={empty}
176
- clearAllLabel={clearAllLabel}
177
- />
178
- </div>
179
-
180
- <div className="flex gap-8 px-4 py-6 sm:px-6">
181
- <div className="hidden lg:block">
182
- <CategoryFilter4Sidebar
183
- filters={filters}
184
- onChange={handleFiltersChange}
185
- countWith={countWith}
186
- facets={facets}
187
- colourNames={colourNames}
188
- modelTypeLabel={modelTypeLabel}
189
- rangeLabel={rangeLabel}
190
- frameSizeLabel={frameSizeLabel}
191
- colourLabel={colourLabel}
192
- featuresLabel={featuresLabel}
193
- />
194
- </div>
195
-
196
- <div className="flex-1">
197
- <div className="hidden items-center justify-between gap-2 lg:flex">
198
- <CategoryFilter4ChipRail
199
- filters={filters}
200
- onChange={setFilters}
201
- empty={empty}
202
- clearAllLabel={clearAllLabel}
203
- />
204
- <span aria-live="polite" className="text-muted-foreground text-xs tabular-nums">
205
- {resultsCountLabel(filtered.length)}
206
- </span>
207
- </div>
208
-
209
- {filtered.length === 0 ? (
210
- <Empty className="mt-6">
211
- <EmptyHeader>
212
- <EmptyTitle>{noResultsTitle}</EmptyTitle>
213
- <EmptyDescription>{noResultsDescription}</EmptyDescription>
214
- </EmptyHeader>
215
- <EmptyContent>
216
- <Button size="sm" onClick={() => handleFiltersChange(empty)}>
217
- {noResultsCtaLabel}
218
- </Button>
219
- </EmptyContent>
220
- </Empty>
221
- ) : (
222
- <div className="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-2 md:grid-cols-3">
223
- {filtered.map(product => {
224
- const card = (
225
- <>
226
- {/*
227
- An empty grey square told the reader nothing and made a filtered grid look
228
- broken rather than filtered. The alt is empty on purpose: the name and price
229
- directly beneath already carry everything the picture would say.
230
- */}
231
- <img
232
- src={fallbackImage || undefined}
233
- alt=""
234
- loading="lazy"
235
- decoding="async"
236
- className="bg-muted aspect-square w-full rounded-lg object-cover"
237
- />
238
- <p className="text-sm font-medium">{product.name}</p>
239
- <p className="text-muted-foreground text-xs tabular-nums">
240
- ${product.price.toLocaleString()} · {product.rangeKm} km
241
- </p>
242
- </>
243
- )
244
-
245
- return onProductSelect ? (
246
- <button
247
- key={product.id}
248
- type="button"
249
- onClick={() => onProductSelect(product)}
250
- className="flex cursor-pointer flex-col gap-1.5 text-left"
251
- >
252
- {card}
253
- </button>
254
- ) : (
255
- <div key={product.id} className="flex flex-col gap-1.5">
256
- {card}
257
- </div>
258
- )
259
- })}
260
- </div>
261
- )}
262
- </div>
263
- </div>
264
-
265
- <CategoryFilter4Drawer
266
- open={drawerOpen}
267
- onOpenChange={setDrawerOpen}
268
- filters={filters}
269
- onChange={handleFiltersChange}
270
- countWith={countWith}
271
- resultCount={filtered.length}
272
- empty={empty}
273
- facets={facets}
274
- colourNames={colourNames}
275
- filtersLabel={filtersLabel}
276
- modelTypeLabel={modelTypeLabel}
277
- rangeLabel={rangeLabel}
278
- frameSizeLabel={frameSizeLabel}
279
- colourLabel={colourLabel}
280
- featuresLabel={featuresLabel}
281
- clearAllLabel={clearAllLabel}
282
- showResultsLabel={showResultsLabel}
283
- noResultsLabel={noResultsLabel}
284
- noResultsDescription={noResultsDescription}
285
- />
286
- <CategoryFilter4SortSheet
287
- open={sortOpen}
288
- onOpenChange={setSortOpen}
289
- sort={sort}
290
- onSortChange={handleSortChange}
291
- sortOptions={facets.sortOptions}
292
- sortByLabel={sortByLabel}
293
- />
294
- </div>
295
- )
296
- }
297
-
298
- export default CategoryFilter4
@@ -1,47 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { formatCardNumber, formatExpiryDate } from './card-formatting'
3
-
4
- describe('formatCardNumber', () => {
5
- it('strips non-digit characters', () => {
6
- expect(formatCardNumber('4242 4242 4242 4242')).toBe('4242 4242 4242 4242')
7
- })
8
-
9
- it('groups digits in blocks of four', () => {
10
- expect(formatCardNumber('4242424242424242')).toBe('4242 4242 4242 4242')
11
- })
12
-
13
- it('caps at sixteen digits', () => {
14
- expect(formatCardNumber('42424242424242421234')).toBe('4242 4242 4242 4242')
15
- })
16
-
17
- it('does not add a trailing group separator', () => {
18
- expect(formatCardNumber('42424242')).toBe('4242 4242')
19
- })
20
-
21
- it('returns an empty string for no input', () => {
22
- expect(formatCardNumber('')).toBe('')
23
- })
24
- })
25
-
26
- describe('formatExpiryDate', () => {
27
- it('masks two-digit months without a slash', () => {
28
- expect(formatExpiryDate('12')).toBe('12')
29
- })
30
-
31
- it('masks MM/YY once a third digit is typed', () => {
32
- expect(formatExpiryDate('122')).toBe('12/2')
33
- expect(formatExpiryDate('1226')).toBe('12/26')
34
- })
35
-
36
- it('strips non-digit characters', () => {
37
- expect(formatExpiryDate('12/26')).toBe('12/26')
38
- })
39
-
40
- it('caps at four digits', () => {
41
- expect(formatExpiryDate('12261234')).toBe('12/26')
42
- })
43
-
44
- it('returns an empty string for no input', () => {
45
- expect(formatExpiryDate('')).toBe('')
46
- })
47
- })
@@ -1,9 +0,0 @@
1
- export function formatCardNumber(value: string, maxDigits = 16): string {
2
- const digits = value.replace(/\D/g, '').slice(0, maxDigits)
3
- return digits.replace(/(\d{4})(?=\d)/g, '$1 ')
4
- }
5
-
6
- export function formatExpiryDate(value: string, maxDigits = 4): string {
7
- const digits = value.replace(/\D/g, '').slice(0, maxDigits)
8
- return digits.length > 2 ? `${digits.slice(0, 2)}/${digits.slice(2)}` : digits
9
- }
@@ -1,96 +0,0 @@
1
- 'use client'
2
-
3
- import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
4
- import { Input } from '../../../ui/input'
5
- import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
6
- import type {
7
- CheckoutForm1ChangeHandler,
8
- CheckoutForm1FieldDescriptions,
9
- CheckoutForm1FieldErrors,
10
- CheckoutForm1FormData,
11
- } from './checkout-form-1'
12
-
13
- export interface CheckoutForm1ContactStepProps {
14
- formData: CheckoutForm1FormData
15
- onChange: CheckoutForm1ChangeHandler
16
- fieldErrors?: CheckoutForm1FieldErrors
17
- fieldDescriptions?: CheckoutForm1FieldDescriptions
18
- }
19
-
20
- export function CheckoutForm1ContactStep({
21
- formData,
22
- onChange,
23
- fieldErrors,
24
- fieldDescriptions,
25
- }: CheckoutForm1ContactStepProps) {
26
- return (
27
- <FieldGroup className="grid gap-4">
28
- <Field data-invalid={!!fieldErrors?.email?.length}>
29
- <FieldLabel htmlFor="checkout-email">Email</FieldLabel>
30
- <Input
31
- id="checkout-email"
32
- type="text"
33
- inputMode="email"
34
- placeholder="john@example.com"
35
- value={formData.email}
36
- onChange={(e) => onChange('email', e.target.value)}
37
- {...checkoutFieldAria('checkout-email', fieldErrors?.email)}
38
- />
39
- <CheckoutFieldFeedback
40
- id="checkout-email"
41
- errors={fieldErrors?.email}
42
- description={fieldDescriptions?.email}
43
- />
44
- </Field>
45
- <div className="grid gap-4 md:grid-cols-2">
46
- <Field data-invalid={!!fieldErrors?.firstName?.length}>
47
- <FieldLabel htmlFor="checkout-first-name">First name</FieldLabel>
48
- <Input
49
- id="checkout-first-name"
50
- placeholder="John"
51
- value={formData.firstName}
52
- onChange={(e) => onChange('firstName', e.target.value)}
53
- {...checkoutFieldAria('checkout-first-name', fieldErrors?.firstName)}
54
- />
55
- <CheckoutFieldFeedback
56
- id="checkout-first-name"
57
- errors={fieldErrors?.firstName}
58
- description={fieldDescriptions?.firstName}
59
- />
60
- </Field>
61
- <Field data-invalid={!!fieldErrors?.lastName?.length}>
62
- <FieldLabel htmlFor="checkout-last-name">Last name</FieldLabel>
63
- <Input
64
- id="checkout-last-name"
65
- placeholder="Doe"
66
- value={formData.lastName}
67
- onChange={(e) => onChange('lastName', e.target.value)}
68
- {...checkoutFieldAria('checkout-last-name', fieldErrors?.lastName)}
69
- />
70
- <CheckoutFieldFeedback
71
- id="checkout-last-name"
72
- errors={fieldErrors?.lastName}
73
- description={fieldDescriptions?.lastName}
74
- />
75
- </Field>
76
- </div>
77
- <Field data-invalid={!!fieldErrors?.phone?.length}>
78
- <FieldLabel htmlFor="checkout-phone">Phone (optional)</FieldLabel>
79
- <Input
80
- id="checkout-phone"
81
- type="text"
82
- inputMode="tel"
83
- placeholder="+1 (555) 000-0000"
84
- value={formData.phone}
85
- onChange={(e) => onChange('phone', e.target.value)}
86
- {...checkoutFieldAria('checkout-phone', fieldErrors?.phone)}
87
- />
88
- <CheckoutFieldFeedback
89
- id="checkout-phone"
90
- errors={fieldErrors?.phone}
91
- description={fieldDescriptions?.phone}
92
- />
93
- </Field>
94
- </FieldGroup>
95
- )
96
- }