@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +156 -40
  3. package/package.json +1 -1
  4. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  5. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  6. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  7. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  8. package/src/blocks/application/verification/verify-1.tsx +2 -2
  9. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  10. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  11. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  12. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  13. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  14. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  15. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  16. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  17. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  19. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  20. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  21. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  22. package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
  23. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  24. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  25. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  27. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
  29. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
  30. package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
  32. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  33. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  34. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  35. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  36. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  37. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  38. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  39. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  40. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  41. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  42. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  43. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  44. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  45. package/src/index.ts +80 -81
  46. package/src/lib/contracts-curated.test.ts +5 -5
  47. package/src/lib/contracts.test.ts +55 -0
  48. package/src/lib/contracts.ts +64 -2
  49. package/tsup.config.ts +1 -1
  50. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  51. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  52. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  53. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  54. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  55. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  56. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  57. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  58. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  59. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  60. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  61. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  62. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  63. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  67. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  68. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  69. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  70. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  71. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  72. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  73. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  74. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  75. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  76. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  77. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  78. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  79. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  80. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  81. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  82. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  83. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  84. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  85. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  86. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  87. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  88. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  89. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  90. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  91. 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