@cartbase/storefront 0.20.0 → 0.20.1
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/LICENSE +21 -21
- package/package.json +258 -258
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +778 -778
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -78
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-info.tsx +5 -5
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -110
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/lightbox-state.ts +46 -46
- package/src/reviews-ui/review-header.tsx +166 -166
- package/src/reviews-ui/review-lightbox.tsx +271 -271
- package/src/reviews-ui/review-list.tsx +193 -193
- package/src/reviews-ui/review-widget.tsx +219 -218
- package/src/reviews-ui/widget-options.ts +55 -55
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
|
@@ -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
|
+
}
|