@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,392 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { ChevronLeft, ChevronRight, Lock } from 'lucide-react'
|
|
5
|
-
|
|
6
|
-
import type { PaymentMethod } from '@thorprovider/types'
|
|
7
|
-
|
|
8
|
-
import { Button } from '../../../ui/button'
|
|
9
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
10
|
-
|
|
11
|
-
import { CheckoutForm3InformationStep } from './checkout-form-3-information-step'
|
|
12
|
-
import { CheckoutForm3OrderSummary } from './checkout-form-3-order-summary'
|
|
13
|
-
import { CheckoutForm3PaymentStep } from './checkout-form-3-payment-step'
|
|
14
|
-
import { CheckoutForm3Progress, CHECKOUT_FORM_3_STEP_ICONS } from './checkout-form-3-progress'
|
|
15
|
-
import { CheckoutForm3ReviewStep } from './checkout-form-3-review-step'
|
|
16
|
-
import { CheckoutForm3ShippingStep } from './checkout-form-3-shipping-step'
|
|
17
|
-
|
|
18
|
-
export interface CheckoutForm3CartItem {
|
|
19
|
-
id: number
|
|
20
|
-
name: string
|
|
21
|
-
price: number
|
|
22
|
-
quantity: number
|
|
23
|
-
image: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CheckoutForm3ShippingMethod {
|
|
27
|
-
id: string
|
|
28
|
-
name: string
|
|
29
|
-
description: string
|
|
30
|
-
cost: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface CheckoutForm3CountryOption {
|
|
34
|
-
value: string
|
|
35
|
-
label: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface CheckoutForm3Step {
|
|
39
|
-
id: number
|
|
40
|
-
name: string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface CheckoutForm3FormData {
|
|
44
|
-
email: string
|
|
45
|
-
firstName: string
|
|
46
|
-
lastName: string
|
|
47
|
-
phone: string
|
|
48
|
-
address: string
|
|
49
|
-
apartment: string
|
|
50
|
-
city: string
|
|
51
|
-
state: string
|
|
52
|
-
zipCode: string
|
|
53
|
-
country: string
|
|
54
|
-
shippingMethod: string
|
|
55
|
-
paymentMethod: string
|
|
56
|
-
cardNumber: string
|
|
57
|
-
expiryDate: string
|
|
58
|
-
cvv: string
|
|
59
|
-
cardName: string
|
|
60
|
-
billingAddress: string
|
|
61
|
-
newsletter: boolean
|
|
62
|
-
saveInfo: boolean
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type CheckoutForm3ChangeHandler = (
|
|
66
|
-
field: keyof CheckoutForm3FormData,
|
|
67
|
-
value: string | boolean
|
|
68
|
-
) => void
|
|
69
|
-
|
|
70
|
-
export interface CheckoutForm3Props {
|
|
71
|
-
cartItems: CheckoutForm3CartItem[]
|
|
72
|
-
shippingMethods: CheckoutForm3ShippingMethod[]
|
|
73
|
-
paymentMethods?: PaymentMethod[]
|
|
74
|
-
taxRate?: number
|
|
75
|
-
countryOptions?: CheckoutForm3CountryOption[]
|
|
76
|
-
steps?: CheckoutForm3Step[]
|
|
77
|
-
currencySymbol?: string
|
|
78
|
-
titleLabel?: string
|
|
79
|
-
subtitleLabel?: string
|
|
80
|
-
backLabel?: string
|
|
81
|
-
continueLabel?: string
|
|
82
|
-
placeOrderLabel?: string
|
|
83
|
-
shippingAddressLabel?: string
|
|
84
|
-
shippingMethodLabel?: string
|
|
85
|
-
paymentLabel?: string
|
|
86
|
-
noMethodsLabel?: string
|
|
87
|
-
securePaymentLabel?: string
|
|
88
|
-
emailLabel?: string
|
|
89
|
-
firstNameLabel?: string
|
|
90
|
-
lastNameLabel?: string
|
|
91
|
-
phoneLabel?: string
|
|
92
|
-
addressLabel?: string
|
|
93
|
-
apartmentLabel?: string
|
|
94
|
-
cityLabel?: string
|
|
95
|
-
stateLabel?: string
|
|
96
|
-
zipCodeLabel?: string
|
|
97
|
-
countryLabel?: string
|
|
98
|
-
cardNumberLabel?: string
|
|
99
|
-
expiryLabel?: string
|
|
100
|
-
cvvLabel?: string
|
|
101
|
-
cardNameLabel?: string
|
|
102
|
-
newsletterLabel?: string
|
|
103
|
-
saveInfoLabel?: string
|
|
104
|
-
orderSummaryLabel?: string
|
|
105
|
-
subtotalLabel?: string
|
|
106
|
-
shippingLabel?: string
|
|
107
|
-
taxLabel?: string
|
|
108
|
-
totalLabel?: string
|
|
109
|
-
freeLabel?: string
|
|
110
|
-
orderReviewLabel?: string
|
|
111
|
-
contactReviewLabel?: string
|
|
112
|
-
shipToReviewLabel?: string
|
|
113
|
-
paymentReviewLabel?: string
|
|
114
|
-
shippingReviewLabel?: string
|
|
115
|
-
onComplete?: (data: CheckoutForm3FormData) => void
|
|
116
|
-
onNextStep?: (step: number) => void
|
|
117
|
-
onPreviousStep?: (step: number) => void
|
|
118
|
-
onFieldChange?: CheckoutForm3ChangeHandler
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const DEFAULT_COUNTRY_OPTIONS: CheckoutForm3CountryOption[] = [
|
|
122
|
-
{ value: 'US', label: 'Estados Unidos' },
|
|
123
|
-
{ value: 'CA', label: 'Canadá' },
|
|
124
|
-
{ value: 'UK', label: 'Reino Unido' },
|
|
125
|
-
{ value: 'AU', label: 'Australia' },
|
|
126
|
-
]
|
|
127
|
-
|
|
128
|
-
const DEFAULT_STEPS: CheckoutForm3Step[] = [
|
|
129
|
-
{ id: 1, name: 'Información' },
|
|
130
|
-
{ id: 2, name: 'Envío' },
|
|
131
|
-
{ id: 3, name: 'Pago' },
|
|
132
|
-
{ id: 4, name: 'Revisión' },
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
export default function CheckoutForm3({
|
|
136
|
-
cartItems,
|
|
137
|
-
shippingMethods,
|
|
138
|
-
paymentMethods = [],
|
|
139
|
-
taxRate = 0,
|
|
140
|
-
countryOptions = DEFAULT_COUNTRY_OPTIONS,
|
|
141
|
-
steps = DEFAULT_STEPS,
|
|
142
|
-
currencySymbol = '$',
|
|
143
|
-
titleLabel = 'Checkout seguro',
|
|
144
|
-
subtitleLabel,
|
|
145
|
-
backLabel = 'Anterior',
|
|
146
|
-
continueLabel = 'Continuar',
|
|
147
|
-
placeOrderLabel = 'Realizar pedido',
|
|
148
|
-
shippingAddressLabel = 'Dirección de envío',
|
|
149
|
-
shippingMethodLabel = 'Método de envío',
|
|
150
|
-
paymentLabel = 'Método de pago',
|
|
151
|
-
noMethodsLabel = 'No hay métodos de pago disponibles para esta región.',
|
|
152
|
-
securePaymentLabel = 'Tu información de pago está cifrada y es segura',
|
|
153
|
-
emailLabel = 'Correo electrónico',
|
|
154
|
-
firstNameLabel = 'Nombre',
|
|
155
|
-
lastNameLabel = 'Apellidos',
|
|
156
|
-
phoneLabel = 'Teléfono',
|
|
157
|
-
addressLabel = 'Dirección',
|
|
158
|
-
apartmentLabel = 'Apartamento, suite, etc.',
|
|
159
|
-
cityLabel = 'Ciudad',
|
|
160
|
-
stateLabel = 'Estado/Provincia',
|
|
161
|
-
zipCodeLabel = 'Código postal',
|
|
162
|
-
countryLabel = 'País',
|
|
163
|
-
cardNumberLabel = 'Número de tarjeta',
|
|
164
|
-
expiryLabel = 'Fecha de vencimiento',
|
|
165
|
-
cvvLabel = 'CVV',
|
|
166
|
-
cardNameLabel = 'Nombre en la tarjeta',
|
|
167
|
-
newsletterLabel = 'Quiero recibir novedades y ofertas',
|
|
168
|
-
saveInfoLabel = 'Guardar esta información para la próxima vez',
|
|
169
|
-
orderSummaryLabel = 'Resumen del pedido',
|
|
170
|
-
subtotalLabel = 'Subtotal',
|
|
171
|
-
shippingLabel = 'Envío',
|
|
172
|
-
taxLabel = 'Impuestos',
|
|
173
|
-
totalLabel = 'Total',
|
|
174
|
-
freeLabel = 'Gratis',
|
|
175
|
-
orderReviewLabel = 'Revisión del pedido',
|
|
176
|
-
contactReviewLabel = 'Contacto',
|
|
177
|
-
shipToReviewLabel = 'Enviar a',
|
|
178
|
-
paymentReviewLabel = 'Pago',
|
|
179
|
-
shippingReviewLabel = 'Envío',
|
|
180
|
-
onComplete,
|
|
181
|
-
onNextStep,
|
|
182
|
-
onPreviousStep,
|
|
183
|
-
onFieldChange,
|
|
184
|
-
}: CheckoutForm3Props) {
|
|
185
|
-
const [currentStep, setCurrentStep] = useState(1)
|
|
186
|
-
const [formData, setFormData] = useState<CheckoutForm3FormData>(() => ({
|
|
187
|
-
email: '',
|
|
188
|
-
firstName: '',
|
|
189
|
-
lastName: '',
|
|
190
|
-
phone: '',
|
|
191
|
-
address: '',
|
|
192
|
-
apartment: '',
|
|
193
|
-
city: '',
|
|
194
|
-
state: '',
|
|
195
|
-
zipCode: '',
|
|
196
|
-
country: countryOptions[0]?.value ?? '',
|
|
197
|
-
shippingMethod: shippingMethods[0]?.id ?? '',
|
|
198
|
-
paymentMethod: paymentMethods[0]?.id ?? '',
|
|
199
|
-
cardNumber: '',
|
|
200
|
-
expiryDate: '',
|
|
201
|
-
cvv: '',
|
|
202
|
-
cardName: '',
|
|
203
|
-
billingAddress: 'same',
|
|
204
|
-
newsletter: false,
|
|
205
|
-
saveInfo: false,
|
|
206
|
-
}))
|
|
207
|
-
|
|
208
|
-
const subtotal = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
209
|
-
const shippingCosts = Object.fromEntries(
|
|
210
|
-
shippingMethods.map(method => [method.id, method.cost]),
|
|
211
|
-
) as Record<string, number>
|
|
212
|
-
const shipping = shippingCosts[formData.shippingMethod] ?? 0
|
|
213
|
-
const tax = subtotal * taxRate
|
|
214
|
-
const total = subtotal + shipping + tax
|
|
215
|
-
|
|
216
|
-
const handleInputChange: CheckoutForm3ChangeHandler = (field, value) => {
|
|
217
|
-
setFormData(prev => ({ ...prev, [field]: value }))
|
|
218
|
-
onFieldChange?.(field, value)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const handleNextStep = () => {
|
|
222
|
-
const next = Math.min(currentStep + 1, steps.length)
|
|
223
|
-
setCurrentStep(next)
|
|
224
|
-
onNextStep?.(next)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const handlePreviousStep = () => {
|
|
228
|
-
const next = Math.max(currentStep - 1, 1)
|
|
229
|
-
setCurrentStep(next)
|
|
230
|
-
onPreviousStep?.(next)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Continue stays disabled until the fields this step is responsible for are filled in. */
|
|
234
|
-
const isStepComplete = (stepNumber: number) => {
|
|
235
|
-
if (stepNumber < currentStep) return true
|
|
236
|
-
if (stepNumber === 1) return Boolean(formData.email && formData.firstName && formData.lastName)
|
|
237
|
-
if (stepNumber === 2) return Boolean(formData.address && formData.city && formData.zipCode)
|
|
238
|
-
if (stepNumber === 3) {
|
|
239
|
-
const selectedMethod = paymentMethods.find(method => method.id === formData.paymentMethod)
|
|
240
|
-
if (!formData.paymentMethod) return false
|
|
241
|
-
return selectedMethod?.type === 'card' ? Boolean(formData.cardNumber) : true
|
|
242
|
-
}
|
|
243
|
-
return false
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const CurrentStepIcon = CHECKOUT_FORM_3_STEP_ICONS[currentStep - 1]
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
<section className='py-8'>
|
|
250
|
-
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
251
|
-
<div className='mb-8'>
|
|
252
|
-
<h1 className='text-3xl font-bold tracking-tight text-balance'>{titleLabel}</h1>
|
|
253
|
-
<p className='text-muted-foreground mt-2'>
|
|
254
|
-
{subtitleLabel ?? `Completa tu compra en ${steps.length} sencillos pasos`}
|
|
255
|
-
</p>
|
|
256
|
-
</div>
|
|
257
|
-
|
|
258
|
-
<div className='grid gap-8 lg:grid-cols-3'>
|
|
259
|
-
<div className='lg:col-span-2'>
|
|
260
|
-
<CheckoutForm3Progress
|
|
261
|
-
steps={steps}
|
|
262
|
-
currentStep={currentStep}
|
|
263
|
-
isStepComplete={isStepComplete}
|
|
264
|
-
/>
|
|
265
|
-
|
|
266
|
-
<Card>
|
|
267
|
-
<CardHeader>
|
|
268
|
-
<CardTitle className='flex items-center gap-2'>
|
|
269
|
-
{CurrentStepIcon ? <CurrentStepIcon className='size-5' aria-hidden='true' /> : null}
|
|
270
|
-
{steps[currentStep - 1].name}
|
|
271
|
-
</CardTitle>
|
|
272
|
-
</CardHeader>
|
|
273
|
-
<CardContent className='flex flex-col gap-6'>
|
|
274
|
-
{currentStep === 1 ? (
|
|
275
|
-
<CheckoutForm3InformationStep
|
|
276
|
-
formData={formData}
|
|
277
|
-
onChange={handleInputChange}
|
|
278
|
-
emailLabel={emailLabel}
|
|
279
|
-
firstNameLabel={firstNameLabel}
|
|
280
|
-
lastNameLabel={lastNameLabel}
|
|
281
|
-
phoneLabel={phoneLabel}
|
|
282
|
-
newsletterLabel={newsletterLabel}
|
|
283
|
-
/>
|
|
284
|
-
) : null}
|
|
285
|
-
|
|
286
|
-
{currentStep === 2 ? (
|
|
287
|
-
<CheckoutForm3ShippingStep
|
|
288
|
-
formData={formData}
|
|
289
|
-
onChange={handleInputChange}
|
|
290
|
-
countryOptions={countryOptions}
|
|
291
|
-
shippingMethods={shippingMethods}
|
|
292
|
-
shippingAddressLabel={shippingAddressLabel}
|
|
293
|
-
shippingMethodLabel={shippingMethodLabel}
|
|
294
|
-
addressLabel={addressLabel}
|
|
295
|
-
apartmentLabel={apartmentLabel}
|
|
296
|
-
cityLabel={cityLabel}
|
|
297
|
-
stateLabel={stateLabel}
|
|
298
|
-
zipCodeLabel={zipCodeLabel}
|
|
299
|
-
countryLabel={countryLabel}
|
|
300
|
-
freeLabel={freeLabel}
|
|
301
|
-
currencySymbol={currencySymbol}
|
|
302
|
-
/>
|
|
303
|
-
) : null}
|
|
304
|
-
|
|
305
|
-
{currentStep === 3 ? (
|
|
306
|
-
<CheckoutForm3PaymentStep
|
|
307
|
-
formData={formData}
|
|
308
|
-
onChange={handleInputChange}
|
|
309
|
-
paymentMethods={paymentMethods}
|
|
310
|
-
paymentLabel={paymentLabel}
|
|
311
|
-
noMethodsLabel={noMethodsLabel}
|
|
312
|
-
cardNumberLabel={cardNumberLabel}
|
|
313
|
-
expiryLabel={expiryLabel}
|
|
314
|
-
cvvLabel={cvvLabel}
|
|
315
|
-
cardNameLabel={cardNameLabel}
|
|
316
|
-
securePaymentLabel={securePaymentLabel}
|
|
317
|
-
/>
|
|
318
|
-
) : null}
|
|
319
|
-
|
|
320
|
-
{currentStep === 4 ? (
|
|
321
|
-
<CheckoutForm3ReviewStep
|
|
322
|
-
formData={formData}
|
|
323
|
-
onChange={handleInputChange}
|
|
324
|
-
shippingMethods={shippingMethods}
|
|
325
|
-
paymentMethods={paymentMethods}
|
|
326
|
-
orderReviewLabel={orderReviewLabel}
|
|
327
|
-
contactReviewLabel={contactReviewLabel}
|
|
328
|
-
shipToReviewLabel={shipToReviewLabel}
|
|
329
|
-
paymentReviewLabel={paymentReviewLabel}
|
|
330
|
-
shippingReviewLabel={shippingReviewLabel}
|
|
331
|
-
saveInfoLabel={saveInfoLabel}
|
|
332
|
-
/>
|
|
333
|
-
) : null}
|
|
334
|
-
|
|
335
|
-
<div className='flex justify-between pt-6'>
|
|
336
|
-
<Button
|
|
337
|
-
type='button'
|
|
338
|
-
variant='outline'
|
|
339
|
-
className='h-9 px-4 py-2 cursor-pointer'
|
|
340
|
-
onClick={handlePreviousStep}
|
|
341
|
-
disabled={currentStep === 1}
|
|
342
|
-
>
|
|
343
|
-
<ChevronLeft data-icon='inline-start' />
|
|
344
|
-
{backLabel}
|
|
345
|
-
</Button>
|
|
346
|
-
|
|
347
|
-
{currentStep < steps.length ? (
|
|
348
|
-
<Button
|
|
349
|
-
type='button'
|
|
350
|
-
onClick={handleNextStep}
|
|
351
|
-
className='h-9 px-4 py-2 cursor-pointer'
|
|
352
|
-
disabled={!isStepComplete(currentStep)}
|
|
353
|
-
>
|
|
354
|
-
{continueLabel}
|
|
355
|
-
<ChevronRight data-icon='inline-end' />
|
|
356
|
-
</Button>
|
|
357
|
-
) : (
|
|
358
|
-
<Button
|
|
359
|
-
type='button'
|
|
360
|
-
className='h-9 px-4 py-2 min-w-32 cursor-pointer'
|
|
361
|
-
onClick={() => onComplete?.(formData)}
|
|
362
|
-
>
|
|
363
|
-
<Lock data-icon='inline-start' />
|
|
364
|
-
{placeOrderLabel}
|
|
365
|
-
</Button>
|
|
366
|
-
)}
|
|
367
|
-
</div>
|
|
368
|
-
</CardContent>
|
|
369
|
-
</Card>
|
|
370
|
-
</div>
|
|
371
|
-
|
|
372
|
-
<div className='lg:col-span-1'>
|
|
373
|
-
<CheckoutForm3OrderSummary
|
|
374
|
-
items={cartItems}
|
|
375
|
-
subtotal={subtotal}
|
|
376
|
-
shipping={shipping}
|
|
377
|
-
tax={tax}
|
|
378
|
-
total={total}
|
|
379
|
-
currencySymbol={currencySymbol}
|
|
380
|
-
orderSummaryLabel={orderSummaryLabel}
|
|
381
|
-
subtotalLabel={subtotalLabel}
|
|
382
|
-
shippingLabel={shippingLabel}
|
|
383
|
-
taxLabel={taxLabel}
|
|
384
|
-
totalLabel={totalLabel}
|
|
385
|
-
freeLabel={freeLabel}
|
|
386
|
-
/>
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
</div>
|
|
390
|
-
</section>
|
|
391
|
-
)
|
|
392
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { ReactElement, ReactNode } from 'react'
|
|
2
|
-
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import { FooterSection1 } from './footer-section-1'
|
|
5
|
-
|
|
6
|
-
const base = {
|
|
7
|
-
brandName: 'ElectroCuba',
|
|
8
|
-
linkColumns: [],
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type SubmitHandler = (event: { preventDefault: () => void }) => void
|
|
12
|
-
|
|
13
|
-
function findForm(node: ReactNode): ReactElement<{ onSubmit?: SubmitHandler }> | undefined {
|
|
14
|
-
if (node === null || node === undefined || typeof node !== 'object') return undefined
|
|
15
|
-
if (Array.isArray(node)) {
|
|
16
|
-
for (const child of node) {
|
|
17
|
-
const found = findForm(child)
|
|
18
|
-
if (found) return found
|
|
19
|
-
}
|
|
20
|
-
return undefined
|
|
21
|
-
}
|
|
22
|
-
const element = node as ReactElement<{ children?: ReactNode }>
|
|
23
|
-
if (element.type === 'form') return element as ReactElement<{ onSubmit?: SubmitHandler }>
|
|
24
|
-
return findForm(element.props?.children)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('FooterSection1 — optional brand accent', () => {
|
|
28
|
-
it('renders the brand name unchanged when no accent is supplied', () => {
|
|
29
|
-
const html = renderToStaticMarkup(<FooterSection1 {...base} />)
|
|
30
|
-
expect(html).toContain('>ElectroCuba</span>')
|
|
31
|
-
expect(html).not.toContain('text-brand-promo')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('accents a matched trailing segment without duplicating the brand name', () => {
|
|
35
|
-
const html = renderToStaticMarkup(
|
|
36
|
-
<FooterSection1 {...base} brandAccent={{ text: 'Cuba', className: 'text-brand-promo' }} />,
|
|
37
|
-
)
|
|
38
|
-
expect(html).toContain('Electro<span class="text-brand-promo">Cuba</span>')
|
|
39
|
-
expect(html.match(/Electro/g)).toHaveLength(1)
|
|
40
|
-
expect(html.match(/Cuba/g)).toHaveLength(1)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('accents the full brand name when the accent covers all of it', () => {
|
|
44
|
-
const html = renderToStaticMarkup(
|
|
45
|
-
<FooterSection1 {...base} brandAccent={{ text: 'ElectroCuba', className: 'text-brand-promo' }} />,
|
|
46
|
-
)
|
|
47
|
-
expect(html).toContain('<span class="text-brand-promo">ElectroCuba</span>')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('does not accent when the segment is not a trailing match', () => {
|
|
51
|
-
const html = renderToStaticMarkup(<FooterSection1 {...base} brandAccent={{ text: 'Electro' }} />)
|
|
52
|
-
expect(html).toContain('>ElectroCuba</span>')
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
describe('FooterSection1 — newsletter requires a submit callback', () => {
|
|
57
|
-
it('renders no form, input, or button when the newsletter has no onSubmit', () => {
|
|
58
|
-
const html = renderToStaticMarkup(
|
|
59
|
-
<FooterSection1 {...base} newsletter={{ buttonLabel: 'Suscribirse', placeholder: 'tu@correo.com' }} />,
|
|
60
|
-
)
|
|
61
|
-
expect(html).not.toContain('<form')
|
|
62
|
-
expect(html).not.toContain('<input')
|
|
63
|
-
expect(html).not.toContain('<button')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('renders the interactive form when a submit handler is supplied', () => {
|
|
67
|
-
const html = renderToStaticMarkup(
|
|
68
|
-
<FooterSection1 {...base} newsletter={{ buttonLabel: 'Suscribirse', onSubmit: () => {} }} />,
|
|
69
|
-
)
|
|
70
|
-
expect(html).toContain('<form')
|
|
71
|
-
expect(html).toContain('<input')
|
|
72
|
-
expect(html).toContain('<button')
|
|
73
|
-
expect(html).toContain('Suscribirse')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('retains the supplied submit handler and forwards the email', () => {
|
|
77
|
-
const onSubmit = vi.fn()
|
|
78
|
-
const tree = FooterSection1({
|
|
79
|
-
...base,
|
|
80
|
-
newsletter: { buttonLabel: 'Suscribirse', onSubmit },
|
|
81
|
-
})
|
|
82
|
-
const form = findForm(tree)
|
|
83
|
-
expect(form).toBeDefined()
|
|
84
|
-
|
|
85
|
-
vi.stubGlobal(
|
|
86
|
-
'FormData',
|
|
87
|
-
class {
|
|
88
|
-
get() {
|
|
89
|
-
return 'shopper@example.com'
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
)
|
|
93
|
-
try {
|
|
94
|
-
form?.props.onSubmit?.({ preventDefault: () => {} })
|
|
95
|
-
} finally {
|
|
96
|
-
vi.unstubAllGlobals()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
expect(onSubmit).toHaveBeenCalledWith('shopper@example.com')
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('FooterSection1 — optional outer surface className', () => {
|
|
104
|
-
it('keeps the default surface when className is omitted', () => {
|
|
105
|
-
const html = renderToStaticMarkup(<FooterSection1 {...base} />)
|
|
106
|
-
expect(html).toContain('bg-secondary')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('merges a branded surface onto the footer and drops the default background', () => {
|
|
110
|
-
const html = renderToStaticMarkup(
|
|
111
|
-
<FooterSection1 {...base} className='bg-brand-nav text-brand-nav-foreground' />,
|
|
112
|
-
)
|
|
113
|
-
expect(html).toContain('bg-brand-nav')
|
|
114
|
-
expect(html).toContain('text-brand-nav-foreground')
|
|
115
|
-
expect(html).not.toContain('bg-secondary')
|
|
116
|
-
})
|
|
117
|
-
})
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Input } from '../../../ui/input'
|
|
4
|
-
import { Button } from '../../../ui/button'
|
|
5
|
-
import { ArrowRight } from 'lucide-react'
|
|
6
|
-
import { cn } from '../../../lib/utils'
|
|
7
|
-
|
|
8
|
-
export interface FooterLink {
|
|
9
|
-
label: string
|
|
10
|
-
href?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface FooterLinkColumn {
|
|
14
|
-
title: string
|
|
15
|
-
links: FooterLink[]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface FooterNewsletter {
|
|
19
|
-
placeholder?: string
|
|
20
|
-
buttonLabel: string
|
|
21
|
-
/**
|
|
22
|
-
* Submit handler. The interactive form (input + submit button) renders only
|
|
23
|
-
* when this is supplied. A newsletter without `onSubmit` renders no form,
|
|
24
|
-
* input, or button — never a fake form.
|
|
25
|
-
*/
|
|
26
|
-
onSubmit?: (email: string) => void
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface BrandAccent {
|
|
30
|
-
/** Trailing segment of `brandName` to render with the accent styling. */
|
|
31
|
-
text: string
|
|
32
|
-
/** Consumer-supplied classes for the accent span (e.g. a brand color token). */
|
|
33
|
-
className?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface FooterSection1Props {
|
|
37
|
-
brandName: string
|
|
38
|
-
/**
|
|
39
|
-
* Optional accent for a trailing segment of `brandName` (e.g. the "Cuba" in
|
|
40
|
-
* "ElectroCuba"). When omitted, or when `text` is not a trailing segment of
|
|
41
|
-
* `brandName`, the brand name renders unchanged.
|
|
42
|
-
*/
|
|
43
|
-
brandAccent?: BrandAccent
|
|
44
|
-
brandDescription?: string
|
|
45
|
-
linkColumns: FooterLinkColumn[]
|
|
46
|
-
newsletter?: FooterNewsletter
|
|
47
|
-
/**
|
|
48
|
-
* Consumer-supplied classes merged onto the outer `<footer>` as the final
|
|
49
|
-
* argument, so a semantic surface such as `bg-brand-nav
|
|
50
|
-
* text-brand-nav-foreground` overrides the default `bg-secondary`. Purely
|
|
51
|
-
* presentational: no layout, structure, or behavior changes.
|
|
52
|
-
*/
|
|
53
|
-
className?: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function FooterSection1({
|
|
57
|
-
brandName,
|
|
58
|
-
brandAccent,
|
|
59
|
-
brandDescription,
|
|
60
|
-
linkColumns,
|
|
61
|
-
newsletter,
|
|
62
|
-
className,
|
|
63
|
-
}: FooterSection1Props) {
|
|
64
|
-
const accentText = brandAccent?.text ?? ''
|
|
65
|
-
const hasAccent = accentText.length > 0 && brandName.endsWith(accentText)
|
|
66
|
-
const baseName = hasAccent ? brandName.slice(0, brandName.length - accentText.length) : brandName
|
|
67
|
-
const newsletterSubmit = newsletter?.onSubmit
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<footer
|
|
71
|
-
className={cn('bg-secondary w-full py-16', className)}
|
|
72
|
-
aria-label='Pie de página'
|
|
73
|
-
>
|
|
74
|
-
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
75
|
-
<div className='grid grid-cols-1 gap-8 lg:grid-cols-12'>
|
|
76
|
-
{/* Left Section */}
|
|
77
|
-
<div className='me-8 md:col-span-5'>
|
|
78
|
-
<div className='mb-6 flex items-center gap-2'>
|
|
79
|
-
<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
|
80
|
-
<path
|
|
81
|
-
d='M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z'
|
|
82
|
-
fill='currentColor'
|
|
83
|
-
/>
|
|
84
|
-
<path
|
|
85
|
-
d='M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z'
|
|
86
|
-
fill='currentColor'
|
|
87
|
-
/>
|
|
88
|
-
<path
|
|
89
|
-
d='M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z'
|
|
90
|
-
fill='currentColor'
|
|
91
|
-
/>
|
|
92
|
-
<path
|
|
93
|
-
d='M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z'
|
|
94
|
-
fill='currentColor'
|
|
95
|
-
/>
|
|
96
|
-
<path
|
|
97
|
-
d='M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z'
|
|
98
|
-
fill='currentColor'
|
|
99
|
-
/>
|
|
100
|
-
<path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
|
|
101
|
-
<path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
|
|
102
|
-
<circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
|
|
103
|
-
<circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
|
|
104
|
-
</svg>
|
|
105
|
-
<span className='text-xl font-semibold'>
|
|
106
|
-
{baseName}
|
|
107
|
-
{hasAccent ? <span className={brandAccent?.className}>{accentText}</span> : null}
|
|
108
|
-
</span>
|
|
109
|
-
</div>
|
|
110
|
-
{brandDescription && (
|
|
111
|
-
<p className='text-muted-foreground mb-8 max-w-sm text-sm leading-relaxed text-balance'>
|
|
112
|
-
{brandDescription}
|
|
113
|
-
</p>
|
|
114
|
-
)}
|
|
115
|
-
{newsletter && newsletterSubmit ? (
|
|
116
|
-
<form
|
|
117
|
-
className='relative'
|
|
118
|
-
onSubmit={e => {
|
|
119
|
-
e.preventDefault()
|
|
120
|
-
const email = new FormData(e.currentTarget).get('email')
|
|
121
|
-
if (typeof email === 'string') {
|
|
122
|
-
newsletterSubmit(email)
|
|
123
|
-
}
|
|
124
|
-
}}
|
|
125
|
-
>
|
|
126
|
-
<Input
|
|
127
|
-
type='email'
|
|
128
|
-
name='email'
|
|
129
|
-
placeholder={newsletter.placeholder}
|
|
130
|
-
className='border-muted-foreground/20 bg-background/80 h-12 pe-32'
|
|
131
|
-
aria-label='Correo electrónico'
|
|
132
|
-
/>
|
|
133
|
-
<Button
|
|
134
|
-
type='submit'
|
|
135
|
-
variant='default'
|
|
136
|
-
className='absolute end-1 top-1 h-10 cursor-pointer px-4'
|
|
137
|
-
aria-label='Suscribirse a la newsletter'
|
|
138
|
-
>
|
|
139
|
-
{newsletter.buttonLabel}
|
|
140
|
-
<ArrowRight />
|
|
141
|
-
</Button>
|
|
142
|
-
</form>
|
|
143
|
-
) : null}
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{/* Right Section */}
|
|
147
|
-
<div className='md:col-span-7'>
|
|
148
|
-
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3'>
|
|
149
|
-
{linkColumns.map(column => (
|
|
150
|
-
<div key={column.title}>
|
|
151
|
-
<h3 className='mb-4 text-lg font-medium'>{column.title}</h3>
|
|
152
|
-
<ul className='flex flex-col gap-3'>
|
|
153
|
-
{column.links.map(link => (
|
|
154
|
-
<li key={link.label}>
|
|
155
|
-
{link.href ? (
|
|
156
|
-
<a
|
|
157
|
-
href={link.href}
|
|
158
|
-
className='text-muted-foreground hover:text-primary text-sm transition-colors'
|
|
159
|
-
>
|
|
160
|
-
{link.label}
|
|
161
|
-
</a>
|
|
162
|
-
) : (
|
|
163
|
-
<span className='text-muted-foreground text-sm'>{link.label}</span>
|
|
164
|
-
)}
|
|
165
|
-
</li>
|
|
166
|
-
))}
|
|
167
|
-
</ul>
|
|
168
|
-
</div>
|
|
169
|
-
))}
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
</footer>
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export default FooterSection1
|