@cartbase/storefront 0.16.0 → 0.18.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 (110) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -255
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -130
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/products.ts +43 -1
  18. package/src/api/redirects.ts +37 -37
  19. package/src/api/regions.ts +200 -200
  20. package/src/api/reviews.ts +259 -259
  21. package/src/api/search.ts +163 -163
  22. package/src/api/store.ts +35 -35
  23. package/src/api/types.ts +91 -91
  24. package/src/cart-drawer/context.tsx +277 -164
  25. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  26. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  27. package/src/cart-drawer/free-gift.tsx +1 -1
  28. package/src/cart-drawer/gift-wrap.tsx +82 -82
  29. package/src/cart-drawer/index.ts +71 -68
  30. package/src/cart-drawer/item/index.tsx +162 -162
  31. package/src/cart-drawer/item/quantity.tsx +8 -14
  32. package/src/cart-drawer/item/upsell.tsx +110 -110
  33. package/src/cart-drawer/labels.ts +123 -123
  34. package/src/cart-drawer/mutation-queue.ts +78 -0
  35. package/src/cart-drawer/notes.tsx +131 -131
  36. package/src/cart-drawer/sticky-footer.tsx +73 -73
  37. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  38. package/src/checkout/address-error-copy.ts +117 -117
  39. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  40. package/src/checkout/compare-addresses.ts +40 -40
  41. package/src/checkout/discount-section.tsx +218 -218
  42. package/src/checkout/error-copy-codes.ts +63 -63
  43. package/src/checkout/geocode.ts +154 -154
  44. package/src/checkout/gift-card-section.tsx +224 -224
  45. package/src/checkout/index.ts +72 -72
  46. package/src/checkout/labels.ts +495 -495
  47. package/src/checkout/payment-button.tsx +372 -372
  48. package/src/checkout/payment-error-copy.ts +154 -154
  49. package/src/checkout/promotion-error-copy.ts +91 -91
  50. package/src/checkout/use-checkout-orchestration.ts +1592 -1592
  51. package/src/common/country-flag.tsx +52 -52
  52. package/src/common/country-select.tsx +11 -11
  53. package/src/common/index.ts +20 -20
  54. package/src/common/market-select.tsx +57 -57
  55. package/src/index.ts +12 -12
  56. package/src/lib/country-name.ts +59 -59
  57. package/src/lib/get-product-price.ts +133 -133
  58. package/src/lib/media-image.tsx +39 -0
  59. package/src/lib/payment-constants.ts +53 -53
  60. package/src/lib/platform.ts +13 -13
  61. package/src/lib/price.tsx +39 -39
  62. package/src/lib/store-api-error.ts +36 -36
  63. package/src/lib/variant-caption.ts +32 -32
  64. package/src/locales/bg.ts +467 -467
  65. package/src/locales/context.ts +37 -37
  66. package/src/locales/en.ts +26 -26
  67. package/src/locales/es.ts +466 -466
  68. package/src/locales/index.ts +19 -19
  69. package/src/locales/provider.tsx +59 -59
  70. package/src/locales/types.ts +77 -77
  71. package/src/order/index.ts +62 -62
  72. package/src/order/labels.ts +79 -79
  73. package/src/order/order-item.tsx +1 -1
  74. package/src/order/order-totals.tsx +250 -250
  75. package/src/primitives/select-field.tsx +93 -93
  76. package/src/products/image-gallery.tsx +43 -43
  77. package/src/products/index.ts +68 -59
  78. package/src/products/option-select.tsx +67 -67
  79. package/src/products/product-actions-wrapper.tsx +63 -62
  80. package/src/products/product-actions.tsx +151 -150
  81. package/src/products/product-price.tsx +69 -69
  82. package/src/products/product-promises.tsx +61 -61
  83. package/src/products/product-specs.tsx +65 -65
  84. package/src/products/product-tabs.tsx +123 -123
  85. package/src/products/product-template.tsx +163 -162
  86. package/src/products/purchase-options.tsx +130 -130
  87. package/src/products/sets.ts +110 -0
  88. package/src/products/thumbnail.tsx +1 -1
  89. package/src/products/use-product-actions.ts +199 -164
  90. package/src/products/variant-matching.ts +71 -71
  91. package/src/products/variant-url.ts +74 -74
  92. package/src/reviews-ui/index.ts +73 -73
  93. package/src/store/category-template.tsx +136 -136
  94. package/src/store/index.ts +40 -40
  95. package/src/tracking/chatgpt-pixel.tsx +99 -99
  96. package/src/tracking/consent-init.tsx +62 -62
  97. package/src/tracking/events.ts +348 -348
  98. package/src/tracking/ga4.tsx +93 -93
  99. package/src/tracking/google-ads.ts +84 -84
  100. package/src/tracking/gtm.tsx +60 -60
  101. package/src/tracking/inline-script.ts +49 -49
  102. package/src/tracking/oaiq.ts +206 -206
  103. package/src/tracking/tiktok-pixel.tsx +91 -91
  104. package/src/tracking/track-init.tsx +56 -56
  105. package/src/tracking/track-order-purchase.tsx +122 -122
  106. package/src/tracking/ttq.ts +180 -180
  107. package/src/tracking/use-tracking-config.ts +54 -54
  108. package/theme/index.css +25 -25
  109. package/theme/theme.css +150 -150
  110. package/theme/tokens.css +106 -106
@@ -1,73 +1,73 @@
1
- "use client"
2
-
3
- import Link from "next/link"
4
-
5
- import { Price } from "../lib/price"
6
- import { productTotal } from "../lib/cart-helpers"
7
- import { useCartDrawer } from "./context"
8
-
9
- /**
10
- * CartStickyFooter — bottom-of-drawer subtotal + checkout CTA with
11
- * iPhone home-indicator safe-area padding.
12
- *
13
- * Ported from `@1click/ui/src/cart-drawer/sticky-footer.tsx` (v2.3.1);
14
- * originally extracted from mindpages-storefront
15
- * src/modules/cart-drawer/cart-sticky-footer.tsx. Amounts EUR major units.
16
- */
17
-
18
- export function CartStickyFooter() {
19
- const { cart, close, labels, hrefs } = useCartDrawer()
20
- if (!cart || !cart.items?.length) return null
21
-
22
- // Drawer is pre-checkout context — never read `cart.total` here.
23
- // `cart.total` includes shipping/tax/COD fee, all of which are
24
- // checkout-context state that must not leak into the shopping
25
- // drawer. Sum of product line totals only.
26
- const total = productTotal(cart.items)
27
- const currencyCode = cart.currency_code
28
-
29
- return (
30
- <div
31
- className="flex-shrink-0 bg-card px-5 sm:px-6 pt-4 sm:pt-5 relative z-10"
32
- style={{
33
- boxShadow: "0 -4px 12px rgba(0,0,0,0.04)",
34
- paddingBottom: "max(1.25rem, env(safe-area-inset-bottom, 1.25rem))",
35
- }}
36
- >
37
- <div className="flex items-baseline justify-between mb-1">
38
- <span className="text-[15px] font-bold text-foreground">
39
- {labels.subtotal}
40
- </span>
41
- <Price
42
- amount={total}
43
- currencyCode={currencyCode}
44
- className="text-xl font-bold text-foreground tracking-tight"
45
- />
46
- </div>
47
- <p className="text-[11px] text-muted-foreground mb-3.5 sm:mb-4">
48
- {labels.taxAndShipping}
49
- </p>
50
- <Link href={hrefs.checkout} onClick={close}>
51
- <button
52
- type="button"
53
- className="w-full h-[52px] bg-foreground hover:bg-foreground/90 active:bg-foreground/95 text-card text-[15px] sm:text-sm font-semibold rounded-[2px] flex items-center justify-center gap-2.5 transition-colors active:scale-[0.98]"
54
- >
55
- <svg
56
- width="16"
57
- height="16"
58
- viewBox="0 0 16 16"
59
- fill="none"
60
- stroke="currentColor"
61
- strokeWidth="1.5"
62
- strokeLinecap="round"
63
- strokeLinejoin="round"
64
- >
65
- <rect x="3" y="6" width="10" height="8" rx="1.5" />
66
- <path d="M5.5 6V4.5a2.5 2.5 0 015 0V6" />
67
- </svg>
68
- {labels.secureCheckout}
69
- </button>
70
- </Link>
71
- </div>
72
- )
73
- }
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+
5
+ import { Price } from "../lib/price"
6
+ import { productTotal } from "../lib/cart-helpers"
7
+ import { useCartDrawer } from "./context"
8
+
9
+ /**
10
+ * CartStickyFooter — bottom-of-drawer subtotal + checkout CTA with
11
+ * iPhone home-indicator safe-area padding.
12
+ *
13
+ * Ported from `@1click/ui/src/cart-drawer/sticky-footer.tsx` (v2.3.1);
14
+ * originally extracted from mindpages-storefront
15
+ * src/modules/cart-drawer/cart-sticky-footer.tsx. Amounts EUR major units.
16
+ */
17
+
18
+ export function CartStickyFooter() {
19
+ const { cart, close, labels, hrefs } = useCartDrawer()
20
+ if (!cart || !cart.items?.length) return null
21
+
22
+ // Drawer is pre-checkout context — never read `cart.total` here.
23
+ // `cart.total` includes shipping/tax/COD fee, all of which are
24
+ // checkout-context state that must not leak into the shopping
25
+ // drawer. Sum of product line totals only.
26
+ const total = productTotal(cart.items)
27
+ const currencyCode = cart.currency_code
28
+
29
+ return (
30
+ <div
31
+ className="flex-shrink-0 bg-card px-5 sm:px-6 pt-4 sm:pt-5 relative z-10"
32
+ style={{
33
+ boxShadow: "0 -4px 12px rgba(0,0,0,0.04)",
34
+ paddingBottom: "max(1.25rem, env(safe-area-inset-bottom, 1.25rem))",
35
+ }}
36
+ >
37
+ <div className="flex items-baseline justify-between mb-1">
38
+ <span className="text-[15px] font-bold text-foreground">
39
+ {labels.subtotal}
40
+ </span>
41
+ <Price
42
+ amount={total}
43
+ currencyCode={currencyCode}
44
+ className="text-xl font-bold text-foreground tracking-tight"
45
+ />
46
+ </div>
47
+ <p className="text-[11px] text-muted-foreground mb-3.5 sm:mb-4">
48
+ {labels.taxAndShipping}
49
+ </p>
50
+ <Link href={hrefs.checkout} onClick={close}>
51
+ <button
52
+ type="button"
53
+ className="w-full h-[52px] bg-foreground hover:bg-foreground/90 active:bg-foreground/95 text-card text-[15px] sm:text-sm font-semibold rounded-[2px] flex items-center justify-center gap-2.5 transition-colors active:scale-[0.98]"
54
+ >
55
+ <svg
56
+ width="16"
57
+ height="16"
58
+ viewBox="0 0 16 16"
59
+ fill="none"
60
+ stroke="currentColor"
61
+ strokeWidth="1.5"
62
+ strokeLinecap="round"
63
+ strokeLinejoin="round"
64
+ >
65
+ <rect x="3" y="6" width="10" height="8" rx="1.5" />
66
+ <path d="M5.5 6V4.5a2.5 2.5 0 015 0V6" />
67
+ </svg>
68
+ {labels.secureCheckout}
69
+ </button>
70
+ </Link>
71
+ </div>
72
+ )
73
+ }
@@ -1,197 +1,197 @@
1
- "use client"
2
-
3
- import type { AppliedGiftCard } from "../api/carts"
4
- import { convertToLocale } from "../lib/money"
5
- import { useCartDrawer } from "./context"
6
-
7
- /**
8
- * CartSummaryBreakdown — full totals breakdown for the drawer (or a cart
9
- * page): subtotal, discount, shipping, tax, COD fee, total, applied
10
- * gift-card tender and the remainder. Alternative to the compact
11
- * `CartStickyFooter` (which deliberately shows a product-only subtotal in
12
- * pre-checkout context).
13
- *
14
- * Ported from `@1click/ui/src/cart-drawer/summary-breakdown.tsx` (v2.3.1)
15
- * and EXPANDED for Cartbase: the source rendered a client-summed product
16
- * subtotal only; Cartbase's decorated cart carries server-computed totals on
17
- * every read (`subtotal`, `discount_total`, `shipping_total`, `tax_total`,
18
- * `payment_method_fee_total`/`payment_method_fee_label`, `gift_cards[]`, `gift_card_total`,
19
- * `gift_card_remainder`, `total` — all EUR major units), so this component
20
- * renders EXACTLY what the server returns and computes no money. The only
21
- * client-side transformation is the display sign on deductions.
22
- *
23
- * Row rules (see `selectSummaryRows`):
24
- * - discount / tax / method-fee rows appear only when their total is > 0.
25
- * - the method fee renders the server's `payment_method_fee_label`
26
- * (labels.paymentMethodFee fallback).
27
- * - shipping renders the amount once a shipping method is set (0 → FREE);
28
- * before that it shows "calculated at checkout".
29
- * - gift-card tender renders one row per applied card (masked `last4`,
30
- * negative display) plus the `gift_card_remainder` row — the amount the
31
- * remainder provider actually charges. A depleted card stays listed at 0
32
- * (server keeps it in `gift_cards` at amount 0).
33
- */
34
-
35
- /** The decorated-cart fields the breakdown consumes (structural subset). */
36
- export type SummaryCartTotals = {
37
- subtotal: number
38
- discount_total: number
39
- shipping_total: number
40
- tax_total: number
41
- total: number
42
- payment_method_fee_total?: number
43
- payment_method_fee_label?: string | null
44
- gift_cards?: AppliedGiftCard[]
45
- gift_card_total?: number
46
- gift_card_remainder?: number
47
- shipping_methods?: ReadonlyArray<unknown>
48
- }
49
-
50
- export type SummaryRow =
51
- | { kind: "subtotal"; amount: number }
52
- | { kind: "discount"; amount: number }
53
- | { kind: "shipping"; amount: number }
54
- | { kind: "shipping_pending" }
55
- | { kind: "tax"; amount: number }
56
- | { kind: "payment_method_fee"; amount: number; label: string | null }
57
- | { kind: "total"; amount: number }
58
- | { kind: "gift_card"; last4: string; amount: number }
59
- | { kind: "remainder"; amount: number }
60
-
61
- /**
62
- * Pure row selection over the decorated-cart totals — exported for unit
63
- * tests. Amounts pass through verbatim (server truth); deduction rows
64
- * (discount, gift cards) carry a negated amount for display only.
65
- */
66
- export function selectSummaryRows(cart: SummaryCartTotals): SummaryRow[] {
67
- const rows: SummaryRow[] = []
68
- rows.push({ kind: "subtotal", amount: cart.subtotal })
69
-
70
- if ((cart.discount_total ?? 0) > 0) {
71
- rows.push({ kind: "discount", amount: -cart.discount_total })
72
- }
73
-
74
- if (cart.shipping_methods && cart.shipping_methods.length > 0) {
75
- rows.push({ kind: "shipping", amount: cart.shipping_total })
76
- } else {
77
- rows.push({ kind: "shipping_pending" })
78
- }
79
-
80
- if ((cart.tax_total ?? 0) > 0) {
81
- rows.push({ kind: "tax", amount: cart.tax_total })
82
- }
83
-
84
- if ((cart.payment_method_fee_total ?? 0) > 0) {
85
- rows.push({
86
- kind: "payment_method_fee",
87
- amount: cart.payment_method_fee_total!,
88
- label: cart.payment_method_fee_label ?? null,
89
- })
90
- }
91
-
92
- rows.push({ kind: "total", amount: cart.total })
93
-
94
- if (cart.gift_cards && cart.gift_cards.length > 0) {
95
- for (const card of cart.gift_cards) {
96
- // `|| 0` normalizes -0 (a depleted card at amount 0) so Intl never
97
- // renders a stray "-€0.00".
98
- rows.push({ kind: "gift_card", last4: card.last4, amount: -card.amount || 0 })
99
- }
100
- rows.push({ kind: "remainder", amount: cart.gift_card_remainder ?? 0 })
101
- }
102
-
103
- return rows
104
- }
105
-
106
- export function CartSummaryBreakdown() {
107
- const { cart, labels } = useCartDrawer()
108
- if (!cart) return null
109
-
110
- const currencyCode = cart.currency_code
111
- const money = (amount: number) =>
112
- convertToLocale({ amount, currency_code: currencyCode })
113
- const rows = selectSummaryRows(cart)
114
-
115
- return (
116
- <div className="px-6 py-5 space-y-2">
117
- {rows.map((row, i) => {
118
- switch (row.kind) {
119
- case "subtotal":
120
- return (
121
- <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
122
- <span>{labels.subtotal}</span>
123
- <span className="tabular-nums">{money(row.amount)}</span>
124
- </div>
125
- )
126
- case "discount":
127
- return (
128
- <div key={i} className="flex justify-between items-center text-[13px] text-success">
129
- <span>{labels.discount}</span>
130
- <span className="tabular-nums">{money(row.amount)}</span>
131
- </div>
132
- )
133
- case "shipping":
134
- return (
135
- <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
136
- <span>{labels.shipping}</span>
137
- <span className="tabular-nums">
138
- {row.amount === 0 ? labels.free : money(row.amount)}
139
- </span>
140
- </div>
141
- )
142
- case "shipping_pending":
143
- return (
144
- <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
145
- <span>{labels.shipping}</span>
146
- <span>{labels.calculatedAtCheckout}</span>
147
- </div>
148
- )
149
- case "tax":
150
- return (
151
- <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
152
- <span>{labels.tax}</span>
153
- <span className="tabular-nums">{money(row.amount)}</span>
154
- </div>
155
- )
156
- case "payment_method_fee":
157
- return (
158
- <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
159
- <span>{row.label ?? labels.paymentMethodFee}</span>
160
- <span className="tabular-nums">{money(row.amount)}</span>
161
- </div>
162
- )
163
- case "total":
164
- return (
165
- <div
166
- key={i}
167
- className="flex justify-between items-center pt-2 mt-1 border-t border-border"
168
- >
169
- <span className="text-[15px] font-bold text-foreground">
170
- {labels.total}
171
- </span>
172
- <span className="text-[17px] font-bold text-foreground tracking-tight tabular-nums">
173
- {money(row.amount)}
174
- </span>
175
- </div>
176
- )
177
- case "gift_card":
178
- return (
179
- <div key={i} className="flex justify-between items-center text-[13px] text-success">
180
- <span>
181
- {labels.giftCard} ••{row.last4}
182
- </span>
183
- <span className="tabular-nums">{money(row.amount)}</span>
184
- </div>
185
- )
186
- case "remainder":
187
- return (
188
- <div key={i} className="flex justify-between items-center text-[13px] font-semibold text-foreground">
189
- <span>{labels.remainingToPay}</span>
190
- <span className="tabular-nums">{money(row.amount)}</span>
191
- </div>
192
- )
193
- }
194
- })}
195
- </div>
196
- )
197
- }
1
+ "use client"
2
+
3
+ import type { AppliedGiftCard } from "../api/carts"
4
+ import { convertToLocale } from "../lib/money"
5
+ import { useCartDrawer } from "./context"
6
+
7
+ /**
8
+ * CartSummaryBreakdown — full totals breakdown for the drawer (or a cart
9
+ * page): subtotal, discount, shipping, tax, COD fee, total, applied
10
+ * gift-card tender and the remainder. Alternative to the compact
11
+ * `CartStickyFooter` (which deliberately shows a product-only subtotal in
12
+ * pre-checkout context).
13
+ *
14
+ * Ported from `@1click/ui/src/cart-drawer/summary-breakdown.tsx` (v2.3.1)
15
+ * and EXPANDED for Cartbase: the source rendered a client-summed product
16
+ * subtotal only; Cartbase's decorated cart carries server-computed totals on
17
+ * every read (`subtotal`, `discount_total`, `shipping_total`, `tax_total`,
18
+ * `payment_method_fee_total`/`payment_method_fee_label`, `gift_cards[]`, `gift_card_total`,
19
+ * `gift_card_remainder`, `total` — all EUR major units), so this component
20
+ * renders EXACTLY what the server returns and computes no money. The only
21
+ * client-side transformation is the display sign on deductions.
22
+ *
23
+ * Row rules (see `selectSummaryRows`):
24
+ * - discount / tax / method-fee rows appear only when their total is > 0.
25
+ * - the method fee renders the server's `payment_method_fee_label`
26
+ * (labels.paymentMethodFee fallback).
27
+ * - shipping renders the amount once a shipping method is set (0 → FREE);
28
+ * before that it shows "calculated at checkout".
29
+ * - gift-card tender renders one row per applied card (masked `last4`,
30
+ * negative display) plus the `gift_card_remainder` row — the amount the
31
+ * remainder provider actually charges. A depleted card stays listed at 0
32
+ * (server keeps it in `gift_cards` at amount 0).
33
+ */
34
+
35
+ /** The decorated-cart fields the breakdown consumes (structural subset). */
36
+ export type SummaryCartTotals = {
37
+ subtotal: number
38
+ discount_total: number
39
+ shipping_total: number
40
+ tax_total: number
41
+ total: number
42
+ payment_method_fee_total?: number
43
+ payment_method_fee_label?: string | null
44
+ gift_cards?: AppliedGiftCard[]
45
+ gift_card_total?: number
46
+ gift_card_remainder?: number
47
+ shipping_methods?: ReadonlyArray<unknown>
48
+ }
49
+
50
+ export type SummaryRow =
51
+ | { kind: "subtotal"; amount: number }
52
+ | { kind: "discount"; amount: number }
53
+ | { kind: "shipping"; amount: number }
54
+ | { kind: "shipping_pending" }
55
+ | { kind: "tax"; amount: number }
56
+ | { kind: "payment_method_fee"; amount: number; label: string | null }
57
+ | { kind: "total"; amount: number }
58
+ | { kind: "gift_card"; last4: string; amount: number }
59
+ | { kind: "remainder"; amount: number }
60
+
61
+ /**
62
+ * Pure row selection over the decorated-cart totals — exported for unit
63
+ * tests. Amounts pass through verbatim (server truth); deduction rows
64
+ * (discount, gift cards) carry a negated amount for display only.
65
+ */
66
+ export function selectSummaryRows(cart: SummaryCartTotals): SummaryRow[] {
67
+ const rows: SummaryRow[] = []
68
+ rows.push({ kind: "subtotal", amount: cart.subtotal })
69
+
70
+ if ((cart.discount_total ?? 0) > 0) {
71
+ rows.push({ kind: "discount", amount: -cart.discount_total })
72
+ }
73
+
74
+ if (cart.shipping_methods && cart.shipping_methods.length > 0) {
75
+ rows.push({ kind: "shipping", amount: cart.shipping_total })
76
+ } else {
77
+ rows.push({ kind: "shipping_pending" })
78
+ }
79
+
80
+ if ((cart.tax_total ?? 0) > 0) {
81
+ rows.push({ kind: "tax", amount: cart.tax_total })
82
+ }
83
+
84
+ if ((cart.payment_method_fee_total ?? 0) > 0) {
85
+ rows.push({
86
+ kind: "payment_method_fee",
87
+ amount: cart.payment_method_fee_total!,
88
+ label: cart.payment_method_fee_label ?? null,
89
+ })
90
+ }
91
+
92
+ rows.push({ kind: "total", amount: cart.total })
93
+
94
+ if (cart.gift_cards && cart.gift_cards.length > 0) {
95
+ for (const card of cart.gift_cards) {
96
+ // `|| 0` normalizes -0 (a depleted card at amount 0) so Intl never
97
+ // renders a stray "-€0.00".
98
+ rows.push({ kind: "gift_card", last4: card.last4, amount: -card.amount || 0 })
99
+ }
100
+ rows.push({ kind: "remainder", amount: cart.gift_card_remainder ?? 0 })
101
+ }
102
+
103
+ return rows
104
+ }
105
+
106
+ export function CartSummaryBreakdown() {
107
+ const { cart, labels } = useCartDrawer()
108
+ if (!cart) return null
109
+
110
+ const currencyCode = cart.currency_code
111
+ const money = (amount: number) =>
112
+ convertToLocale({ amount, currency_code: currencyCode })
113
+ const rows = selectSummaryRows(cart)
114
+
115
+ return (
116
+ <div className="px-6 py-5 space-y-2">
117
+ {rows.map((row, i) => {
118
+ switch (row.kind) {
119
+ case "subtotal":
120
+ return (
121
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
122
+ <span>{labels.subtotal}</span>
123
+ <span className="tabular-nums">{money(row.amount)}</span>
124
+ </div>
125
+ )
126
+ case "discount":
127
+ return (
128
+ <div key={i} className="flex justify-between items-center text-[13px] text-success">
129
+ <span>{labels.discount}</span>
130
+ <span className="tabular-nums">{money(row.amount)}</span>
131
+ </div>
132
+ )
133
+ case "shipping":
134
+ return (
135
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
136
+ <span>{labels.shipping}</span>
137
+ <span className="tabular-nums">
138
+ {row.amount === 0 ? labels.free : money(row.amount)}
139
+ </span>
140
+ </div>
141
+ )
142
+ case "shipping_pending":
143
+ return (
144
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
145
+ <span>{labels.shipping}</span>
146
+ <span>{labels.calculatedAtCheckout}</span>
147
+ </div>
148
+ )
149
+ case "tax":
150
+ return (
151
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
152
+ <span>{labels.tax}</span>
153
+ <span className="tabular-nums">{money(row.amount)}</span>
154
+ </div>
155
+ )
156
+ case "payment_method_fee":
157
+ return (
158
+ <div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
159
+ <span>{row.label ?? labels.paymentMethodFee}</span>
160
+ <span className="tabular-nums">{money(row.amount)}</span>
161
+ </div>
162
+ )
163
+ case "total":
164
+ return (
165
+ <div
166
+ key={i}
167
+ className="flex justify-between items-center pt-2 mt-1 border-t border-border"
168
+ >
169
+ <span className="text-[15px] font-bold text-foreground">
170
+ {labels.total}
171
+ </span>
172
+ <span className="text-[17px] font-bold text-foreground tracking-tight tabular-nums">
173
+ {money(row.amount)}
174
+ </span>
175
+ </div>
176
+ )
177
+ case "gift_card":
178
+ return (
179
+ <div key={i} className="flex justify-between items-center text-[13px] text-success">
180
+ <span>
181
+ {labels.giftCard} ••{row.last4}
182
+ </span>
183
+ <span className="tabular-nums">{money(row.amount)}</span>
184
+ </div>
185
+ )
186
+ case "remainder":
187
+ return (
188
+ <div key={i} className="flex justify-between items-center text-[13px] font-semibold text-foreground">
189
+ <span>{labels.remainingToPay}</span>
190
+ <span className="tabular-nums">{money(row.amount)}</span>
191
+ </div>
192
+ )
193
+ }
194
+ })}
195
+ </div>
196
+ )
197
+ }