@thorprovider/blocks 0.1.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 (184) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/.turbo/turbo-test.log +10 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +161 -0
  6. package/components.json +33 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/contracts.d.mts +563 -0
  9. package/dist/contracts.d.ts +563 -0
  10. package/dist/contracts.js +207 -0
  11. package/dist/contracts.js.map +1 -0
  12. package/dist/contracts.mjs +168 -0
  13. package/dist/contracts.mjs.map +1 -0
  14. package/dist/index.d.mts +2408 -0
  15. package/dist/index.d.ts +2408 -0
  16. package/dist/index.js +16422 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +16352 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/docs/standards/data-contracts.md +70 -0
  21. package/docs/standards/theming.md +85 -0
  22. package/package.json +62 -0
  23. package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
  24. package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
  25. package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
  26. package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
  27. package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
  28. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
  29. package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
  30. package/src/blocks/application/verification/verify-1.tsx +307 -0
  31. package/src/blocks/application/verification/verify-confetti.tsx +192 -0
  32. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
  33. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
  34. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
  35. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
  36. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
  37. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
  38. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
  39. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
  40. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
  41. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
  42. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
  43. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
  44. package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
  45. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
  46. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
  47. package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
  48. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
  49. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
  50. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
  51. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
  52. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
  53. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
  54. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
  55. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
  56. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
  57. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
  58. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
  59. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
  60. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
  61. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
  62. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
  63. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
  67. package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
  68. package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
  69. package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
  70. package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
  71. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
  72. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
  73. package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
  74. package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
  75. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
  76. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
  77. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
  78. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
  79. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
  80. package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
  81. package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
  82. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
  83. package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
  84. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
  85. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
  86. package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
  87. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
  88. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
  89. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
  90. package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
  91. package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
  92. package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
  93. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
  94. package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
  95. package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
  96. package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
  97. package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
  98. package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
  99. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
  105. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
  106. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
  107. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
  108. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
  109. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
  110. package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
  111. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
  112. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
  113. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
  114. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
  115. package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
  116. package/src/blocks/marketing/banners/banner-1.tsx +188 -0
  117. package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
  118. package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
  119. package/src/blocks/marketing/features/feature-1.tsx +97 -0
  120. package/src/blocks/marketing/features/feature-2.tsx +82 -0
  121. package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
  122. package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
  123. package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
  124. package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
  125. package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
  126. package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
  127. package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
  128. package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
  129. package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
  130. package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
  131. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
  132. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
  133. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
  134. package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
  135. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
  136. package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
  137. package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
  138. package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
  139. package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
  140. package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
  141. package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
  142. package/src/blocks/shared/reaction-toggle.tsx +145 -0
  143. package/src/hooks/use-mobile.ts +19 -0
  144. package/src/index.ts +200 -0
  145. package/src/lib/contracts.test.ts +32 -0
  146. package/src/lib/contracts.ts +235 -0
  147. package/src/lib/types/menu.ts +43 -0
  148. package/src/lib/types.ts +4 -0
  149. package/src/lib/utils.ts +10 -0
  150. package/src/ui/accordion.tsx +65 -0
  151. package/src/ui/avatar.tsx +108 -0
  152. package/src/ui/badge.tsx +47 -0
  153. package/src/ui/breadcrumb.tsx +108 -0
  154. package/src/ui/button-group.tsx +83 -0
  155. package/src/ui/button.tsx +63 -0
  156. package/src/ui/card.tsx +91 -0
  157. package/src/ui/carousel.tsx +241 -0
  158. package/src/ui/dialog.tsx +158 -0
  159. package/src/ui/drawer.tsx +134 -0
  160. package/src/ui/dropdown-menu.tsx +256 -0
  161. package/src/ui/empty.tsx +103 -0
  162. package/src/ui/field.tsx +248 -0
  163. package/src/ui/input-group.tsx +170 -0
  164. package/src/ui/input-otp.tsx +76 -0
  165. package/src/ui/input.tsx +20 -0
  166. package/src/ui/label.tsx +23 -0
  167. package/src/ui/navigation-menu.tsx +167 -0
  168. package/src/ui/pagination.tsx +127 -0
  169. package/src/ui/progress.tsx +30 -0
  170. package/src/ui/radio-group.tsx +44 -0
  171. package/src/ui/select.tsx +189 -0
  172. package/src/ui/separator.tsx +27 -0
  173. package/src/ui/sheet.tsx +142 -0
  174. package/src/ui/sidebar.tsx +726 -0
  175. package/src/ui/skeleton.tsx +13 -0
  176. package/src/ui/table.tsx +115 -0
  177. package/src/ui/tabs.tsx +90 -0
  178. package/src/ui/textarea.tsx +17 -0
  179. package/src/ui/toggle-group.tsx +83 -0
  180. package/src/ui/toggle.tsx +46 -0
  181. package/src/ui/tooltip.tsx +56 -0
  182. package/tsconfig.json +20 -0
  183. package/tsup.config.ts +16 -0
  184. package/vitest.config.ts +9 -0
@@ -0,0 +1,190 @@
1
+ import { Truck } from 'lucide-react'
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
4
+ import { CheckboxCheck } from './checkbox-check'
5
+ import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
6
+ import { Input } from '../../../ui/input'
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectGroup,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from '../../../ui/select'
15
+ import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
16
+ import type {
17
+ CheckoutForm2CountryOption,
18
+ CheckoutForm2Data,
19
+ CheckoutForm2FieldDescriptions,
20
+ CheckoutForm2FieldErrors,
21
+ } from './checkout-form-2'
22
+
23
+ interface CheckoutForm2ShippingCardProps {
24
+ data: CheckoutForm2Data
25
+ onChange: (field: keyof CheckoutForm2Data, value: string | boolean) => void
26
+ countryOptions: CheckoutForm2CountryOption[]
27
+ shippingAddressLabel: string
28
+ firstNameLabel: string
29
+ lastNameLabel: string
30
+ addressLabel: string
31
+ cityLabel: string
32
+ zipCodeLabel: string
33
+ countryLabel: string
34
+ saveInfoLabel: string
35
+ fieldErrors?: CheckoutForm2FieldErrors
36
+ fieldDescriptions?: CheckoutForm2FieldDescriptions
37
+ }
38
+
39
+ const countryLabelFor = (value: string, options: CheckoutForm2CountryOption[]) =>
40
+ options.find(option => option.value === value)?.label ?? value
41
+
42
+ export function CheckoutForm2ShippingCard({
43
+ data,
44
+ onChange,
45
+ countryOptions,
46
+ shippingAddressLabel,
47
+ firstNameLabel,
48
+ lastNameLabel,
49
+ addressLabel,
50
+ cityLabel,
51
+ zipCodeLabel,
52
+ countryLabel,
53
+ saveInfoLabel,
54
+ fieldErrors,
55
+ fieldDescriptions,
56
+ }: CheckoutForm2ShippingCardProps) {
57
+ return (
58
+ <Card>
59
+ <CardHeader>
60
+ <CardTitle className='flex items-center gap-2'>
61
+ <Truck className='size-5' aria-hidden='true' />
62
+ {shippingAddressLabel}
63
+ </CardTitle>
64
+ </CardHeader>
65
+ <CardContent>
66
+ <FieldGroup>
67
+ <div className='grid gap-4 sm:grid-cols-2'>
68
+ <Field data-invalid={!!fieldErrors?.firstName?.length}>
69
+ <FieldLabel htmlFor='firstName'>{firstNameLabel}</FieldLabel>
70
+ <Input
71
+ id='firstName'
72
+ placeholder='Juan'
73
+ value={data.firstName}
74
+ onChange={event => onChange('firstName', event.target.value)}
75
+ {...checkoutFieldAria('firstName', fieldErrors?.firstName)}
76
+ className='h-9'
77
+ />
78
+ <CheckoutFieldFeedback
79
+ id='firstName'
80
+ errors={fieldErrors?.firstName}
81
+ description={fieldDescriptions?.firstName}
82
+ />
83
+ </Field>
84
+ <Field data-invalid={!!fieldErrors?.lastName?.length}>
85
+ <FieldLabel htmlFor='lastName'>{lastNameLabel}</FieldLabel>
86
+ <Input
87
+ id='lastName'
88
+ placeholder='Pérez'
89
+ value={data.lastName}
90
+ onChange={event => onChange('lastName', event.target.value)}
91
+ {...checkoutFieldAria('lastName', fieldErrors?.lastName)}
92
+ className='h-9'
93
+ />
94
+ <CheckoutFieldFeedback
95
+ id='lastName'
96
+ errors={fieldErrors?.lastName}
97
+ description={fieldDescriptions?.lastName}
98
+ />
99
+ </Field>
100
+ </div>
101
+
102
+ <Field data-invalid={!!fieldErrors?.address?.length}>
103
+ <FieldLabel htmlFor='address'>{addressLabel}</FieldLabel>
104
+ <Input
105
+ id='address'
106
+ placeholder='Calle Principal 123'
107
+ value={data.address}
108
+ onChange={event => onChange('address', event.target.value)}
109
+ {...checkoutFieldAria('address', fieldErrors?.address)}
110
+ className='h-9'
111
+ />
112
+ <CheckoutFieldFeedback
113
+ id='address'
114
+ errors={fieldErrors?.address}
115
+ description={fieldDescriptions?.address}
116
+ />
117
+ </Field>
118
+
119
+ <div className='grid gap-4 sm:grid-cols-2 md:grid-cols-3'>
120
+ <Field data-invalid={!!fieldErrors?.city?.length}>
121
+ <FieldLabel htmlFor='city'>{cityLabel}</FieldLabel>
122
+ <Input
123
+ id='city'
124
+ placeholder='Madrid'
125
+ value={data.city}
126
+ onChange={event => onChange('city', event.target.value)}
127
+ {...checkoutFieldAria('city', fieldErrors?.city)}
128
+ className='h-9'
129
+ />
130
+ <CheckoutFieldFeedback
131
+ id='city'
132
+ errors={fieldErrors?.city}
133
+ description={fieldDescriptions?.city}
134
+ />
135
+ </Field>
136
+ <Field data-invalid={!!fieldErrors?.zipCode?.length}>
137
+ <FieldLabel htmlFor='zipCode'>{zipCodeLabel}</FieldLabel>
138
+ <Input
139
+ id='zipCode'
140
+ placeholder='28001'
141
+ value={data.zipCode}
142
+ onChange={event => onChange('zipCode', event.target.value)}
143
+ {...checkoutFieldAria('zipCode', fieldErrors?.zipCode)}
144
+ className='h-9'
145
+ />
146
+ <CheckoutFieldFeedback
147
+ id='zipCode'
148
+ errors={fieldErrors?.zipCode}
149
+ description={fieldDescriptions?.zipCode}
150
+ />
151
+ </Field>
152
+ <Field data-invalid={!!fieldErrors?.country?.length}>
153
+ <FieldLabel htmlFor='country'>{countryLabel}</FieldLabel>
154
+ <Select value={data.country} onValueChange={value => value && onChange('country', value)}>
155
+ <SelectTrigger id='country' {...checkoutFieldAria('country', fieldErrors?.country)} className='w-full'>
156
+ <SelectValue>{countryLabelFor(data.country, countryOptions)}</SelectValue>
157
+ </SelectTrigger>
158
+ <SelectContent>
159
+ <SelectGroup>
160
+ {countryOptions.map(country => (
161
+ <SelectItem key={country.value} value={country.value}>
162
+ {country.label}
163
+ </SelectItem>
164
+ ))}
165
+ </SelectGroup>
166
+ </SelectContent>
167
+ </Select>
168
+ <CheckoutFieldFeedback
169
+ id='country'
170
+ errors={fieldErrors?.country}
171
+ description={fieldDescriptions?.country}
172
+ />
173
+ </Field>
174
+ </div>
175
+
176
+ <Field orientation='horizontal'>
177
+ <CheckboxCheck
178
+ id='saveInfo'
179
+ checked={data.saveInfo}
180
+ onCheckedChange={checked => onChange('saveInfo', checked as boolean)}
181
+ />
182
+ <FieldLabel htmlFor='saveInfo' className='text-sm font-normal'>
183
+ {saveInfoLabel}
184
+ </FieldLabel>
185
+ </Field>
186
+ </FieldGroup>
187
+ </CardContent>
188
+ </Card>
189
+ )
190
+ }
@@ -0,0 +1,312 @@
1
+ 'use client'
2
+
3
+ import { useState, type FormEvent } from 'react'
4
+ import { ArrowLeft, Lock, Package } from 'lucide-react'
5
+
6
+ import { Button } from '../../../ui/button'
7
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
8
+ import { CheckboxCheck } from './checkbox-check'
9
+ import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
10
+ import { Input } from '../../../ui/input'
11
+ import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
12
+ import { formatCardNumber, formatExpiryDate } from './card-formatting'
13
+ import type { PaymentMethod } from '@thorprovider/types'
14
+ import type { CheckoutFieldErrors } from './checkout-form-types'
15
+
16
+ import { CheckoutForm2OrderSummary } from './checkout-form-2-order-summary'
17
+ import { CheckoutForm2PaymentCard } from './checkout-form-2-payment-card'
18
+ import { CheckoutForm2ShippingCard } from './checkout-form-2-shipping-card'
19
+
20
+ export interface CheckoutForm2Item {
21
+ id: string
22
+ name: string
23
+ price: number
24
+ quantity: number
25
+ image: string
26
+ }
27
+
28
+ export interface CheckoutForm2CountryOption {
29
+ value: string
30
+ label: string
31
+ }
32
+
33
+ export interface CheckoutForm2Data {
34
+ email: string
35
+ firstName: string
36
+ lastName: string
37
+ address: string
38
+ city: string
39
+ zipCode: string
40
+ country: string
41
+ saveInfo: boolean
42
+ newsletter: boolean
43
+ cardNumber: string
44
+ expiryDate: string
45
+ cvv: string
46
+ cardName: string
47
+ }
48
+
49
+ export type CheckoutForm2FieldErrors = CheckoutFieldErrors<keyof CheckoutForm2Data>
50
+
51
+ export type CheckoutForm2FieldDescriptions = Partial<
52
+ Record<keyof CheckoutForm2Data, string>
53
+ >
54
+
55
+ export interface CheckoutForm2Totals {
56
+ subtotal: number
57
+ shipping: number
58
+ tax: number
59
+ total: number
60
+ }
61
+
62
+ export interface CheckoutForm2Props {
63
+ items: CheckoutForm2Item[]
64
+ countryOptions?: CheckoutForm2CountryOption[]
65
+ paymentMethods?: PaymentMethod[]
66
+ noMethodsLabel?: string
67
+ taxRate?: number
68
+ shippingCost?: number
69
+ freeShippingThreshold?: number
70
+ currencySymbol?: string
71
+ titleLabel?: string
72
+ descriptionLabel?: string
73
+ backToCartLabel?: string
74
+ completeOrderLabel?: string
75
+ contactLabel?: string
76
+ emailLabel?: string
77
+ emailPlaceholder?: string
78
+ newsletterLabel?: string
79
+ shippingAddressLabel?: string
80
+ firstNameLabel?: string
81
+ lastNameLabel?: string
82
+ addressLabel?: string
83
+ cityLabel?: string
84
+ zipCodeLabel?: string
85
+ countryLabel?: string
86
+ saveInfoLabel?: string
87
+ paymentLabel?: string
88
+ cardNumberLabel?: string
89
+ expiryLabel?: string
90
+ cvvLabel?: string
91
+ cardNameLabel?: string
92
+ encryptedNoteLabel?: string
93
+ orderSummaryLabel?: string
94
+ subtotalLabel?: string
95
+ shippingLineLabel?: string
96
+ freeShippingLineLabel?: string
97
+ taxLabel?: string
98
+ totalLabel?: string
99
+ freeShippingNoteLabel?: string
100
+ estimatedDeliveryLabel?: string
101
+ values: CheckoutForm2Data
102
+ onChange: (field: keyof CheckoutForm2Data, value: string | boolean) => void
103
+ fieldErrors?: CheckoutForm2FieldErrors
104
+ fieldDescriptions?: CheckoutForm2FieldDescriptions
105
+ onBackToCart?: () => void
106
+ onCompleteOrder?: (data: CheckoutForm2Data, totals: CheckoutForm2Totals) => void
107
+ }
108
+
109
+ const DEFAULT_COUNTRY_OPTIONS: CheckoutForm2CountryOption[] = [
110
+ { value: 'US', label: 'Estados Unidos' },
111
+ { value: 'CA', label: 'Canadá' },
112
+ { value: 'UK', label: 'Reino Unido' },
113
+ { value: 'AU', label: 'Australia' },
114
+ ]
115
+
116
+ export function CheckoutForm2({
117
+ items,
118
+ countryOptions = DEFAULT_COUNTRY_OPTIONS,
119
+ paymentMethods = [],
120
+ noMethodsLabel,
121
+ taxRate = 0.08,
122
+ shippingCost = 9.99,
123
+ freeShippingThreshold = 75,
124
+ currencySymbol = '$',
125
+ titleLabel = 'Finalizar compra',
126
+ descriptionLabel = 'Completa tu pedido de forma segura',
127
+ backToCartLabel = 'Volver al carrito',
128
+ completeOrderLabel = 'Completar pedido',
129
+ contactLabel = 'Información de contacto',
130
+ emailLabel = 'Correo electrónico',
131
+ emailPlaceholder = 'tucorreo@ejemplo.com',
132
+ newsletterLabel = 'Quiero recibir novedades y ofertas',
133
+ shippingAddressLabel = 'Dirección de envío',
134
+ firstNameLabel = 'Nombre',
135
+ lastNameLabel = 'Apellido',
136
+ addressLabel = 'Dirección',
137
+ cityLabel = 'Ciudad',
138
+ zipCodeLabel = 'Código postal',
139
+ countryLabel = 'País',
140
+ saveInfoLabel = 'Guardar esta información para la próxima vez',
141
+ paymentLabel = 'Método de pago',
142
+ cardNumberLabel = 'Número de tarjeta',
143
+ expiryLabel = 'Fecha de vencimiento',
144
+ cvvLabel = 'CVV',
145
+ cardNameLabel = 'Nombre en la tarjeta',
146
+ encryptedNoteLabel = 'Tu información de pago está cifrada y segura',
147
+ orderSummaryLabel = 'Resumen del pedido',
148
+ subtotalLabel = 'Subtotal',
149
+ shippingLineLabel = 'Envío',
150
+ freeShippingLineLabel = 'Gratis',
151
+ taxLabel = 'Impuestos',
152
+ totalLabel = 'Total',
153
+ freeShippingNoteLabel = 'Envío gratis en pedidos superiores a',
154
+ estimatedDeliveryLabel = 'Entrega estimada: 3-5 días hábiles',
155
+ values,
156
+ onChange,
157
+ fieldErrors,
158
+ fieldDescriptions,
159
+ onBackToCart,
160
+ onCompleteOrder,
161
+ }: CheckoutForm2Props) {
162
+ const [paymentMethod, setPaymentMethod] = useState(paymentMethods[0]?.id ?? '')
163
+
164
+ const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
165
+ const shipping = subtotal >= freeShippingThreshold ? 0 : shippingCost
166
+ const tax = subtotal * taxRate
167
+ const total = subtotal + shipping + tax
168
+
169
+ const handleCardNumberChange = (value: string) => {
170
+ onChange('cardNumber', formatCardNumber(value))
171
+ }
172
+
173
+ const handleExpiryChange = (value: string) => {
174
+ onChange('expiryDate', formatExpiryDate(value))
175
+ }
176
+
177
+ const handleSubmit = (event: FormEvent) => {
178
+ event.preventDefault()
179
+ onCompleteOrder?.(values, { subtotal, shipping, tax, total })
180
+ }
181
+
182
+ return (
183
+ <section className='py-8'>
184
+ <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
185
+ <div className='mb-8'>
186
+ <Button
187
+ type='button'
188
+ variant='secondary'
189
+ size='sm'
190
+ className='mb-4 h-8 px-3 text-xs cursor-pointer'
191
+ onClick={onBackToCart}
192
+ >
193
+ <ArrowLeft />
194
+ {backToCartLabel}
195
+ </Button>
196
+ <h1 className='text-3xl font-bold tracking-tight text-balance'>{titleLabel}</h1>
197
+ <p className='text-muted-foreground mt-2'>{descriptionLabel}</p>
198
+ </div>
199
+
200
+ <div className='grid gap-8 lg:grid-cols-3'>
201
+ <div className='order-2 lg:order-1 lg:col-span-2'>
202
+ <form onSubmit={handleSubmit} className='flex flex-col gap-8'>
203
+ <Card>
204
+ <CardHeader>
205
+ <CardTitle className='flex items-center gap-2'>
206
+ <Package className='size-5' aria-hidden='true' />
207
+ {contactLabel}
208
+ </CardTitle>
209
+ </CardHeader>
210
+ <CardContent>
211
+ <FieldGroup>
212
+ <Field data-invalid={!!fieldErrors?.email?.length}>
213
+ <FieldLabel htmlFor='email'>{emailLabel}</FieldLabel>
214
+ <Input
215
+ id='email'
216
+ type='text'
217
+ inputMode='email'
218
+ placeholder={emailPlaceholder}
219
+ value={values.email}
220
+ onChange={event => onChange('email', event.target.value)}
221
+ {...checkoutFieldAria('email', fieldErrors?.email)}
222
+ className='h-9'
223
+ />
224
+ <CheckoutFieldFeedback
225
+ id='email'
226
+ errors={fieldErrors?.email}
227
+ description={fieldDescriptions?.email}
228
+ />
229
+ </Field>
230
+ <Field orientation='horizontal'>
231
+ <CheckboxCheck
232
+ id='newsletter'
233
+ checked={values.newsletter}
234
+ onCheckedChange={checked => onChange('newsletter', checked as boolean)}
235
+ />
236
+ <FieldLabel htmlFor='newsletter' className='text-sm font-normal'>
237
+ {newsletterLabel}
238
+ </FieldLabel>
239
+ </Field>
240
+ </FieldGroup>
241
+ </CardContent>
242
+ </Card>
243
+
244
+ <CheckoutForm2ShippingCard
245
+ data={values}
246
+ onChange={onChange}
247
+ countryOptions={countryOptions}
248
+ shippingAddressLabel={shippingAddressLabel}
249
+ firstNameLabel={firstNameLabel}
250
+ lastNameLabel={lastNameLabel}
251
+ addressLabel={addressLabel}
252
+ cityLabel={cityLabel}
253
+ zipCodeLabel={zipCodeLabel}
254
+ countryLabel={countryLabel}
255
+ saveInfoLabel={saveInfoLabel}
256
+ fieldErrors={fieldErrors}
257
+ fieldDescriptions={fieldDescriptions}
258
+ />
259
+
260
+ <CheckoutForm2PaymentCard
261
+ paymentMethod={paymentMethod}
262
+ onPaymentMethodChange={setPaymentMethod}
263
+ data={values}
264
+ onChange={onChange}
265
+ onCardNumberChange={handleCardNumberChange}
266
+ onExpiryChange={handleExpiryChange}
267
+ paymentMethods={paymentMethods}
268
+ noMethodsLabel={noMethodsLabel}
269
+ paymentLabel={paymentLabel}
270
+ cardNumberLabel={cardNumberLabel}
271
+ expiryLabel={expiryLabel}
272
+ cvvLabel={cvvLabel}
273
+ cardNameLabel={cardNameLabel}
274
+ encryptedNoteLabel={encryptedNoteLabel}
275
+ fieldErrors={fieldErrors}
276
+ fieldDescriptions={fieldDescriptions}
277
+ />
278
+
279
+ <Button type='submit' size='lg' className='h-10 px-8 w-full cursor-pointer'>
280
+ <Lock />
281
+ {completeOrderLabel} • {currencySymbol}
282
+ {total.toFixed(2)}
283
+ </Button>
284
+ </form>
285
+ </div>
286
+
287
+ <div className='order-1 lg:order-2 lg:col-span-1'>
288
+ <CheckoutForm2OrderSummary
289
+ items={items}
290
+ subtotal={subtotal}
291
+ shipping={shipping}
292
+ tax={tax}
293
+ total={total}
294
+ currencySymbol={currencySymbol}
295
+ orderSummaryLabel={orderSummaryLabel}
296
+ subtotalLabel={subtotalLabel}
297
+ shippingLabel={shippingLineLabel}
298
+ freeShippingLabel={freeShippingLineLabel}
299
+ taxLabel={taxLabel}
300
+ totalLabel={totalLabel}
301
+ freeShippingNoteLabel={freeShippingNoteLabel}
302
+ estimatedDeliveryLabel={estimatedDeliveryLabel}
303
+ freeShippingThreshold={freeShippingThreshold}
304
+ />
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </section>
309
+ )
310
+ }
311
+
312
+ export default CheckoutForm2
@@ -0,0 +1,91 @@
1
+ 'use client'
2
+
3
+ import { CheckboxCheck } from './checkbox-check'
4
+ import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
5
+ import { Input } from '../../../ui/input'
6
+
7
+ import type { CheckoutForm3ChangeHandler, CheckoutForm3FormData } from './checkout-form-3'
8
+
9
+ interface CheckoutForm3InformationStepProps {
10
+ formData: CheckoutForm3FormData
11
+ onChange: CheckoutForm3ChangeHandler
12
+ emailLabel: string
13
+ firstNameLabel: string
14
+ lastNameLabel: string
15
+ phoneLabel: string
16
+ newsletterLabel: string
17
+ }
18
+
19
+ export function CheckoutForm3InformationStep({
20
+ formData,
21
+ onChange,
22
+ emailLabel,
23
+ firstNameLabel,
24
+ lastNameLabel,
25
+ phoneLabel,
26
+ newsletterLabel,
27
+ }: CheckoutForm3InformationStepProps) {
28
+ return (
29
+ <FieldGroup>
30
+ <Field>
31
+ <FieldLabel htmlFor='email'>{emailLabel} *</FieldLabel>
32
+ <Input
33
+ id='email'
34
+ type='email'
35
+ placeholder='tucorreo@ejemplo.com'
36
+ value={formData.email}
37
+ onChange={event => onChange('email', event.target.value)}
38
+ required
39
+ className='h-9'
40
+ />
41
+ </Field>
42
+
43
+ <div className='grid gap-4 sm:grid-cols-2'>
44
+ <Field>
45
+ <FieldLabel htmlFor='firstName'>{firstNameLabel} *</FieldLabel>
46
+ <Input
47
+ id='firstName'
48
+ placeholder='Juan'
49
+ value={formData.firstName}
50
+ onChange={event => onChange('firstName', event.target.value)}
51
+ required
52
+ className='h-9'
53
+ />
54
+ </Field>
55
+ <Field>
56
+ <FieldLabel htmlFor='lastName'>{lastNameLabel} *</FieldLabel>
57
+ <Input
58
+ id='lastName'
59
+ placeholder='Pérez'
60
+ value={formData.lastName}
61
+ onChange={event => onChange('lastName', event.target.value)}
62
+ required
63
+ className='h-9'
64
+ />
65
+ </Field>
66
+ </div>
67
+
68
+ <Field>
69
+ <FieldLabel htmlFor='phone'>{phoneLabel}</FieldLabel>
70
+ <Input
71
+ id='phone'
72
+ placeholder='+34 600 123 456'
73
+ value={formData.phone}
74
+ onChange={event => onChange('phone', event.target.value)}
75
+ className='h-9'
76
+ />
77
+ </Field>
78
+
79
+ <Field orientation='horizontal'>
80
+ <CheckboxCheck
81
+ id='newsletterCheck'
82
+ checked={formData.newsletter}
83
+ onCheckedChange={checked => onChange('newsletter', checked as boolean)}
84
+ />
85
+ <FieldLabel htmlFor='newsletterCheck' className='text-sm font-normal'>
86
+ {newsletterLabel}
87
+ </FieldLabel>
88
+ </Field>
89
+ </FieldGroup>
90
+ )
91
+ }
@@ -0,0 +1,110 @@
1
+ import { Badge } from '../../../ui/badge'
2
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
3
+ import { Separator } from '../../../ui/separator'
4
+
5
+ import type { CheckoutForm3CartItem } from './checkout-form-3'
6
+
7
+ interface CheckoutForm3OrderSummaryProps {
8
+ items: CheckoutForm3CartItem[]
9
+ subtotal: number
10
+ shipping: number
11
+ tax: number
12
+ total: number
13
+ currencySymbol: string
14
+ orderSummaryLabel: string
15
+ subtotalLabel: string
16
+ shippingLabel: string
17
+ taxLabel: string
18
+ totalLabel: string
19
+ freeLabel: string
20
+ }
21
+
22
+ export function CheckoutForm3OrderSummary({
23
+ items,
24
+ subtotal,
25
+ shipping,
26
+ tax,
27
+ total,
28
+ currencySymbol,
29
+ orderSummaryLabel,
30
+ subtotalLabel,
31
+ shippingLabel,
32
+ taxLabel,
33
+ totalLabel,
34
+ freeLabel,
35
+ }: CheckoutForm3OrderSummaryProps) {
36
+ return (
37
+ <Card className='sticky top-6'>
38
+ <CardHeader>
39
+ <CardTitle>{orderSummaryLabel}</CardTitle>
40
+ </CardHeader>
41
+ <CardContent className='flex flex-col gap-4'>
42
+ <div className='flex flex-col gap-3'>
43
+ {items.map(item => (
44
+ <div key={item.id} className='flex items-center gap-3'>
45
+ <div className='relative'>
46
+ <img
47
+ src={item.image || undefined}
48
+ alt={item.name}
49
+ width={48}
50
+ height={48}
51
+ className='size-12 rounded-md object-cover'
52
+ />
53
+ <Badge
54
+ variant='secondary'
55
+ className='px-2.5 py-0.5 font-semibold absolute -top-2 -end-2 size-5 rounded-full p-0 text-xs'
56
+ >
57
+ {item.quantity}
58
+ </Badge>
59
+ </div>
60
+ <div className='flex-1'>
61
+ <h4 className='text-sm font-medium'>{item.name}</h4>
62
+ <p className='text-muted-foreground text-sm'>
63
+ {currencySymbol}
64
+ {item.price}
65
+ </p>
66
+ </div>
67
+ <div className='text-sm font-medium'>
68
+ {currencySymbol}
69
+ {(item.price * item.quantity).toFixed(2)}
70
+ </div>
71
+ </div>
72
+ ))}
73
+ </div>
74
+
75
+ <Separator />
76
+
77
+ <div className='flex flex-col gap-2 text-sm'>
78
+ <div className='flex justify-between'>
79
+ <span>{subtotalLabel}</span>
80
+ <span>
81
+ {currencySymbol}
82
+ {subtotal.toFixed(2)}
83
+ </span>
84
+ </div>
85
+ <div className='flex justify-between'>
86
+ <span>{shippingLabel}</span>
87
+ <span>{shipping === 0 ? freeLabel : `${currencySymbol}${shipping.toFixed(2)}`}</span>
88
+ </div>
89
+ <div className='flex justify-between'>
90
+ <span>{taxLabel}</span>
91
+ <span>
92
+ {currencySymbol}
93
+ {tax.toFixed(2)}
94
+ </span>
95
+ </div>
96
+ </div>
97
+
98
+ <Separator />
99
+
100
+ <div className='flex justify-between text-lg font-semibold'>
101
+ <span>{totalLabel}</span>
102
+ <span>
103
+ {currencySymbol}
104
+ {total.toFixed(2)}
105
+ </span>
106
+ </div>
107
+ </CardContent>
108
+ </Card>
109
+ )
110
+ }