@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,235 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Lock } from 'lucide-react'
5
+ import { Button } from '../../../ui/button'
6
+ import { Card, CardContent } from '../../../ui/card'
7
+ import { cn } from '../../../lib/utils'
8
+ import { CheckoutForm1ContactStep } from './checkout-form-1-contact-step'
9
+ import { CheckoutForm1ShippingStep } from './checkout-form-1-shipping-step'
10
+ import { CheckoutForm1PaymentStep } from './checkout-form-1-payment-step'
11
+ import { CheckoutForm1OrderSummary } from './checkout-form-1-order-summary'
12
+ import { CheckoutForm1Progress } from './checkout-form-1-progress'
13
+ import { formatCardNumber, formatExpiryDate } from './card-formatting'
14
+ import type { PaymentMethod } from '@thorprovider/types'
15
+ import type { CheckoutFieldErrors } from './checkout-form-types'
16
+
17
+ export interface CheckoutForm1FormData {
18
+ email: string
19
+ firstName: string
20
+ lastName: string
21
+ phone: string
22
+ address: string
23
+ city: string
24
+ state: string
25
+ zipCode: string
26
+ country: string
27
+ paymentMethod: string
28
+ cardNumber: string
29
+ expiryDate: string
30
+ cvv: string
31
+ cardName: string
32
+ saveInfo: boolean
33
+ sameAsBilling: boolean
34
+ newsletter: boolean
35
+ promoCode: string
36
+ }
37
+
38
+ export type CheckoutForm1FieldErrors = CheckoutFieldErrors<keyof CheckoutForm1FormData>
39
+
40
+ export type CheckoutForm1FieldDescriptions = Partial<
41
+ Record<keyof CheckoutForm1FormData, string>
42
+ >
43
+
44
+ export interface CheckoutForm1OrderSummaryItem {
45
+ id: string | number
46
+ name: string
47
+ variant: string
48
+ price: number
49
+ quantity: number
50
+ image: string
51
+ }
52
+
53
+ export interface CheckoutForm1OrderSummary {
54
+ items: CheckoutForm1OrderSummaryItem[]
55
+ shipping: number
56
+ tax: number
57
+ discount?: number
58
+ promoDiscount?: number
59
+ }
60
+
61
+ export interface CheckoutForm1CountryOption {
62
+ value: string
63
+ label: string
64
+ }
65
+
66
+ export type CheckoutForm1ChangeHandler = (
67
+ field: keyof CheckoutForm1FormData,
68
+ value: string | boolean
69
+ ) => void
70
+
71
+ const STEP_COPY = [
72
+ { title: 'Información de contacto', description: 'La usaremos para enviarte novedades de tu pedido.' },
73
+ { title: 'Dirección de envío', description: 'Enviaremos tu pedido a esta dirección.' },
74
+ { title: 'Datos de pago', description: 'Pago seguro para tu pedido.' },
75
+ ] as const
76
+
77
+ export interface CheckoutForm1Props {
78
+ title?: string
79
+ description?: string
80
+ orderSummary?: CheckoutForm1OrderSummary
81
+ countries?: CheckoutForm1CountryOption[]
82
+ paymentMethods?: PaymentMethod[]
83
+ noPaymentMethodsLabel?: string
84
+ backLabel?: string
85
+ completeOrderLabel?: string
86
+ continueLabel?: string
87
+ values: CheckoutForm1FormData
88
+ onChange: CheckoutForm1ChangeHandler
89
+ fieldErrors?: CheckoutForm1FieldErrors
90
+ fieldDescriptions?: CheckoutForm1FieldDescriptions
91
+ validateStep?: (step: number) => boolean
92
+ onSubmit?: (data: CheckoutForm1FormData) => void
93
+ /** Disables the submit button while the order is being placed. */
94
+ submitting?: boolean
95
+ /** Order-level error surfaced above the footer actions. */
96
+ submitError?: string
97
+ }
98
+
99
+ export default function CheckoutForm1({
100
+ title,
101
+ description,
102
+ orderSummary,
103
+ countries,
104
+ paymentMethods,
105
+ noPaymentMethodsLabel,
106
+ backLabel = 'Atrás',
107
+ completeOrderLabel = 'Completar pedido',
108
+ continueLabel = 'Continuar',
109
+ values,
110
+ onChange,
111
+ fieldErrors,
112
+ fieldDescriptions,
113
+ validateStep,
114
+ onSubmit,
115
+ submitting = false,
116
+ submitError,
117
+ }: CheckoutForm1Props) {
118
+ const [step, setStep] = useState(1)
119
+
120
+ const handleCardNumberChange = (value: string) => {
121
+ onChange('cardNumber', formatCardNumber(value))
122
+ }
123
+
124
+ const handleExpiryChange = (value: string) => {
125
+ onChange('expiryDate', formatExpiryDate(value))
126
+ }
127
+
128
+ const handleContinue = () => {
129
+ if (validateStep && !validateStep(step)) return
130
+ setStep((s) => Math.min(STEP_COPY.length, s + 1))
131
+ }
132
+
133
+ const handleSubmit = () => {
134
+ if (validateStep) {
135
+ const allValid = Array.from({ length: STEP_COPY.length }, (_, index) => index + 1).every(
136
+ (currentStep) => validateStep(currentStep)
137
+ )
138
+ if (!allValid) return
139
+ }
140
+ onSubmit?.(values)
141
+ }
142
+
143
+ const stepCount = STEP_COPY.length
144
+ const isLastStep = step === stepCount
145
+
146
+ return (
147
+ <div className="mx-auto max-w-6xl px-4 py-12 sm:px-6 lg:px-8">
148
+ {(title || description) && (
149
+ <header className="mb-8 text-center">
150
+ {title && <h1 className="text-3xl font-bold tracking-tight sm:text-4xl">{title}</h1>}
151
+ {description && <p className="mt-2 text-muted-foreground">{description}</p>}
152
+ </header>
153
+ )}
154
+
155
+ <CheckoutForm1Progress step={step} />
156
+
157
+ <div className="mt-8 grid gap-6 lg:grid-cols-3">
158
+ <Card className={cn('lg:col-span-2', !orderSummary && 'lg:col-span-3')}>
159
+ <CardContent className="p-6">
160
+ <div className="mb-6">
161
+ <h2 className="text-lg font-semibold">{STEP_COPY[step - 1].title}</h2>
162
+ <p className="text-sm text-muted-foreground">{STEP_COPY[step - 1].description}</p>
163
+ </div>
164
+
165
+ {step === 1 && (
166
+ <CheckoutForm1ContactStep
167
+ formData={values}
168
+ onChange={onChange}
169
+ fieldErrors={fieldErrors}
170
+ fieldDescriptions={fieldDescriptions}
171
+ />
172
+ )}
173
+ {step === 2 && (
174
+ <CheckoutForm1ShippingStep
175
+ formData={values}
176
+ onChange={onChange}
177
+ countries={countries}
178
+ fieldErrors={fieldErrors}
179
+ fieldDescriptions={fieldDescriptions}
180
+ />
181
+ )}
182
+ {step === 3 && (
183
+ <CheckoutForm1PaymentStep
184
+ formData={values}
185
+ onChange={onChange}
186
+ onCardNumberChange={handleCardNumberChange}
187
+ onExpiryChange={handleExpiryChange}
188
+ paymentMethods={paymentMethods}
189
+ noPaymentMethodsLabel={noPaymentMethodsLabel}
190
+ fieldErrors={fieldErrors}
191
+ fieldDescriptions={fieldDescriptions}
192
+ />
193
+ )}
194
+
195
+ {submitError && (
196
+ <div
197
+ role="alert"
198
+ className="mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
199
+ >
200
+ {submitError}
201
+ </div>
202
+ )}
203
+
204
+ <div className="mt-6 flex items-center justify-between gap-4">
205
+ <Button
206
+ type="button"
207
+ variant="outline"
208
+ onClick={() => setStep((s) => Math.max(1, s - 1))}
209
+ disabled={step === 1 || submitting}
210
+ >
211
+ {backLabel}
212
+ </Button>
213
+ {isLastStep ? (
214
+ <Button type="button" onClick={handleSubmit} disabled={submitting}>
215
+ <Lock className="size-4" />
216
+ {submitting ? 'Procesando pedido…' : completeOrderLabel}
217
+ </Button>
218
+ ) : (
219
+ <Button type="button" onClick={handleContinue} disabled={submitting}>
220
+ {continueLabel}
221
+ </Button>
222
+ )}
223
+ </div>
224
+ </CardContent>
225
+ </Card>
226
+
227
+ {orderSummary && (
228
+ <div className="lg:col-span-1">
229
+ <CheckoutForm1OrderSummary orderSummary={orderSummary} />
230
+ </div>
231
+ )}
232
+ </div>
233
+ </div>
234
+ )
235
+ }
@@ -0,0 +1,130 @@
1
+ import { Truck } from 'lucide-react'
2
+
3
+ import { Badge } from '../../../ui/badge'
4
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
5
+ import { Separator } from '../../../ui/separator'
6
+ import type { CheckoutForm2Item } from './checkout-form-2'
7
+
8
+ interface CheckoutForm2OrderSummaryProps {
9
+ items: CheckoutForm2Item[]
10
+ subtotal: number
11
+ shipping: number
12
+ tax: number
13
+ total: number
14
+ currencySymbol: string
15
+ orderSummaryLabel: string
16
+ subtotalLabel: string
17
+ shippingLabel: string
18
+ freeShippingLabel: string
19
+ taxLabel: string
20
+ totalLabel: string
21
+ freeShippingNoteLabel: string
22
+ estimatedDeliveryLabel: string
23
+ freeShippingThreshold: number
24
+ }
25
+
26
+ export function CheckoutForm2OrderSummary({
27
+ items,
28
+ subtotal,
29
+ shipping,
30
+ tax,
31
+ total,
32
+ currencySymbol,
33
+ orderSummaryLabel,
34
+ subtotalLabel,
35
+ shippingLabel,
36
+ freeShippingLabel,
37
+ taxLabel,
38
+ totalLabel,
39
+ freeShippingNoteLabel,
40
+ estimatedDeliveryLabel,
41
+ freeShippingThreshold,
42
+ }: CheckoutForm2OrderSummaryProps) {
43
+ return (
44
+ <Card className='sticky top-6'>
45
+ <CardHeader>
46
+ <CardTitle>{orderSummaryLabel}</CardTitle>
47
+ </CardHeader>
48
+ <CardContent className='flex flex-col gap-4'>
49
+ <div className='flex flex-col gap-3'>
50
+ {items.map(item => (
51
+ <div key={item.id} className='flex items-center gap-3'>
52
+ <div className='relative'>
53
+ <img
54
+ src={item.image || undefined}
55
+ alt={item.name}
56
+ width={48}
57
+ height={48}
58
+ className='size-12 rounded-md object-cover'
59
+ />
60
+ <Badge
61
+ variant='secondary'
62
+ className='absolute -top-2 -end-2 size-5 rounded-full p-0 text-xs font-semibold'
63
+ >
64
+ {item.quantity}
65
+ </Badge>
66
+ </div>
67
+ <div className='flex-1'>
68
+ <h4 className='text-sm font-medium'>{item.name}</h4>
69
+ <p className='text-muted-foreground text-sm'>
70
+ {currencySymbol}
71
+ {item.price} × {item.quantity}
72
+ </p>
73
+ </div>
74
+ <div className='text-sm font-medium'>
75
+ {currencySymbol}
76
+ {(item.price * item.quantity).toFixed(2)}
77
+ </div>
78
+ </div>
79
+ ))}
80
+ </div>
81
+
82
+ <Separator />
83
+
84
+ <div className='flex flex-col gap-2 text-sm'>
85
+ <div className='flex justify-between'>
86
+ <span>{subtotalLabel}</span>
87
+ <span>
88
+ {currencySymbol}
89
+ {subtotal.toFixed(2)}
90
+ </span>
91
+ </div>
92
+ <div className='flex justify-between'>
93
+ <span>{shippingLabel}</span>
94
+ <span>{shipping === 0 ? freeShippingLabel : `${currencySymbol}${shipping.toFixed(2)}`}</span>
95
+ </div>
96
+ <div className='flex justify-between'>
97
+ <span>{taxLabel}</span>
98
+ <span>
99
+ {currencySymbol}
100
+ {tax.toFixed(2)}
101
+ </span>
102
+ </div>
103
+ </div>
104
+
105
+ <Separator />
106
+
107
+ <div className='flex justify-between text-lg font-semibold'>
108
+ <span>{totalLabel}</span>
109
+ <span>
110
+ {currencySymbol}
111
+ {total.toFixed(2)}
112
+ </span>
113
+ </div>
114
+
115
+ {shipping === 0 ? (
116
+ <div className='bg-muted/50 rounded-lg p-3 text-sm'>
117
+ <div className='flex items-center gap-2 font-medium'>
118
+ <Truck className='size-4' aria-hidden='true' />
119
+ <span>
120
+ {freeShippingNoteLabel} {currencySymbol}
121
+ {freeShippingThreshold}
122
+ </span>
123
+ </div>
124
+ <p className='text-muted-foreground mt-1'>{estimatedDeliveryLabel}</p>
125
+ </div>
126
+ ) : null}
127
+ </CardContent>
128
+ </Card>
129
+ )
130
+ }
@@ -0,0 +1,175 @@
1
+ import { CreditCard, Shield } from 'lucide-react'
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
4
+ import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
5
+ import { Input } from '../../../ui/input'
6
+ import { RadioGroup, RadioGroupItem } from '../../../ui/radio-group'
7
+ import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
8
+ import type { PaymentMethod } from '@thorprovider/types'
9
+ import type {
10
+ CheckoutForm2Data,
11
+ CheckoutForm2FieldDescriptions,
12
+ CheckoutForm2FieldErrors,
13
+ } from './checkout-form-2'
14
+
15
+ interface CheckoutForm2PaymentCardProps {
16
+ paymentMethod: string
17
+ onPaymentMethodChange: (method: string) => void
18
+ data: CheckoutForm2Data
19
+ onChange: (field: keyof CheckoutForm2Data, value: string | boolean) => void
20
+ onCardNumberChange: (value: string) => void
21
+ onExpiryChange: (value: string) => void
22
+ paymentMethods?: PaymentMethod[]
23
+ noMethodsLabel?: string
24
+ paymentLabel: string
25
+ cardNumberLabel: string
26
+ expiryLabel: string
27
+ cvvLabel: string
28
+ cardNameLabel: string
29
+ encryptedNoteLabel: string
30
+ fieldErrors?: CheckoutForm2FieldErrors
31
+ fieldDescriptions?: CheckoutForm2FieldDescriptions
32
+ }
33
+
34
+ export function CheckoutForm2PaymentCard({
35
+ paymentMethod,
36
+ onPaymentMethodChange,
37
+ data,
38
+ onChange,
39
+ onCardNumberChange,
40
+ onExpiryChange,
41
+ paymentMethods = [],
42
+ noMethodsLabel = 'No hay métodos de pago disponibles para esta región.',
43
+ paymentLabel,
44
+ cardNumberLabel,
45
+ expiryLabel,
46
+ cvvLabel,
47
+ cardNameLabel,
48
+ encryptedNoteLabel,
49
+ fieldErrors,
50
+ fieldDescriptions,
51
+ }: CheckoutForm2PaymentCardProps) {
52
+ const selectedMethod = paymentMethods.find(method => method.id === paymentMethod)
53
+ const isCard = selectedMethod?.type === 'card'
54
+
55
+ return (
56
+ <Card>
57
+ <CardHeader>
58
+ <CardTitle className='flex items-center gap-2'>
59
+ <CreditCard className='size-5' aria-hidden='true' />
60
+ {paymentLabel}
61
+ </CardTitle>
62
+ </CardHeader>
63
+ <CardContent className='flex flex-col gap-4'>
64
+ {paymentMethods.length === 0 ? (
65
+ <p className='text-muted-foreground text-sm'>{noMethodsLabel}</p>
66
+ ) : (
67
+ <RadioGroup value={paymentMethod} onValueChange={value => value && onPaymentMethodChange(value)}>
68
+ {paymentMethods.map(method => (
69
+ <Field key={method.id} orientation='horizontal'>
70
+ <RadioGroupItem value={method.id} id={method.id} />
71
+ <FieldLabel htmlFor={method.id} className='flex flex-col gap-0.5 font-normal'>
72
+ <span className='flex items-center gap-2'>
73
+ <CreditCard className='size-4' aria-hidden='true' />
74
+ {method.name}
75
+ </span>
76
+ {method.description ? (
77
+ <span className='text-muted-foreground text-xs'>{method.description}</span>
78
+ ) : null}
79
+ </FieldLabel>
80
+ </Field>
81
+ ))}
82
+ </RadioGroup>
83
+ )}
84
+
85
+ {selectedMethod && !isCard ? (
86
+ <div className='bg-muted/50 rounded-lg p-4 text-sm'>
87
+ <p className='font-medium'>{selectedMethod.name}</p>
88
+ {selectedMethod.description ? <p className='text-muted-foreground mt-1'>{selectedMethod.description}</p> : null}
89
+ </div>
90
+ ) : null}
91
+
92
+ {isCard ? (
93
+ <div className='border-muted border-l-2 ps-6'>
94
+ <FieldGroup>
95
+ <Field data-invalid={!!fieldErrors?.cardNumber?.length}>
96
+ <FieldLabel htmlFor='cardNumber'>{cardNumberLabel}</FieldLabel>
97
+ <Input
98
+ id='cardNumber'
99
+ placeholder='1234 5678 9012 3456'
100
+ inputMode='numeric'
101
+ value={data.cardNumber}
102
+ onChange={event => onCardNumberChange(event.target.value)}
103
+ {...checkoutFieldAria('cardNumber', fieldErrors?.cardNumber)}
104
+ className='h-9 font-mono'
105
+ />
106
+ <CheckoutFieldFeedback
107
+ id='cardNumber'
108
+ errors={fieldErrors?.cardNumber}
109
+ description={fieldDescriptions?.cardNumber}
110
+ />
111
+ </Field>
112
+ <div className='grid gap-4 sm:grid-cols-2 md:grid-cols-3'>
113
+ <Field data-invalid={!!fieldErrors?.expiryDate?.length} className='sm:col-span-2'>
114
+ <FieldLabel htmlFor='expiryDate'>{expiryLabel}</FieldLabel>
115
+ <Input
116
+ id='expiryDate'
117
+ placeholder='MM/AA'
118
+ inputMode='numeric'
119
+ value={data.expiryDate}
120
+ onChange={event => onExpiryChange(event.target.value)}
121
+ {...checkoutFieldAria('expiryDate', fieldErrors?.expiryDate)}
122
+ className='h-9 font-mono'
123
+ />
124
+ <CheckoutFieldFeedback
125
+ id='expiryDate'
126
+ errors={fieldErrors?.expiryDate}
127
+ description={fieldDescriptions?.expiryDate}
128
+ />
129
+ </Field>
130
+ <Field data-invalid={!!fieldErrors?.cvv?.length}>
131
+ <FieldLabel htmlFor='cvv'>{cvvLabel}</FieldLabel>
132
+ <Input
133
+ id='cvv'
134
+ placeholder='123'
135
+ inputMode='numeric'
136
+ value={data.cvv}
137
+ onChange={event => onChange('cvv', event.target.value)}
138
+ {...checkoutFieldAria('cvv', fieldErrors?.cvv)}
139
+ className='h-9 font-mono'
140
+ />
141
+ <CheckoutFieldFeedback
142
+ id='cvv'
143
+ errors={fieldErrors?.cvv}
144
+ description={fieldDescriptions?.cvv}
145
+ />
146
+ </Field>
147
+ </div>
148
+ <Field data-invalid={!!fieldErrors?.cardName?.length}>
149
+ <FieldLabel htmlFor='cardName'>{cardNameLabel}</FieldLabel>
150
+ <Input
151
+ id='cardName'
152
+ placeholder='Juan Pérez'
153
+ value={data.cardName}
154
+ onChange={event => onChange('cardName', event.target.value)}
155
+ {...checkoutFieldAria('cardName', fieldErrors?.cardName)}
156
+ className='h-9'
157
+ />
158
+ <CheckoutFieldFeedback
159
+ id='cardName'
160
+ errors={fieldErrors?.cardName}
161
+ description={fieldDescriptions?.cardName}
162
+ />
163
+ </Field>
164
+ </FieldGroup>
165
+ </div>
166
+ ) : null}
167
+
168
+ <div className='bg-muted/50 flex items-center gap-2 rounded-lg p-3 text-sm'>
169
+ <Shield className='size-4 text-success' aria-hidden='true' />
170
+ <span>{encryptedNoteLabel}</span>
171
+ </div>
172
+ </CardContent>
173
+ </Card>
174
+ )
175
+ }