@thorprovider/blocks 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -15
- package/.turbo/turbo-test.log +42 -5
- package/AGENTS.md +1 -1
- package/README.md +94 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts.d.mts +1266 -97
- package/dist/contracts.d.ts +1266 -97
- package/dist/contracts.js +225 -9
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +207 -10
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +756 -96
- package/dist/index.d.ts +756 -96
- package/dist/index.js +5114 -3702
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4939 -3551
- package/dist/index.mjs.map +1 -1
- package/docs/standards/theming.md +32 -5
- package/package.json +2 -2
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +136 -93
- package/src/blocks/e-commerce/category-filters/category-filter-2.test.tsx +513 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +728 -148
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +14 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx +136 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +145 -99
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +215 -102
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +6 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +51 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +216 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +114 -11
- package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
- package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +42 -2
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +10 -8
- package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +51 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +56 -36
- package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +88 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +141 -80
- package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
- package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +182 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +298 -129
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +229 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +313 -110
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +72 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +20 -1
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +117 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +46 -9
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +38 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +204 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +144 -21
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +233 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +235 -60
- package/src/index.ts +56 -0
- package/src/lib/cart.test.ts +15 -0
- package/src/lib/cart.ts +11 -0
- package/src/lib/contracts-curated.test.ts +207 -0
- package/src/lib/contracts-server-boundary.test.ts +52 -0
- package/src/lib/contracts.ts +234 -6
- package/src/lib/money.test.ts +64 -0
- package/src/lib/money.ts +65 -0
- package/tsup.config.ts +0 -1
- package/vitest.config.ts +9 -1
|
@@ -1,139 +1,252 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { Gift, Shield, Truck } from 'lucide-react'
|
|
3
|
+
import { Shield, Truck } from 'lucide-react'
|
|
5
4
|
import { Badge } from '../../../ui/badge'
|
|
6
5
|
import { Button } from '../../../ui/button'
|
|
7
6
|
import { Card, CardContent, CardTitle } from '../../../ui/card'
|
|
8
7
|
import { Field, FieldLabel } from '../../../ui/field'
|
|
9
8
|
import { Input } from '../../../ui/input'
|
|
10
9
|
import { Separator } from '../../../ui/separator'
|
|
11
|
-
import
|
|
10
|
+
import { formatMoney, isAvailableMoney } from '../../../lib/money'
|
|
11
|
+
import type { Money } from '@thorprovider/types'
|
|
12
|
+
import type {
|
|
13
|
+
CheckoutForm1OrderSummary,
|
|
14
|
+
CheckoutForm1OrderSummaryItem,
|
|
15
|
+
CheckoutForm1TotalsState,
|
|
16
|
+
} from './checkout-form-1'
|
|
12
17
|
|
|
13
18
|
export interface CheckoutForm1OrderSummaryProps {
|
|
14
19
|
orderSummary: CheckoutForm1OrderSummary
|
|
20
|
+
totalsState?: CheckoutForm1TotalsState
|
|
21
|
+
totalsErrorLabel?: string
|
|
22
|
+
/** Shown when a required total amount is absent or malformed (never as zero). */
|
|
23
|
+
totalsUnavailableLabel?: string
|
|
24
|
+
/** Shown for a line whose supplied line total is absent or malformed. */
|
|
25
|
+
amountUnavailableLabel?: string
|
|
26
|
+
retryLabel?: string
|
|
27
|
+
onRetryTotals?: () => void
|
|
28
|
+
promoCode?: string
|
|
29
|
+
onPromoCodeChange?: (value: string) => void
|
|
30
|
+
onApplyPromo?: (code: string) => void
|
|
31
|
+
promoPending?: boolean
|
|
32
|
+
promoApplyLabel?: string
|
|
33
|
+
promoPlaceholder?: string
|
|
34
|
+
trustNotes?: string[]
|
|
35
|
+
/** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
|
|
36
|
+
formatMoney?: (money: Money) => string
|
|
15
37
|
}
|
|
16
38
|
|
|
17
|
-
export function CheckoutForm1OrderSummary({
|
|
18
|
-
|
|
39
|
+
export function CheckoutForm1OrderSummary({
|
|
40
|
+
orderSummary,
|
|
41
|
+
totalsState = 'idle',
|
|
42
|
+
totalsErrorLabel = 'No se pudieron actualizar los totales.',
|
|
43
|
+
totalsUnavailableLabel = 'Totales no disponibles.',
|
|
44
|
+
amountUnavailableLabel = '—',
|
|
45
|
+
retryLabel = 'Reintentar',
|
|
46
|
+
onRetryTotals,
|
|
47
|
+
promoCode,
|
|
48
|
+
onPromoCodeChange,
|
|
49
|
+
onApplyPromo,
|
|
50
|
+
promoPending = false,
|
|
51
|
+
promoApplyLabel = 'Aplicar',
|
|
52
|
+
promoPlaceholder = 'Ingresa el código',
|
|
53
|
+
trustNotes,
|
|
54
|
+
formatMoney: formatMoneyProp,
|
|
55
|
+
}: CheckoutForm1OrderSummaryProps) {
|
|
56
|
+
const format = (value: Money | null | undefined) => formatMoney(value, formatMoneyProp)
|
|
57
|
+
const lineAmount = (item: CheckoutForm1OrderSummaryItem): string | null => {
|
|
58
|
+
// A supplied line total that is absent/malformed must not fall back to a
|
|
59
|
+
// numeric guess; only an entirely absent line total uses the legacy field.
|
|
60
|
+
if (item.lineTotal !== undefined) return format(item.lineTotal)
|
|
61
|
+
if (typeof item.price === 'number') return `$${(item.price * item.quantity).toFixed(2)}`
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
19
64
|
|
|
20
|
-
const
|
|
21
|
-
(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<CardContent className="flex flex-col gap-4 p-6">
|
|
34
|
-
<CardTitle>Order Summary</CardTitle>
|
|
35
|
-
|
|
36
|
-
<div className="flex flex-col gap-4">
|
|
37
|
-
{orderSummary.items.map((item) => (
|
|
38
|
-
<div key={item.id} className="flex items-center gap-3">
|
|
39
|
-
<div className="relative">
|
|
40
|
-
<img
|
|
41
|
-
src={item.image || undefined}
|
|
42
|
-
alt={item.name}
|
|
43
|
-
width={64}
|
|
44
|
-
height={64}
|
|
45
|
-
className="size-16 rounded-lg object-cover"
|
|
46
|
-
/>
|
|
47
|
-
<Badge
|
|
48
|
-
variant="secondary"
|
|
49
|
-
className="absolute -right-2 -top-2 size-6 justify-center rounded-full p-0 text-xs"
|
|
50
|
-
>
|
|
51
|
-
{item.quantity}
|
|
52
|
-
</Badge>
|
|
53
|
-
</div>
|
|
54
|
-
<div className="flex-1">
|
|
55
|
-
<p className="text-sm font-medium">{item.name}</p>
|
|
56
|
-
<p className="text-sm text-muted-foreground">{item.variant}</p>
|
|
57
|
-
</div>
|
|
58
|
-
<p className="text-sm font-medium">${item.price.toFixed(2)}</p>
|
|
59
|
-
</div>
|
|
60
|
-
))}
|
|
65
|
+
const renderTotals = () => {
|
|
66
|
+
if (totalsState === 'pending') {
|
|
67
|
+
return <p className="text-muted-foreground text-sm" aria-live="polite">Actualizando totales…</p>
|
|
68
|
+
}
|
|
69
|
+
if (totalsState === 'error') {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col gap-2" role="alert">
|
|
72
|
+
<p className="text-destructive text-sm">{totalsErrorLabel}</p>
|
|
73
|
+
{onRetryTotals ? (
|
|
74
|
+
<Button type="button" variant="outline" size="sm" className="w-fit" onClick={onRetryTotals}>
|
|
75
|
+
{retryLabel}
|
|
76
|
+
</Button>
|
|
77
|
+
) : null}
|
|
61
78
|
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
const totals = orderSummary.totals
|
|
83
|
+
if (totals) {
|
|
84
|
+
const subtotalText = format(totals.subtotal)
|
|
85
|
+
const totalText = format(totals.total)
|
|
86
|
+
// Required totals: if either is absent/malformed, show an explicit
|
|
87
|
+
// unavailable state (with recovery) rather than a fabricated zero.
|
|
88
|
+
if (subtotalText === null || totalText === null) {
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex flex-col gap-2" role="status">
|
|
91
|
+
<p className="text-muted-foreground text-sm">{totalsUnavailableLabel}</p>
|
|
92
|
+
{onRetryTotals ? (
|
|
93
|
+
<Button type="button" variant="outline" size="sm" className="w-fit" onClick={onRetryTotals}>
|
|
94
|
+
{retryLabel}
|
|
95
|
+
</Button>
|
|
96
|
+
) : null}
|
|
76
97
|
</div>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
return (
|
|
81
101
|
<div className="flex flex-col gap-2 text-sm">
|
|
82
102
|
<div className="flex items-center justify-between">
|
|
83
103
|
<span className="text-muted-foreground">Subtotal</span>
|
|
84
|
-
<span className="font-medium"
|
|
104
|
+
<span className="font-medium tabular-nums">{subtotalText}</span>
|
|
85
105
|
</div>
|
|
86
|
-
|
|
87
|
-
<span className="flex items-center gap-1.5 text-muted-foreground">
|
|
88
|
-
<Truck className="size-4" />
|
|
89
|
-
Shipping
|
|
90
|
-
</span>
|
|
91
|
-
<span className="font-medium">${orderSummary.shipping.toFixed(2)}</span>
|
|
92
|
-
</div>
|
|
93
|
-
<div className="flex items-center justify-between">
|
|
94
|
-
<span className="text-muted-foreground">Tax</span>
|
|
95
|
-
<span className="font-medium">${orderSummary.tax.toFixed(2)}</span>
|
|
96
|
-
</div>
|
|
97
|
-
{(orderSummary.discount ?? 0) > 0 && (
|
|
106
|
+
{isAvailableMoney(totals.discount) ? (
|
|
98
107
|
<div className="flex items-center justify-between">
|
|
99
|
-
<span className="text-muted-foreground">
|
|
100
|
-
<span className="font-medium
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
<span className="text-muted-foreground">Descuento</span>
|
|
109
|
+
<span className="font-medium tabular-nums">{format(totals.discount)}</span>
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
112
|
+
{isAvailableMoney(totals.tax) ? (
|
|
113
|
+
<div className="flex items-center justify-between">
|
|
114
|
+
<span className="text-muted-foreground">Impuestos</span>
|
|
115
|
+
<span className="font-medium tabular-nums">{format(totals.tax)}</span>
|
|
103
116
|
</div>
|
|
104
|
-
)}
|
|
105
|
-
{(
|
|
117
|
+
) : null}
|
|
118
|
+
{isAvailableMoney(totals.shipping) ? (
|
|
106
119
|
<div className="flex items-center justify-between">
|
|
107
|
-
<span className="text-muted-foreground">
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
<span className="flex items-center gap-1.5 text-muted-foreground">
|
|
121
|
+
<Truck className="size-4" />
|
|
122
|
+
Envío
|
|
110
123
|
</span>
|
|
124
|
+
<span className="font-medium tabular-nums">{format(totals.shipping)}</span>
|
|
111
125
|
</div>
|
|
112
|
-
)}
|
|
126
|
+
) : null}
|
|
127
|
+
<Separator />
|
|
128
|
+
<div className="flex items-center justify-between">
|
|
129
|
+
<span className="font-semibold">Total</span>
|
|
130
|
+
<span className="font-semibold tabular-nums">{totalText}</span>
|
|
131
|
+
</div>
|
|
113
132
|
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
114
135
|
|
|
136
|
+
// Legacy numeric fallback for consumers that have not moved to `totals`.
|
|
137
|
+
const legacySubtotal = orderSummary.items.reduce(
|
|
138
|
+
(sum, item) => sum + (item.price ?? 0) * item.quantity,
|
|
139
|
+
0
|
|
140
|
+
)
|
|
141
|
+
const legacyTotal =
|
|
142
|
+
legacySubtotal +
|
|
143
|
+
(orderSummary.shipping ?? 0) +
|
|
144
|
+
(orderSummary.tax ?? 0) -
|
|
145
|
+
(orderSummary.discount ?? 0) -
|
|
146
|
+
(orderSummary.promoDiscount ?? 0)
|
|
147
|
+
return (
|
|
148
|
+
<div className="flex flex-col gap-2 text-sm">
|
|
149
|
+
<div className="flex items-center justify-between">
|
|
150
|
+
<span className="text-muted-foreground">Subtotal</span>
|
|
151
|
+
<span className="font-medium tabular-nums">${legacySubtotal.toFixed(2)}</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="flex items-center justify-between">
|
|
154
|
+
<span className="text-muted-foreground">Envío</span>
|
|
155
|
+
<span className="font-medium tabular-nums">${(orderSummary.shipping ?? 0).toFixed(2)}</span>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="flex items-center justify-between">
|
|
158
|
+
<span className="text-muted-foreground">Impuestos</span>
|
|
159
|
+
<span className="font-medium tabular-nums">${(orderSummary.tax ?? 0).toFixed(2)}</span>
|
|
160
|
+
</div>
|
|
115
161
|
<Separator />
|
|
116
|
-
|
|
117
162
|
<div className="flex items-center justify-between">
|
|
118
163
|
<span className="font-semibold">Total</span>
|
|
119
|
-
<span className="font-semibold">${
|
|
164
|
+
<span className="font-semibold tabular-nums">${legacyTotal.toFixed(2)}</span>
|
|
120
165
|
</div>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
121
169
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
170
|
+
return (
|
|
171
|
+
<Card className="sticky top-8">
|
|
172
|
+
<CardContent className="flex flex-col gap-4 p-6">
|
|
173
|
+
<CardTitle>Order Summary</CardTitle>
|
|
174
|
+
|
|
175
|
+
<div className="flex flex-col gap-4">
|
|
176
|
+
{orderSummary.items.map((item) => {
|
|
177
|
+
const amount = lineAmount(item)
|
|
178
|
+
return (
|
|
179
|
+
<div key={String(item.id)} className="flex items-center gap-3">
|
|
180
|
+
<div className="relative">
|
|
181
|
+
{item.image ? (
|
|
182
|
+
<img
|
|
183
|
+
src={item.image || undefined}
|
|
184
|
+
alt={item.name}
|
|
185
|
+
width={64}
|
|
186
|
+
height={64}
|
|
187
|
+
className="bg-muted size-16 rounded-lg object-cover"
|
|
188
|
+
/>
|
|
189
|
+
) : (
|
|
190
|
+
<div aria-hidden="true" className="bg-muted size-16 rounded-lg" />
|
|
191
|
+
)}
|
|
192
|
+
<Badge
|
|
193
|
+
variant="secondary"
|
|
194
|
+
className="absolute -right-2 -top-2 size-6 justify-center rounded-full p-0 text-xs"
|
|
195
|
+
>
|
|
196
|
+
{item.quantity}
|
|
197
|
+
</Badge>
|
|
198
|
+
</div>
|
|
199
|
+
<div className="flex-1">
|
|
200
|
+
<p className="text-sm font-medium">{item.name}</p>
|
|
201
|
+
{item.variant ? <p className="text-sm text-muted-foreground">{item.variant}</p> : null}
|
|
202
|
+
</div>
|
|
203
|
+
<p className="text-sm font-medium tabular-nums">{amount ?? amountUnavailableLabel}</p>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
})}
|
|
135
207
|
</div>
|
|
208
|
+
|
|
209
|
+
<Separator />
|
|
210
|
+
|
|
211
|
+
{onPromoCodeChange ? (
|
|
212
|
+
<Field className="gap-2">
|
|
213
|
+
<FieldLabel htmlFor="checkout-promo-code">Código promocional</FieldLabel>
|
|
214
|
+
<div className="flex gap-2">
|
|
215
|
+
<Input
|
|
216
|
+
id="checkout-promo-code"
|
|
217
|
+
placeholder={promoPlaceholder}
|
|
218
|
+
value={promoCode ?? ''}
|
|
219
|
+
onChange={(e) => onPromoCodeChange(e.target.value)}
|
|
220
|
+
/>
|
|
221
|
+
<Button
|
|
222
|
+
type="button"
|
|
223
|
+
variant="outline"
|
|
224
|
+
disabled={promoPending || !onApplyPromo}
|
|
225
|
+
onClick={() => onApplyPromo?.(promoCode ?? '')}
|
|
226
|
+
>
|
|
227
|
+
{promoApplyLabel}
|
|
228
|
+
</Button>
|
|
229
|
+
</div>
|
|
230
|
+
</Field>
|
|
231
|
+
) : null}
|
|
232
|
+
|
|
233
|
+
<Separator />
|
|
234
|
+
|
|
235
|
+
{renderTotals()}
|
|
236
|
+
|
|
237
|
+
{trustNotes && trustNotes.length > 0 ? (
|
|
238
|
+
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
|
|
239
|
+
{trustNotes.map((note, index) => (
|
|
240
|
+
<span key={`trust:${index}:${note}`} className="flex items-center gap-2">
|
|
241
|
+
<Shield className="size-4" />
|
|
242
|
+
{note}
|
|
243
|
+
</span>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
) : null}
|
|
136
247
|
</CardContent>
|
|
137
248
|
</Card>
|
|
138
249
|
)
|
|
139
|
-
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default CheckoutForm1OrderSummary
|
|
@@ -28,6 +28,8 @@ export interface CheckoutForm1PaymentStepProps {
|
|
|
28
28
|
onExpiryChange: (value: string) => void
|
|
29
29
|
paymentMethods?: PaymentMethod[]
|
|
30
30
|
noPaymentMethodsLabel?: string
|
|
31
|
+
/** Raw card fields are legacy-only; the launch flow defaults to `false`. */
|
|
32
|
+
collectCardFields?: boolean
|
|
31
33
|
fieldErrors?: CheckoutForm1FieldErrors
|
|
32
34
|
fieldDescriptions?: CheckoutForm1FieldDescriptions
|
|
33
35
|
}
|
|
@@ -39,6 +41,7 @@ export function CheckoutForm1PaymentStep({
|
|
|
39
41
|
onExpiryChange,
|
|
40
42
|
paymentMethods = [],
|
|
41
43
|
noPaymentMethodsLabel = 'No payment methods are available for this region.',
|
|
44
|
+
collectCardFields = false,
|
|
42
45
|
fieldErrors,
|
|
43
46
|
fieldDescriptions,
|
|
44
47
|
}: CheckoutForm1PaymentStepProps) {
|
|
@@ -46,6 +49,7 @@ export function CheckoutForm1PaymentStep({
|
|
|
46
49
|
|
|
47
50
|
const selectedMethod = paymentMethods.find(method => method.id === formData.paymentMethod)
|
|
48
51
|
const isCard = selectedMethod?.type === 'card'
|
|
52
|
+
const showCardFields = collectCardFields && isCard
|
|
49
53
|
|
|
50
54
|
return (
|
|
51
55
|
<FieldSet>
|
|
@@ -70,14 +74,14 @@ export function CheckoutForm1PaymentStep({
|
|
|
70
74
|
</RadioGroup>
|
|
71
75
|
)}
|
|
72
76
|
|
|
73
|
-
{selectedMethod && !
|
|
77
|
+
{selectedMethod && !showCardFields ? (
|
|
74
78
|
<div className="bg-muted/50 rounded-lg p-4 text-sm">
|
|
75
79
|
<p className="font-medium">{selectedMethod.name}</p>
|
|
76
80
|
{selectedMethod.description ? <p className="text-muted-foreground mt-1">{selectedMethod.description}</p> : null}
|
|
77
81
|
</div>
|
|
78
82
|
) : null}
|
|
79
83
|
|
|
80
|
-
{
|
|
84
|
+
{showCardFields ? (
|
|
81
85
|
<>
|
|
82
86
|
<Field data-invalid={!!fieldErrors?.cardNumber?.length}>
|
|
83
87
|
<FieldLabel htmlFor="checkout-card-number">Card number</FieldLabel>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { Field, FieldGroup, FieldLabel } from '../../../ui/field'
|
|
3
|
+
import { Field, FieldContent, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '../../../ui/field'
|
|
4
4
|
import { Input } from '../../../ui/input'
|
|
5
|
+
import { RadioGroup, RadioGroupItem } from '../../../ui/radio-group'
|
|
6
|
+
import { formatMoney, isAvailableMoney } from '../../../lib/money'
|
|
7
|
+
import type { Money } from '@thorprovider/types'
|
|
5
8
|
import { CheckoutFieldFeedback, checkoutFieldAria } from './checkout-field-feedback'
|
|
6
9
|
import type {
|
|
7
10
|
CheckoutForm1ChangeHandler,
|
|
@@ -9,6 +12,7 @@ import type {
|
|
|
9
12
|
CheckoutForm1FieldDescriptions,
|
|
10
13
|
CheckoutForm1FieldErrors,
|
|
11
14
|
CheckoutForm1FormData,
|
|
15
|
+
CheckoutForm1ShippingOption,
|
|
12
16
|
} from './checkout-form-1'
|
|
13
17
|
import { CheckoutForm1CountryField } from './checkout-form-1-country-field'
|
|
14
18
|
|
|
@@ -16,6 +20,11 @@ export interface CheckoutForm1ShippingStepProps {
|
|
|
16
20
|
formData: CheckoutForm1FormData
|
|
17
21
|
onChange: CheckoutForm1ChangeHandler
|
|
18
22
|
countries?: CheckoutForm1CountryOption[]
|
|
23
|
+
shippingOptions?: CheckoutForm1ShippingOption[]
|
|
24
|
+
selectedShippingOptionId?: string
|
|
25
|
+
onSelectShippingOption?: (id: string) => void
|
|
26
|
+
/** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
|
|
27
|
+
formatMoney?: (money: Money) => string
|
|
19
28
|
fieldErrors?: CheckoutForm1FieldErrors
|
|
20
29
|
fieldDescriptions?: CheckoutForm1FieldDescriptions
|
|
21
30
|
}
|
|
@@ -24,6 +33,10 @@ export function CheckoutForm1ShippingStep({
|
|
|
24
33
|
formData,
|
|
25
34
|
onChange,
|
|
26
35
|
countries,
|
|
36
|
+
shippingOptions,
|
|
37
|
+
selectedShippingOptionId,
|
|
38
|
+
onSelectShippingOption,
|
|
39
|
+
formatMoney: formatMoneyProp,
|
|
27
40
|
fieldErrors,
|
|
28
41
|
fieldDescriptions,
|
|
29
42
|
}: CheckoutForm1ShippingStepProps) {
|
|
@@ -102,6 +115,42 @@ export function CheckoutForm1ShippingStep({
|
|
|
102
115
|
/>
|
|
103
116
|
)}
|
|
104
117
|
</div>
|
|
118
|
+
|
|
119
|
+
{shippingOptions && shippingOptions.length > 0 ? (
|
|
120
|
+
<FieldSet>
|
|
121
|
+
<FieldLegend>Shipping method</FieldLegend>
|
|
122
|
+
<RadioGroup
|
|
123
|
+
value={selectedShippingOptionId ?? ''}
|
|
124
|
+
onValueChange={(value) => value && onSelectShippingOption?.(String(value))}
|
|
125
|
+
className="grid gap-3"
|
|
126
|
+
>
|
|
127
|
+
{shippingOptions.map((option) => (
|
|
128
|
+
<Field
|
|
129
|
+
key={option.id}
|
|
130
|
+
orientation="horizontal"
|
|
131
|
+
className="justify-between rounded-lg border p-4"
|
|
132
|
+
>
|
|
133
|
+
<FieldContent>
|
|
134
|
+
<FieldLabel htmlFor={`shipping-${option.id}`} className="flex flex-col gap-0.5">
|
|
135
|
+
<span>{option.name}</span>
|
|
136
|
+
{option.description ? (
|
|
137
|
+
<span className="text-muted-foreground text-xs font-normal">
|
|
138
|
+
{option.description}
|
|
139
|
+
</span>
|
|
140
|
+
) : null}
|
|
141
|
+
</FieldLabel>
|
|
142
|
+
</FieldContent>
|
|
143
|
+
<div className="flex items-center gap-3">
|
|
144
|
+
{isAvailableMoney(option.price) ? (
|
|
145
|
+
<span className="text-sm tabular-nums">{formatMoney(option.price, formatMoneyProp)}</span>
|
|
146
|
+
) : null}
|
|
147
|
+
<RadioGroupItem value={option.id} id={`shipping-${option.id}`} />
|
|
148
|
+
</div>
|
|
149
|
+
</Field>
|
|
150
|
+
))}
|
|
151
|
+
</RadioGroup>
|
|
152
|
+
</FieldSet>
|
|
153
|
+
) : null}
|
|
105
154
|
</FieldGroup>
|
|
106
155
|
)
|
|
107
|
-
}
|
|
156
|
+
}
|