@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,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
|
-
}
|