@base44/app-plugin-commerce 0.9.5 → 0.10.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.
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { useCart, useCartUI } from "@/commerce/storefront";
3
+ import { t } from "./i18n/index.js";
4
+
5
+ /**
6
+ * Optional header trigger for the MiniCart: a bag icon with a live item-count
7
+ * badge, wired to the cart drawer. The header itself stays the store's brand
8
+ * surface — use this for the wiring and restyle it via .sfui-cart-button /
9
+ * .sfui-cart-badge (or replace it with your own button calling
10
+ * `useCartUI().toggleCart` and rendering `useCart().itemCount`).
11
+ */
12
+ export function CartButton({ className = "", label }) {
13
+ const { toggleCart } = useCartUI();
14
+ const { itemCount } = useCart();
15
+ return (
16
+ <button
17
+ type="button"
18
+ className={`sfui sfui-cart-button ${className}`}
19
+ onClick={toggleCart}
20
+ aria-label={label ?? t("minicart.button")}
21
+ >
22
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
23
+ <path d="M6 7h12l-1 13H7L6 7Z" strokeLinejoin="round" />
24
+ <path d="M9 9V6a3 3 0 0 1 6 0v3" strokeLinecap="round" />
25
+ </svg>
26
+ {itemCount > 0 && (
27
+ <span className="sfui-cart-badge" aria-hidden="true">
28
+ {itemCount > 99 ? "99+" : itemCount}
29
+ </span>
30
+ )}
31
+ </button>
32
+ );
33
+ }
@@ -0,0 +1,140 @@
1
+ import React, { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { useCart, useStoreInfo } from "@/commerce/storefront";
4
+ import { makeT } from "./i18n/index.js";
5
+ import {
6
+ CartLineRow,
7
+ CouponField,
8
+ ErrorState,
9
+ LoadingState,
10
+ SfuiRoot,
11
+ TotalsBlock,
12
+ couponVisible,
13
+ readOrderNote,
14
+ writeOrderNote,
15
+ } from "./internal.jsx";
16
+
17
+ const BRAND_KEYS = {
18
+ title: "cart.title",
19
+ summaryTitle: "cart.summaryTitle",
20
+ checkoutCta: "cart.checkoutCta",
21
+ continueShopping: "cart.continueShopping",
22
+ emptyTitle: "cart.empty.title",
23
+ emptyBody: "cart.empty.body",
24
+ emptyCta: "cart.empty.cta",
25
+ note: "cart.taxNote",
26
+ };
27
+
28
+ /**
29
+ * The routed cart page — complete: line rows with row-scoped quantity editing,
30
+ * the full totals breakdown, coupons, optional order notes, a designed empty
31
+ * state. Mount it on a route inside the store's layout; everything inside is
32
+ * shipped and tested.
33
+ *
34
+ * @param {object} props
35
+ * @param {Record<string,string>} [props.brand] wording overrides (title,
36
+ * summaryTitle, checkoutCta, continueShopping, emptyTitle, emptyBody,
37
+ * emptyCta, note) — the store's voice; labels beyond these are localized
38
+ * via ./i18n.
39
+ * @param {object} [props.sections] `{ coupon: "auto"|true|false, notes: false,
40
+ * continueShopping: true, taxNote: true }`
41
+ * @param {string} [props.checkoutHref="/checkout"]
42
+ * @param {string} [props.continueHref="/"]
43
+ * @param {(item) => string} [props.productHref] line names link to the product
44
+ * when given; plain text otherwise (never a guessed route).
45
+ * @param {object} [props.slots] `{ lineExtra?({item,line}), aboveSummary?(),
46
+ * emptyState?() }`
47
+ */
48
+ export function CartPage({
49
+ brand,
50
+ sections = {},
51
+ checkoutHref = "/checkout",
52
+ continueHref = "/",
53
+ productHref,
54
+ slots = {},
55
+ }) {
56
+ const tt = makeT(brand, BRAND_KEYS);
57
+ const cartApi = useCart();
58
+ const { info } = useStoreInfo();
59
+ const [note, setNote] = useState(readOrderNote);
60
+ const { status, cart } = cartApi;
61
+ const {
62
+ coupon = "auto",
63
+ notes = false,
64
+ continueShopping = true,
65
+ taxNote = true,
66
+ } = sections;
67
+
68
+ return (
69
+ <SfuiRoot className="sfui-cart" aria-labelledby="sfui-cart-title">
70
+ <div className="sfui-cart-inner">
71
+ <div>
72
+ <h1 id="sfui-cart-title" className="sfui-heading sfui-h1" style={{ marginBlockEnd: "1rem" }}>
73
+ {tt("cart.title")}
74
+ </h1>
75
+ {status === "loading" && <LoadingState />}
76
+ {status === "empty" &&
77
+ (slots.emptyState ? (
78
+ slots.emptyState()
79
+ ) : (
80
+ <div className="sfui-state">
81
+ <h2 className="sfui-heading sfui-h2">{tt("cart.empty.title")}</h2>
82
+ <p className="sfui-muted">{tt("cart.empty.body")}</p>
83
+ <Link className="sfui-btn sfui-btn-inline" to={continueHref}>
84
+ {tt("cart.empty.cta")}
85
+ </Link>
86
+ </div>
87
+ ))}
88
+ {status === "ready" && cartApi.error && <ErrorState onRetry={cartApi.refresh} />}
89
+ {status === "ready" && (
90
+ <ul className="sfui-lines">
91
+ {cart.items.map((item) => (
92
+ <CartLineRow
93
+ key={item.item_key}
94
+ item={item}
95
+ tt={tt}
96
+ productHref={productHref}
97
+ lineExtra={slots.lineExtra}
98
+ />
99
+ ))}
100
+ </ul>
101
+ )}
102
+ </div>
103
+
104
+ {status === "ready" && (
105
+ <aside className="sfui-panel sfui-summary" aria-label={tt("cart.summaryTitle")}>
106
+ <h2 className="sfui-heading sfui-h2">{tt("cart.summaryTitle")}</h2>
107
+ {slots.aboveSummary && slots.aboveSummary()}
108
+ {couponVisible(coupon, cart, info) && <CouponField tt={tt} />}
109
+ {notes && (
110
+ <div>
111
+ <label className="sfui-label" htmlFor="sfui-cart-note">
112
+ {tt("cart.notes.label")}
113
+ </label>
114
+ <textarea
115
+ id="sfui-cart-note"
116
+ value={note}
117
+ placeholder={tt("cart.notes.placeholder")}
118
+ onChange={(e) => {
119
+ setNote(e.target.value);
120
+ writeOrderNote(e.target.value);
121
+ }}
122
+ />
123
+ </div>
124
+ )}
125
+ <TotalsBlock cart={cart} tt={tt} />
126
+ {taxNote && <p className="sfui-note">{tt("cart.taxNote")}</p>}
127
+ <Link className="sfui-btn" to={checkoutHref}>
128
+ {tt("cart.checkoutCta")}
129
+ </Link>
130
+ {continueShopping && (
131
+ <Link className="sfui-btn sfui-btn-ghost" to={continueHref}>
132
+ {tt("cart.continueShopping")}
133
+ </Link>
134
+ )}
135
+ </aside>
136
+ )}
137
+ </div>
138
+ </SfuiRoot>
139
+ );
140
+ }
@@ -0,0 +1,287 @@
1
+ import React, { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import {
4
+ AddressFields,
5
+ CheckoutProvider,
6
+ PaymentMethodPicker,
7
+ ShippingMethodPicker,
8
+ useCheckoutContext,
9
+ useStoreInfo,
10
+ } from "@/commerce/storefront";
11
+ import { makeT } from "./i18n/index.js";
12
+ import {
13
+ CouponField,
14
+ LoadingState,
15
+ SfuiRoot,
16
+ TotalsBlock,
17
+ couponVisible,
18
+ readOrderNote,
19
+ writeOrderNote,
20
+ } from "./internal.jsx";
21
+
22
+ const BRAND_KEYS = {
23
+ title: "checkout.title",
24
+ contactTitle: "checkout.contactTitle",
25
+ shippingTitle: "checkout.shippingTitle",
26
+ paymentTitle: "checkout.paymentTitle",
27
+ summaryTitle: "checkout.summaryTitle",
28
+ submitLabel: "checkout.placeOrder",
29
+ termsLabel: "checkout.terms",
30
+ emptyTitle: "checkout.empty.title",
31
+ emptyCta: "checkout.empty.cta",
32
+ };
33
+
34
+ /**
35
+ * The complete checkout: contact + billing address (AddressFields), optional
36
+ * separate delivery address, shipping choice, payment choice, order summary,
37
+ * blockers, placeOrder — with the offline payment-instructions flow and the
38
+ * card payment-link redirect handled by useCheckout. Mount it on a route; a
39
+ * store customizes it through the theme tokens, `brand` wording and
40
+ * `sections`, never by editing this file.
41
+ *
42
+ * @param {object} props
43
+ * @param {Record<string,string>} [props.brand] wording overrides (title,
44
+ * contactTitle, shippingTitle, paymentTitle, summaryTitle, submitLabel,
45
+ * termsLabel, emptyTitle, emptyCta).
46
+ * @param {"two-column"|"single"} [props.layout="two-column"]
47
+ * @param {object} [props.sections] `{ coupon: "auto"|true|false, notes: false,
48
+ * phone: "optional"|"required"|"hidden", shipToDifferent: true,
49
+ * termsCheckbox: false }` — `phone: "required"` is enforced by marking the
50
+ * field required in the address spec via requiredBillingFields.
51
+ * @param {string} [props.continueHref="/"] where the empty state sends people.
52
+ * @param {(order) => void} [props.onPlaced] replaces the default
53
+ * order-received navigation (advanced; the default flow is complete).
54
+ */
55
+ export function CheckoutPage({
56
+ brand,
57
+ layout = "two-column",
58
+ sections = {},
59
+ continueHref = "/",
60
+ onPlaced,
61
+ }) {
62
+ const {
63
+ phone = "optional",
64
+ shipToDifferent: allowShipToDifferent = true,
65
+ } = sections;
66
+ const options = {
67
+ ...(onPlaced ? { orderReceivedPath: null } : {}),
68
+ ...(phone === "required"
69
+ ? { requiredBillingFields: ["first_name", "last_name", "address_1", "city", "country", "email", "phone"] }
70
+ : {}),
71
+ };
72
+ return (
73
+ <CheckoutProvider options={options}>
74
+ <CheckoutBody
75
+ brand={brand}
76
+ layout={layout}
77
+ sections={{ ...sections, phone, shipToDifferent: allowShipToDifferent }}
78
+ continueHref={continueHref}
79
+ onPlaced={onPlaced}
80
+ />
81
+ </CheckoutProvider>
82
+ );
83
+ }
84
+
85
+ function CheckoutBody({ brand, layout, sections, continueHref, onPlaced }) {
86
+ const tt = makeT(brand, BRAND_KEYS);
87
+ const checkout = useCheckoutContext();
88
+ const { info } = useStoreInfo();
89
+ const [note, setNote] = useState(readOrderNote);
90
+ const [termsAccepted, setTermsAccepted] = useState(false);
91
+ const { coupon = "auto", notes = false, phone, shipToDifferent, termsCheckbox = false } = sections;
92
+ const { cart, stage, blockers, canPlaceOrder, placing, orderError } = checkout;
93
+
94
+ const submit = async () => {
95
+ const extra = note.trim() ? { customer_note: note.trim() } : {};
96
+ const res = await checkout.placeOrder(extra);
97
+ if (res.ok) {
98
+ writeOrderNote("");
99
+ if (onPlaced) onPlaced(res.result);
100
+ }
101
+ };
102
+
103
+ // The submitted guard renders BEFORE the empty-cart branch: placeOrder clears
104
+ // the cart, so checking cart state first repaints "empty" over a just-placed
105
+ // order for the frames before the browser navigates away.
106
+ if (stage === "submitted") {
107
+ return (
108
+ <SfuiRoot className="sfui-checkout">
109
+ <div className="sfui-checkout-inner">
110
+ <LoadingState label={tt("checkout.submitted")} />
111
+ </div>
112
+ </SfuiRoot>
113
+ );
114
+ }
115
+
116
+ if (cart === null || (cart && !cart.items?.length)) {
117
+ return (
118
+ <SfuiRoot className="sfui-checkout">
119
+ <div className="sfui-checkout-inner">
120
+ <div className="sfui-state">
121
+ <h1 className="sfui-heading sfui-h1">{tt("checkout.empty.title")}</h1>
122
+ <Link className="sfui-btn sfui-btn-inline" to={continueHref}>
123
+ {tt("checkout.empty.cta")}
124
+ </Link>
125
+ </div>
126
+ </div>
127
+ </SfuiRoot>
128
+ );
129
+ }
130
+
131
+ const disabled = !canPlaceOrder || placing || (termsCheckbox && !termsAccepted);
132
+
133
+ return (
134
+ <SfuiRoot className="sfui-checkout" aria-labelledby="sfui-checkout-title">
135
+ <div className="sfui-checkout-inner" data-layout={layout}>
136
+ <div className="sfui-checkout-form">
137
+ <h1 id="sfui-checkout-title" className="sfui-heading sfui-h1">
138
+ {tt("checkout.title")}
139
+ </h1>
140
+
141
+ <section className="sfui-section sfui-address" aria-label={tt("checkout.contactTitle")}>
142
+ <h2 className="sfui-label">{tt("checkout.contactTitle")}</h2>
143
+ <AddressFields which="billing" includePhone={phone !== "hidden"} />
144
+ {allowsShipToDifferent(shipToDifferent) && (
145
+ <label className="sfui-checkbox">
146
+ <input
147
+ type="checkbox"
148
+ checked={checkout.shipToDifferent}
149
+ onChange={(e) => checkout.setShipToDifferent(e.target.checked)}
150
+ />
151
+ {tt("checkout.shipToDifferent")}
152
+ </label>
153
+ )}
154
+ {checkout.shipToDifferent && (
155
+ <>
156
+ <h2 className="sfui-label">{tt("checkout.shippingAddressTitle")}</h2>
157
+ <AddressFields which="shipping" includePhone={false} />
158
+ </>
159
+ )}
160
+ </section>
161
+
162
+ <ShippingMethodPicker>
163
+ {({ methods, mustChoose, single, chosen, hint }) => (
164
+ <section className="sfui-section" aria-label={tt("checkout.shippingTitle")}>
165
+ <h2 className="sfui-label">{tt("checkout.shippingTitle")}</h2>
166
+ {mustChoose && (
167
+ <div className="sfui-choices" role="radiogroup" aria-label={tt("checkout.shippingTitle")}>
168
+ {methods.map((m) => (
169
+ <label key={m.id} className="sfui-choice" data-selected={m.selected || undefined}>
170
+ <input type="radio" name="sfui-shipping" checked={m.selected} onChange={m.select} />
171
+ <span className="sfui-choice-main">
172
+ <span className="sfui-choice-title">{m.title}</span>
173
+ </span>
174
+ <span className="sfui-choice-cost">
175
+ {m.cost === 0 ? tt("common.free") : m.costLabel}
176
+ </span>
177
+ </label>
178
+ ))}
179
+ </div>
180
+ )}
181
+ {single && chosen && (
182
+ <div className="sfui-chosen-single">
183
+ <span>{chosen.title}</span>
184
+ <span>{chosen.cost === 0 ? tt("common.free") : chosen.costLabel}</span>
185
+ </div>
186
+ )}
187
+ {hint && (
188
+ <p
189
+ className={hint.severity === "error" ? "sfui-error" : "sfui-note"}
190
+ role={hint.severity === "error" ? "alert" : "status"}
191
+ >
192
+ {hint.serverMessage ?? tt(`checkout.hint.${hint.code}`)}
193
+ </p>
194
+ )}
195
+ </section>
196
+ )}
197
+ </ShippingMethodPicker>
198
+
199
+ <PaymentMethodPicker>
200
+ {({ gateways, mustChoose, single, selected, hint }) => (
201
+ <section className="sfui-section" aria-label={tt("checkout.paymentTitle")}>
202
+ <h2 className="sfui-label">{tt("checkout.paymentTitle")}</h2>
203
+ {mustChoose && (
204
+ <div className="sfui-choices" role="radiogroup" aria-label={tt("checkout.paymentTitle")}>
205
+ {gateways.map((g) => (
206
+ <label key={g.slug} className="sfui-choice" data-selected={g.selected || undefined}>
207
+ <input type="radio" name="sfui-payment" checked={g.selected} onChange={g.select} />
208
+ <span className="sfui-choice-main">
209
+ <span className="sfui-choice-title">{g.title}</span>
210
+ {g.description && <span className="sfui-choice-desc">{g.description}</span>}
211
+ </span>
212
+ </label>
213
+ ))}
214
+ </div>
215
+ )}
216
+ {single && selected && (
217
+ <div className="sfui-chosen-single">
218
+ <span className="sfui-choice-main">
219
+ <span className="sfui-choice-title">{selected.title}</span>
220
+ {selected.description && <span className="sfui-choice-desc">{selected.description}</span>}
221
+ </span>
222
+ </div>
223
+ )}
224
+ {hint && (
225
+ <p className="sfui-error" role="alert">
226
+ {tt("checkout.hint.payment_none")}
227
+ </p>
228
+ )}
229
+ </section>
230
+ )}
231
+ </PaymentMethodPicker>
232
+
233
+ {notes && (
234
+ <section className="sfui-section">
235
+ <label className="sfui-label" htmlFor="sfui-checkout-note">
236
+ {tt("checkout.notes.label")}
237
+ </label>
238
+ <textarea
239
+ id="sfui-checkout-note"
240
+ value={note}
241
+ placeholder={tt("checkout.notes.placeholder")}
242
+ onChange={(e) => {
243
+ setNote(e.target.value);
244
+ writeOrderNote(e.target.value);
245
+ }}
246
+ />
247
+ </section>
248
+ )}
249
+ </div>
250
+
251
+ <aside className="sfui-panel sfui-summary" aria-label={tt("checkout.summaryTitle")}>
252
+ <h2 className="sfui-heading sfui-h2">{tt("checkout.summaryTitle")}</h2>
253
+ {couponVisible(coupon, cart, info) && <CouponField tt={tt} />}
254
+ <TotalsBlock cart={cart} tt={tt} />
255
+ {termsCheckbox && (
256
+ <label className="sfui-checkbox">
257
+ <input type="checkbox" checked={termsAccepted} onChange={(e) => setTermsAccepted(e.target.checked)} />
258
+ {tt("checkout.terms")}
259
+ </label>
260
+ )}
261
+ <button type="button" className="sfui-btn" onClick={submit} disabled={disabled}>
262
+ {placing ? tt("checkout.placing") : tt("checkout.placeOrder")}
263
+ </button>
264
+ {/* A disabled button must say why: one line per blocker, localized. */}
265
+ {!canPlaceOrder && blockers.length > 0 && (
266
+ <ul className="sfui-blockers" aria-live="polite">
267
+ {blockers.map((code) => (
268
+ <li key={code}>{tt(`checkout.blocker.${code}`)}</li>
269
+ ))}
270
+ </ul>
271
+ )}
272
+ {orderError && (
273
+ <p className="sfui-error" role="alert">
274
+ {orderError.code === "card_payment_in_preview"
275
+ ? tt("checkout.error.card_payment_in_preview")
276
+ : orderError.message}
277
+ </p>
278
+ )}
279
+ </aside>
280
+ </div>
281
+ </SfuiRoot>
282
+ );
283
+ }
284
+
285
+ function allowsShipToDifferent(section) {
286
+ return section !== false;
287
+ }
@@ -0,0 +1,128 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { Link } from "react-router-dom";
4
+ import { useCart, useCartUI, useFormatMoney } from "@/commerce/storefront";
5
+ import { makeT } from "./i18n/index.js";
6
+ import { CartLineRow, LoadingState, SfuiRoot } from "./internal.jsx";
7
+
8
+ const BRAND_KEYS = {
9
+ title: "minicart.title",
10
+ checkoutLabel: "minicart.checkout",
11
+ viewCartLabel: "minicart.viewCart",
12
+ emptyTitle: "minicart.empty.title",
13
+ emptyCta: "minicart.empty.cta",
14
+ };
15
+
16
+ /**
17
+ * The cart drawer — a complete overlay: portal, backdrop, dialog semantics,
18
+ * focus trap, body scroll-lock. Mount it ONCE inside the store's layout (never
19
+ * on a route); it opens and closes through <CartUIProvider>'s state, so
20
+ * `useAddToCart` flows and the store's cart button drive it with no extra
21
+ * wiring. Escape and close-on-navigate are the provider's job already.
22
+ *
23
+ * @param {object} props
24
+ * @param {Record<string,string>} [props.brand] wording overrides (title,
25
+ * checkoutLabel, viewCartLabel, emptyTitle, emptyCta).
26
+ * @param {"right"|"left"} [props.side="right"]
27
+ * @param {string} [props.checkoutHref="/checkout"]
28
+ * @param {string} [props.cartHref] renders a "view cart" link when given.
29
+ * @param {(item) => string} [props.productHref]
30
+ */
31
+ export function MiniCart({ brand, side = "right", checkoutHref = "/checkout", cartHref, productHref }) {
32
+ const tt = makeT(brand, BRAND_KEYS);
33
+ const { open, closeCart } = useCartUI();
34
+ const { status, cart, itemCount } = useCart();
35
+ const formatMoney = useFormatMoney();
36
+ const panelRef = useRef(null);
37
+ const restoreRef = useRef(null);
38
+
39
+ // Focus management + scroll lock, for exactly the open window.
40
+ useEffect(() => {
41
+ if (!open) return;
42
+ restoreRef.current = document.activeElement;
43
+ const prevOverflow = document.body.style.overflow;
44
+ document.body.style.overflow = "hidden";
45
+ panelRef.current?.focus();
46
+ const trap = (e) => {
47
+ if (e.key !== "Tab" || !panelRef.current) return;
48
+ const focusables = panelRef.current.querySelectorAll(
49
+ 'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])',
50
+ );
51
+ if (!focusables.length) return;
52
+ const first = focusables[0];
53
+ const last = focusables[focusables.length - 1];
54
+ if (e.shiftKey && document.activeElement === first) {
55
+ e.preventDefault();
56
+ last.focus();
57
+ } else if (!e.shiftKey && document.activeElement === last) {
58
+ e.preventDefault();
59
+ first.focus();
60
+ }
61
+ };
62
+ document.addEventListener("keydown", trap);
63
+ return () => {
64
+ document.removeEventListener("keydown", trap);
65
+ document.body.style.overflow = prevOverflow;
66
+ restoreRef.current?.focus?.();
67
+ };
68
+ }, [open]);
69
+
70
+ if (!open || typeof document === "undefined") return null;
71
+
72
+ return createPortal(
73
+ <SfuiRoot as="div">
74
+ <div className="sfui-drawer-backdrop" onClick={closeCart} aria-hidden="true" />
75
+ <div
76
+ className="sfui-drawer"
77
+ data-side={side}
78
+ role="dialog"
79
+ aria-modal="true"
80
+ aria-label={tt("minicart.title")}
81
+ tabIndex={-1}
82
+ ref={panelRef}
83
+ >
84
+ <div className="sfui-drawer-head">
85
+ <h2 className="sfui-heading sfui-h2">{tt("minicart.title")}</h2>
86
+ <button type="button" className="sfui-drawer-close" onClick={closeCart} aria-label={tt("minicart.close")}>
87
+ ×
88
+ </button>
89
+ </div>
90
+ <div className="sfui-drawer-body">
91
+ {status === "loading" && <LoadingState />}
92
+ {status === "empty" && (
93
+ <div className="sfui-state">
94
+ <h3 className="sfui-heading sfui-h2">{tt("minicart.empty.title")}</h3>
95
+ <button type="button" className="sfui-btn sfui-btn-ghost sfui-btn-inline" onClick={closeCart}>
96
+ {tt("minicart.empty.cta")}
97
+ </button>
98
+ </div>
99
+ )}
100
+ {status === "ready" && (
101
+ <ul className="sfui-lines">
102
+ {cart.items.map((item) => (
103
+ <CartLineRow key={item.item_key} item={item} tt={tt} productHref={productHref} compact />
104
+ ))}
105
+ </ul>
106
+ )}
107
+ </div>
108
+ {status === "ready" && (
109
+ <div className="sfui-drawer-foot">
110
+ <div className="sfui-drawer-subtotal">
111
+ <span>{tt("minicart.subtotal")}</span>
112
+ <span>{formatMoney(cart.totals?.subtotal)}</span>
113
+ </div>
114
+ <Link className="sfui-btn" to={checkoutHref} onClick={closeCart}>
115
+ {tt("minicart.checkout")}
116
+ </Link>
117
+ {cartHref && (
118
+ <Link className="sfui-btn sfui-btn-ghost" to={cartHref} onClick={closeCart}>
119
+ {tt("minicart.viewCart")} {itemCount ? `(${itemCount})` : ""}
120
+ </Link>
121
+ )}
122
+ </div>
123
+ )}
124
+ </div>
125
+ </SfuiRoot>,
126
+ document.body,
127
+ );
128
+ }