@thorprovider/blocks 0.1.1 → 0.1.2

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 (56) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/.turbo/turbo-test.log +32 -5
  3. package/AGENTS.md +1 -1
  4. package/README.md +52 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/contracts.d.mts +1111 -90
  7. package/dist/contracts.d.ts +1111 -90
  8. package/dist/contracts.js +189 -6
  9. package/dist/contracts.js.map +1 -1
  10. package/dist/contracts.mjs +173 -7
  11. package/dist/contracts.mjs.map +1 -1
  12. package/dist/index.d.mts +467 -89
  13. package/dist/index.d.ts +467 -89
  14. package/dist/index.js +2967 -2250
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +2989 -2282
  17. package/dist/index.mjs.map +1 -1
  18. package/docs/standards/theming.md +32 -5
  19. package/package.json +2 -2
  20. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +136 -93
  21. package/src/blocks/e-commerce/category-filters/category-filter-2.test.tsx +513 -0
  22. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +728 -148
  23. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +14 -8
  24. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx +136 -0
  25. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +145 -99
  26. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +215 -102
  27. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +6 -2
  28. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +51 -2
  29. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +216 -0
  30. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +114 -11
  31. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +42 -2
  32. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +10 -8
  33. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +51 -0
  34. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +56 -36
  35. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +182 -0
  36. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +298 -129
  37. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +137 -0
  38. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +231 -89
  39. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +65 -0
  40. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +19 -0
  41. package/src/blocks/marketing/footers/footer-section-1.test.tsx +117 -0
  42. package/src/blocks/marketing/footers/footer-section-1.tsx +46 -9
  43. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +54 -0
  44. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +43 -13
  45. package/src/blocks/marketing/navbars/navbar-4.test.tsx +112 -0
  46. package/src/blocks/marketing/navbars/navbar-4.tsx +101 -28
  47. package/src/index.ts +38 -0
  48. package/src/lib/cart.test.ts +15 -0
  49. package/src/lib/cart.ts +11 -0
  50. package/src/lib/contracts-curated.test.ts +207 -0
  51. package/src/lib/contracts-server-boundary.test.ts +52 -0
  52. package/src/lib/contracts.ts +182 -4
  53. package/src/lib/money.test.ts +64 -0
  54. package/src/lib/money.ts +65 -0
  55. package/tsup.config.ts +0 -1
  56. package/vitest.config.ts +9 -1
@@ -23,8 +23,8 @@ const STAGE_LABELS: Record<FulfilmentStage, string> = {
23
23
 
24
24
  interface CheckoutConfirmationFulfilmentProgressProps {
25
25
  order: {
26
- currentStage: FulfilmentStage
27
- estimatedDelivery: string
26
+ currentStage?: FulfilmentStage
27
+ estimatedDelivery?: string | null
28
28
  trackingHref?: string
29
29
  }
30
30
  /** Ordered by `Object.keys(fulfilmentDetails)` from the orchestrator. */
@@ -72,17 +72,21 @@ export function CheckoutConfirmationFulfilmentProgress({
72
72
  locationLabel = 'Ubicación',
73
73
  trackOrderLabel = 'Seguir este pedido',
74
74
  }: CheckoutConfirmationFulfilmentProgressProps) {
75
- const currentIndex = stages.findIndex(stage => stage.stage === order.currentStage)
76
- const [selected, setSelected] = useState<string>(order.currentStage)
75
+ const currentIndex = order.currentStage
76
+ ? stages.findIndex(stage => stage.stage === order.currentStage)
77
+ : -1
78
+ const [selected, setSelected] = useState<string>(order.currentStage ?? stages[0]?.stage ?? '')
77
79
 
78
80
  return (
79
81
  <Card className='mt-10'>
80
82
  <CardHeader>
81
83
  <CardTitle className='flex flex-wrap items-center justify-between gap-2 text-base'>
82
84
  {progressLabel}
83
- <span className='text-muted-foreground text-sm font-normal'>
84
- {`${estimatedDeliveryLabel} ${longDate.format(new Date(order.estimatedDelivery))}`}
85
- </span>
85
+ {order.estimatedDelivery ? (
86
+ <span className='text-muted-foreground text-sm font-normal'>
87
+ {`${estimatedDeliveryLabel} ${longDate.format(new Date(order.estimatedDelivery))}`}
88
+ </span>
89
+ ) : null}
86
90
  </CardTitle>
87
91
  </CardHeader>
88
92
 
@@ -156,7 +160,9 @@ export function CheckoutConfirmationFulfilmentProgress({
156
160
  ? stampDate.format(new Date(detail.at))
157
161
  : done
158
162
  ? inProgressLabel
159
- : `${expectedByLabel} ${longDate.format(new Date(order.estimatedDelivery))}`}
163
+ : order.estimatedDelivery
164
+ ? `${expectedByLabel} ${longDate.format(new Date(order.estimatedDelivery))}`
165
+ : null}
160
166
  </dd>
161
167
  </div>
162
168
 
@@ -0,0 +1,136 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { CheckoutConfirmation, type Order, type OrderLine } from './checkout-confirmation'
4
+
5
+ const order: Order = {
6
+ number: '1001',
7
+ placedAt: '2026-10-06T00:00:00.000Z',
8
+ totals: {},
9
+ }
10
+
11
+ const orderLines: OrderLine[] = [
12
+ { id: 'l1', name: 'Taladro', quantity: 1, lineTotal: { amount: '45.00', currencyCode: 'USD' } },
13
+ {
14
+ id: 'l2',
15
+ name: 'Batería',
16
+ quantity: 2,
17
+ lineTotal: { amount: '40.00', currencyCode: 'USD' },
18
+ image: '',
19
+ },
20
+ ]
21
+
22
+ describe('CheckoutConfirmation — truthful optional data', () => {
23
+ it('omits email, ETA, progress, addresses, and totals when the backend supplies none', () => {
24
+ const html = renderToStaticMarkup(<CheckoutConfirmation order={order} orderLines={orderLines} />)
25
+ expect(html).toContain('Pedido 1001 realizado')
26
+ expect(html).not.toContain('Enviamos la confirmación')
27
+ expect(html).not.toContain('Entrega estimada')
28
+ expect(html).not.toContain('Progreso')
29
+ expect(html).not.toContain('Subtotal')
30
+ expect(html).not.toContain('Total')
31
+ expect(html).not.toContain('Gratis')
32
+ expect(html).not.toContain('Facturación')
33
+ })
34
+
35
+ it('renders supplied email, totals, and a zero-shipping free label', () => {
36
+ const html = renderToStaticMarkup(
37
+ <CheckoutConfirmation
38
+ order={{
39
+ ...order,
40
+ email: 'ana@example.com',
41
+ totals: {
42
+ subtotal: { amount: '85.00', currencyCode: 'USD' },
43
+ shipping: { amount: '0', currencyCode: 'USD' },
44
+ tax: { amount: '6.80', currencyCode: 'USD' },
45
+ total: { amount: '91.80', currencyCode: 'USD' },
46
+ },
47
+ }}
48
+ orderLines={orderLines}
49
+ formatPrice={money => `${money.currencyCode} ${money.amount}`}
50
+ />,
51
+ )
52
+ expect(html).toContain('ana@example.com')
53
+ expect(html).toContain('USD 85.00')
54
+ expect(html).toContain('Gratis')
55
+ expect(html).toContain('USD 91.80')
56
+ })
57
+
58
+ it('renders the fulfilment progress only when stages and a current stage are supplied', () => {
59
+ const withProgress = renderToStaticMarkup(
60
+ <CheckoutConfirmation
61
+ order={{ ...order, currentStage: 'Confirmed', estimatedDelivery: '2026-10-10T00:00:00.000Z' }}
62
+ orderLines={orderLines}
63
+ fulfilmentDetails={{
64
+ Confirmed: { stage: 'Confirmed', summary: 'Confirmado', location: 'Bodega', at: '2026-10-06T00:00:00.000Z' },
65
+ Packed: { stage: 'Packed', summary: 'Empaquetado', location: 'Bodega', at: null },
66
+ Shipped: { stage: 'Shipped', summary: 'Enviado', location: 'Bodega', at: null },
67
+ Delivered: { stage: 'Delivered', summary: 'Entregado', location: 'Bodega', at: null },
68
+ }}
69
+ />,
70
+ )
71
+ expect(withProgress).toContain('Progreso')
72
+ expect(withProgress).toContain('Entrega estimada')
73
+
74
+ const withoutProgress = renderToStaticMarkup(
75
+ <CheckoutConfirmation order={order} orderLines={orderLines} />,
76
+ )
77
+ expect(withoutProgress).not.toContain('Progreso')
78
+ })
79
+
80
+ it('omits an absent line image without emitting an empty src', () => {
81
+ const html = renderToStaticMarkup(<CheckoutConfirmation order={order} orderLines={orderLines} />)
82
+ expect(html).not.toMatch(/src=""/)
83
+ })
84
+
85
+ it('renders duplicate address lines without colliding keys', () => {
86
+ const html = renderToStaticMarkup(
87
+ <CheckoutConfirmation
88
+ order={{
89
+ ...order,
90
+ shipping: { name: 'Ana', lines: ['Calle 1', 'Calle 1'] },
91
+ }}
92
+ orderLines={orderLines}
93
+ />,
94
+ )
95
+ expect((html.match(/Calle 1/g) ?? []).length).toBe(2)
96
+ })
97
+
98
+ it('never renders a fake zero for absent or malformed line/total amounts', () => {
99
+ const html = renderToStaticMarkup(
100
+ <CheckoutConfirmation
101
+ order={{
102
+ ...order,
103
+ totals: {
104
+ subtotal: { amount: '', currencyCode: 'USD' },
105
+ total: { amount: 'abc', currencyCode: 'USD' },
106
+ },
107
+ }}
108
+ orderLines={[
109
+ { id: 'l1', name: 'Pieza', quantity: 1, lineTotal: { amount: '', currencyCode: 'USD' } },
110
+ ]}
111
+ formatPrice={money => `${money.currencyCode} ${money.amount}`}
112
+ />,
113
+ )
114
+ expect(html).not.toContain('USD 0')
115
+ expect(html).toContain('—')
116
+ })
117
+
118
+ it('preserves a legitimate zero shipping and total', () => {
119
+ const html = renderToStaticMarkup(
120
+ <CheckoutConfirmation
121
+ order={{
122
+ ...order,
123
+ totals: {
124
+ subtotal: { amount: '0', currencyCode: 'USD' },
125
+ shipping: { amount: '0', currencyCode: 'USD' },
126
+ total: { amount: '0', currencyCode: 'USD' },
127
+ },
128
+ }}
129
+ orderLines={orderLines}
130
+ formatPrice={money => `${money.currencyCode} ${money.amount}`}
131
+ />,
132
+ )
133
+ expect(html).toContain('USD 0')
134
+ expect(html).toContain('Gratis')
135
+ })
136
+ })
@@ -2,6 +2,8 @@ import { CircleCheck } from 'lucide-react'
2
2
  import { Button } from '../../../ui/button'
3
3
  import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
4
4
  import { Separator } from '../../../ui/separator'
5
+ import { formatMoney, isAvailableMoney } from '../../../lib/money'
6
+ import type { Money } from '@thorprovider/types'
5
7
  import { CheckoutConfirmationFulfilmentProgress } from './checkout-confirmation-fulfilment-progress'
6
8
 
7
9
  export type FulfilmentStage = 'Confirmed' | 'Packed' | 'Shipped' | 'Delivered'
@@ -9,10 +11,13 @@ export type FulfilmentStage = 'Confirmed' | 'Packed' | 'Shipped' | 'Delivered'
9
11
  export interface OrderLine {
10
12
  id: string
11
13
  name: string
12
- variant: string
14
+ variant?: string
13
15
  quantity: number
14
- price: number
15
- image: string
16
+ /** Canonical backend line total. */
17
+ lineTotal: Money
18
+ unitPrice?: Money
19
+ /** Optional; an absent image is omitted rather than serialized as `""`. */
20
+ image?: string
16
21
  }
17
22
 
18
23
  export interface FulfilmentStageDetail {
@@ -32,26 +37,29 @@ export interface FulfilmentStageDetail {
32
37
  export interface OrderAddress {
33
38
  name: string
34
39
  lines: string[]
35
- method: string
40
+ method?: string
36
41
  }
37
42
 
38
43
  export interface OrderTotals {
39
- subtotal: number
40
- shipping: number
41
- tax: number
44
+ subtotal?: Money
45
+ discount?: Money
46
+ shipping?: Money
47
+ tax?: Money
48
+ total?: Money
42
49
  }
43
50
 
44
51
  export interface Order {
45
52
  number: string
46
53
  /** ISO date */
47
54
  placedAt: string
48
- email: string
49
- currentStage: FulfilmentStage
50
- /** ISO date */
51
- estimatedDelivery: string
55
+ email?: string
56
+ /** Present only when the backend reports a fulfilment stage. */
57
+ currentStage?: FulfilmentStage
58
+ /** ISO date. Rendered only when supplied; no delivery estimate is fabricated. */
59
+ estimatedDelivery?: string | null
52
60
  trackingHref?: string
53
- shipping: OrderAddress
54
- billing: OrderAddress
61
+ shipping?: OrderAddress
62
+ billing?: OrderAddress
55
63
  totals: OrderTotals
56
64
  }
57
65
 
@@ -59,22 +67,28 @@ export interface CheckoutConfirmationProps {
59
67
  order: Order
60
68
  orderLines: OrderLine[]
61
69
  /**
62
- * Keyed by `FulfilmentStage`. The stage strip is ordered by `Object.keys`,
63
- * so define the record in the canonical `Confirmed → Packed → Shipped → Delivered` order.
70
+ * Keyed by `FulfilmentStage`. Rendered only when supplied; the stage strip is
71
+ * ordered by `Object.keys`, so define the record in the canonical
72
+ * `Confirmed → Packed → Shipped → Delivered` order.
64
73
  */
65
- fulfilmentDetails: Record<FulfilmentStage, FulfilmentStageDetail>
66
- /** Override price formatting (locale, currency). Defaults to USD. */
67
- formatPrice?: (value: number) => string
74
+ fulfilmentDetails?: Record<FulfilmentStage, FulfilmentStageDetail>
75
+ /** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
76
+ formatPrice?: (money: Money) => string
68
77
  titleLabel?: string
69
78
  /** Template with `{number}`/`{date}`/`{email}` placeholders, resolved internally. */
70
79
  orderPlacedLabel?: string
80
+ /** Used when the order carries no email. */
81
+ orderPlacedNoEmailLabel?: string
71
82
  itemsLabel?: string
72
83
  quantityLabel?: string
73
84
  subtotalLabel?: string
85
+ discountLabel?: string
74
86
  shippingLabel?: string
75
87
  taxLabel?: string
76
88
  totalLabel?: string
77
89
  freeLabel?: string
90
+ /** Shown for a line whose supplied line total is absent or malformed. */
91
+ amountUnavailableLabel?: string
78
92
  shippingAddressLabel?: string
79
93
  billingAddressLabel?: string
80
94
  continueShoppingLabel?: string
@@ -95,7 +109,6 @@ export interface CheckoutConfirmationProps {
95
109
  onContinueShopping?: () => void
96
110
  }
97
111
 
98
- const usdFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
99
112
  const longDate = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' })
100
113
 
101
114
  /**
@@ -110,15 +123,18 @@ export function CheckoutConfirmation({
110
123
  orderLines,
111
124
  fulfilmentDetails,
112
125
  formatPrice,
113
- titleLabel = '¡Gracias! Tu pedido está en camino',
114
- orderPlacedLabel = 'Pedido {number} realizado el {date}. Enviamos la confirmación a {email}.',
115
- itemsLabel = 'Artículos',
126
+ titleLabel = 'Pedido registrado',
127
+ orderPlacedLabel = 'Pedido {number} · {date} · Correo de contacto: {email}',
128
+ orderPlacedNoEmailLabel = 'Pedido {number} realizado el {date}.',
129
+ itemsLabel = 'Comprobante del pedido',
116
130
  quantityLabel = 'Cantidad',
117
131
  subtotalLabel = 'Subtotal',
132
+ discountLabel = 'Descuento',
118
133
  shippingLabel = 'Envío',
119
134
  taxLabel = 'Impuestos',
120
135
  totalLabel = 'Total',
121
136
  freeLabel = 'Gratis',
137
+ amountUnavailableLabel = '—',
122
138
  shippingAddressLabel = 'Envío',
123
139
  billingAddressLabel = 'Facturación',
124
140
  continueShoppingLabel = 'Seguir comprando',
@@ -137,9 +153,21 @@ export function CheckoutConfirmation({
137
153
  supportHref,
138
154
  onContinueShopping,
139
155
  }: CheckoutConfirmationProps) {
140
- const total = order.totals.subtotal + order.totals.shipping + order.totals.tax
141
- const format = formatPrice ?? (value => usdFormatter.format(value))
142
- const fulfilmentStages = Object.keys(fulfilmentDetails) as FulfilmentStage[]
156
+ const format = (value: Money | null | undefined) => formatMoney(value, formatPrice)
157
+ const placedDate = longDate.format(new Date(order.placedAt))
158
+ const placedText = order.email
159
+ ? orderPlacedLabel
160
+ .replace('{number}', order.number)
161
+ .replace('{date}', placedDate)
162
+ .replace('{email}', order.email)
163
+ : orderPlacedNoEmailLabel.replace('{number}', order.number).replace('{date}', placedDate)
164
+ const fulfilmentStages = fulfilmentDetails
165
+ ? (Object.keys(fulfilmentDetails) as FulfilmentStage[])
166
+ : []
167
+ const addresses = [
168
+ { title: shippingAddressLabel, address: order.shipping },
169
+ { title: billingAddressLabel, address: order.billing },
170
+ ].filter((section): section is { title: string; address: OrderAddress } => Boolean(section.address))
143
171
 
144
172
  return (
145
173
  <div className='w-full py-16 sm:py-24'>
@@ -152,31 +180,28 @@ export function CheckoutConfirmation({
152
180
  <CircleCheck className='size-6' />
153
181
  </span>
154
182
  <h1 className='mt-4 text-3xl font-semibold tracking-tight'>{titleLabel}</h1>
155
- <p className='text-muted-foreground mt-3'>
156
- {orderPlacedLabel
157
- .replace('{number}', order.number)
158
- .replace('{date}', longDate.format(new Date(order.placedAt)))
159
- .replace('{email}', order.email)}
160
- </p>
183
+ <p className='text-muted-foreground mt-3'>{placedText}</p>
161
184
  </div>
162
185
 
163
- <CheckoutConfirmationFulfilmentProgress
164
- order={{
165
- currentStage: order.currentStage,
166
- estimatedDelivery: order.estimatedDelivery,
167
- trackingHref: order.trackingHref,
168
- }}
169
- stages={fulfilmentStages.map(stage => fulfilmentDetails[stage])}
170
- progressLabel={progressLabel}
171
- estimatedDeliveryLabel={estimatedDeliveryLabel}
172
- currentStageLabel={currentStageLabel}
173
- inProgressLabel={inProgressLabel}
174
- expectedByLabel={expectedByLabel}
175
- completedLabel={completedLabel}
176
- statusLabel={statusLabel}
177
- locationLabel={locationLabel}
178
- trackOrderLabel={trackOrderLabel}
179
- />
186
+ {fulfilmentDetails && order.currentStage ? (
187
+ <CheckoutConfirmationFulfilmentProgress
188
+ order={{
189
+ currentStage: order.currentStage,
190
+ estimatedDelivery: order.estimatedDelivery,
191
+ trackingHref: order.trackingHref,
192
+ }}
193
+ stages={fulfilmentStages.map(stage => fulfilmentDetails[stage])}
194
+ progressLabel={progressLabel}
195
+ estimatedDeliveryLabel={estimatedDeliveryLabel}
196
+ currentStageLabel={currentStageLabel}
197
+ inProgressLabel={inProgressLabel}
198
+ expectedByLabel={expectedByLabel}
199
+ completedLabel={completedLabel}
200
+ statusLabel={statusLabel}
201
+ locationLabel={locationLabel}
202
+ trackOrderLabel={trackOrderLabel}
203
+ />
204
+ ) : null}
180
205
 
181
206
  <Card className='mt-6'>
182
207
  <CardHeader>
@@ -186,22 +211,26 @@ export function CheckoutConfirmation({
186
211
  <ul className='divide-border divide-y'>
187
212
  {orderLines.map(line => (
188
213
  <li key={line.id} className='flex gap-4 py-4 first:pt-0 last:pb-0'>
189
- <img
190
- src={line.image || undefined}
191
- alt={line.name}
192
- width={72}
193
- height={88}
194
- className='bg-muted h-22 w-18 shrink-0 rounded-md object-cover'
195
- />
214
+ {line.image ? (
215
+ <img
216
+ src={line.image || undefined}
217
+ alt={line.name}
218
+ width={72}
219
+ height={88}
220
+ className='bg-muted h-22 w-18 shrink-0 rounded-md object-cover'
221
+ />
222
+ ) : (
223
+ <div aria-hidden='true' className='bg-muted h-22 w-18 shrink-0 rounded-md' />
224
+ )}
196
225
  <div className='min-w-0 flex-1'>
197
226
  <p className='text-sm font-medium'>{line.name}</p>
198
- <p className='text-muted-foreground text-sm'>{line.variant}</p>
227
+ {line.variant ? <p className='text-muted-foreground text-sm'>{line.variant}</p> : null}
199
228
  <p className='text-muted-foreground text-sm tabular-nums'>
200
229
  {`${quantityLabel} ${line.quantity}`}
201
230
  </p>
202
231
  </div>
203
232
  <p className='text-sm font-medium tabular-nums'>
204
- {format(line.price * line.quantity)}
233
+ {format(line.lineTotal) ?? amountUnavailableLabel}
205
234
  </p>
206
235
  </li>
207
236
  ))}
@@ -210,51 +239,68 @@ export function CheckoutConfirmation({
210
239
  <Separator className='my-4' />
211
240
 
212
241
  <dl className='flex flex-col gap-2 text-sm'>
213
- <div className='flex justify-between'>
214
- <dt className='text-muted-foreground'>{subtotalLabel}</dt>
215
- <dd className='tabular-nums'>{format(order.totals.subtotal)}</dd>
216
- </div>
217
- <div className='flex justify-between'>
218
- <dt className='text-muted-foreground'>{shippingLabel}</dt>
219
- <dd className='tabular-nums'>
220
- {order.totals.shipping === 0 ? freeLabel : format(order.totals.shipping)}
221
- </dd>
222
- </div>
223
- <div className='flex justify-between'>
224
- <dt className='text-muted-foreground'>{taxLabel}</dt>
225
- <dd className='tabular-nums'>{format(order.totals.tax)}</dd>
226
- </div>
227
- <div className='flex justify-between pt-2 text-base font-semibold'>
228
- <dt>{totalLabel}</dt>
229
- <dd className='tabular-nums'>{format(total)}</dd>
230
- </div>
242
+ {isAvailableMoney(order.totals.subtotal) ? (
243
+ <div className='flex justify-between'>
244
+ <dt className='text-muted-foreground'>{subtotalLabel}</dt>
245
+ <dd className='tabular-nums'>{format(order.totals.subtotal)}</dd>
246
+ </div>
247
+ ) : null}
248
+ {isAvailableMoney(order.totals.discount) ? (
249
+ <div className='flex justify-between'>
250
+ <dt className='text-muted-foreground'>{discountLabel}</dt>
251
+ <dd className='tabular-nums'>{format(order.totals.discount)}</dd>
252
+ </div>
253
+ ) : null}
254
+ {isAvailableMoney(order.totals.shipping) ? (
255
+ <div className='flex justify-between'>
256
+ <dt className='text-muted-foreground'>{shippingLabel}</dt>
257
+ <dd className='tabular-nums'>
258
+ {Number(order.totals.shipping.amount) === 0
259
+ ? freeLabel
260
+ : format(order.totals.shipping)}
261
+ </dd>
262
+ </div>
263
+ ) : null}
264
+ {isAvailableMoney(order.totals.tax) ? (
265
+ <div className='flex justify-between'>
266
+ <dt className='text-muted-foreground'>{taxLabel}</dt>
267
+ <dd className='tabular-nums'>{format(order.totals.tax)}</dd>
268
+ </div>
269
+ ) : null}
270
+ {isAvailableMoney(order.totals.total) ? (
271
+ <div className='flex justify-between pt-2 text-base font-semibold'>
272
+ <dt>{totalLabel}</dt>
273
+ <dd className='tabular-nums'>{format(order.totals.total)}</dd>
274
+ </div>
275
+ ) : null}
231
276
  </dl>
232
277
  </CardContent>
233
278
  </Card>
234
279
 
235
- <div className='mt-6 grid gap-6 sm:grid-cols-2'>
236
- {[
237
- { title: shippingAddressLabel, address: order.shipping },
238
- { title: billingAddressLabel, address: order.billing },
239
- ].map(section => (
240
- <Card key={section.title}>
241
- <CardHeader>
242
- <CardTitle className='text-base'>{section.title}</CardTitle>
243
- </CardHeader>
244
- <CardContent className='text-sm'>
245
- <address className='text-muted-foreground not-italic'>
246
- <span className='text-foreground block font-medium'>{section.address.name}</span>
247
- {section.address.lines.map(line => (
248
- <span key={line} className='block'>
249
- {line}
250
- </span>
251
- ))}
252
- </address>
253
- <p className='text-muted-foreground mt-3'>{section.address.method}</p>
254
- </CardContent>
255
- </Card>
256
- ))}
257
- </div>
280
+ {addresses.length > 0 ? (
281
+ <div className='mt-6 grid gap-6 sm:grid-cols-2'>
282
+ {addresses.map(section => (
283
+ <Card key={section.title}>
284
+ <CardHeader>
285
+ <CardTitle className='text-base'>{section.title}</CardTitle>
286
+ </CardHeader>
287
+ <CardContent className='text-sm'>
288
+ <address className='text-muted-foreground not-italic'>
289
+ <span className='text-foreground block font-medium'>{section.address.name}</span>
290
+ {section.address.lines.map((line, index) => (
291
+ <span key={`${section.title}:${index}:${line}`} className='block'>
292
+ {line}
293
+ </span>
294
+ ))}
295
+ </address>
296
+ {section.address.method ? (
297
+ <p className='text-muted-foreground mt-3'>{section.address.method}</p>
298
+ ) : null}
299
+ </CardContent>
300
+ </Card>
301
+ ))}
302
+ </div>
303
+ ) : null}
258
304
 
259
305
  <div className='mt-8 flex flex-col items-center gap-2'>
260
306
  {continueShoppingHref ? (
@@ -280,4 +326,4 @@ export function CheckoutConfirmation({
280
326
  )
281
327
  }
282
328
 
283
- export default CheckoutConfirmation
329
+ export default CheckoutConfirmation