@godaddy/gd-commerce-storefront 0.3.0 → 0.5.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.
- package/README.md +166 -12
- package/dist/index.d.ts +303 -9
- package/dist/index.js +1265 -617
- package/dist/styles.css +87 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Link, useParams, useSearchParams } from "react-router";
|
|
1
|
+
import { Children, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
2
|
import { useQuery } from "@tanstack/react-query";
|
|
5
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import * as Dialog from "@radix-ui/react-dialog";
|
|
5
|
+
import { Link, useParams, useSearchParams } from "react-router";
|
|
6
6
|
|
|
7
7
|
//#region src/api.ts
|
|
8
8
|
var CartIdStorage = class {
|
|
@@ -98,57 +98,6 @@ function getCartSummaryTotals(order) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region src/catalog-model.ts
|
|
103
|
-
function getSingleMatchedSku(skuGroup) {
|
|
104
|
-
const connection = skuGroup?.skus;
|
|
105
|
-
if (connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? 0) > 1) return null;
|
|
106
|
-
const edges = connection?.edges ?? [];
|
|
107
|
-
const sku = edges.length === 1 ? edges[0]?.node : null;
|
|
108
|
-
return sku?.id ? sku : null;
|
|
109
|
-
}
|
|
110
|
-
function getLabeledSkuOptions(skuGroup) {
|
|
111
|
-
if (getProductAttributes(skuGroup).length > 0) return [];
|
|
112
|
-
const connection = skuGroup?.skus;
|
|
113
|
-
const edges = connection?.edges ?? [];
|
|
114
|
-
if (edges.length < 2 || connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? edges.length) !== edges.length) return [];
|
|
115
|
-
const skus = edges.flatMap((edge) => edge?.node?.id && (edge.node.label?.trim() || edge.node.name?.trim()) ? [edge.node] : []);
|
|
116
|
-
const labels = new Set(skus.map((sku) => (sku.label?.trim() || sku.name?.trim() || "").toLowerCase()));
|
|
117
|
-
const ids = new Set(skus.map((sku) => sku.id));
|
|
118
|
-
return skus.length === edges.length && labels.size === skus.length && ids.size === skus.length ? skus : [];
|
|
119
|
-
}
|
|
120
|
-
function getAvailableInventoryQuantity(item) {
|
|
121
|
-
const edges = item?.inventoryCounts?.edges;
|
|
122
|
-
if (!edges || edges.length === 0) return null;
|
|
123
|
-
return edges.find((edge) => edge?.node?.type === "AVAILABLE")?.node?.quantity ?? 0;
|
|
124
|
-
}
|
|
125
|
-
const imageMediaTypes = new Set(["IMAGE", "PHOTO"]);
|
|
126
|
-
function getImageUrls(item) {
|
|
127
|
-
return item?.mediaObjects?.edges?.filter((edge) => imageMediaTypes.has(edge?.node?.type ?? "") && edge?.node?.url).map((edge) => edge?.node?.url).filter((url) => Boolean(url)) ?? [];
|
|
128
|
-
}
|
|
129
|
-
function getPrimaryImageUrl(item) {
|
|
130
|
-
return getImageUrls(item)[0] ?? null;
|
|
131
|
-
}
|
|
132
|
-
function getProductAttributes(skuGroup) {
|
|
133
|
-
return skuGroup?.attributes?.edges?.map((edge) => {
|
|
134
|
-
const attributeNode = edge?.node;
|
|
135
|
-
const values = attributeNode?.values?.edges?.map((valueEdge) => {
|
|
136
|
-
const valueNode = valueEdge?.node;
|
|
137
|
-
return {
|
|
138
|
-
id: valueNode?.id || "",
|
|
139
|
-
name: valueNode?.name || "",
|
|
140
|
-
label: valueNode?.label || valueNode?.name || ""
|
|
141
|
-
};
|
|
142
|
-
}) ?? [];
|
|
143
|
-
return {
|
|
144
|
-
id: attributeNode?.id || "",
|
|
145
|
-
name: attributeNode?.name || "",
|
|
146
|
-
label: attributeNode?.label || attributeNode?.name || "",
|
|
147
|
-
values
|
|
148
|
-
};
|
|
149
|
-
}) ?? [];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
101
|
//#endregion
|
|
153
102
|
//#region src/sample-cart.tsx
|
|
154
103
|
const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
|
|
@@ -156,6 +105,7 @@ const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
|
|
|
156
105
|
name: `Product ${index + 1}`,
|
|
157
106
|
price: (index + 1) * 1e3
|
|
158
107
|
}));
|
|
108
|
+
const noQuantities = {};
|
|
159
109
|
const SampleCartContext = createContext(null);
|
|
160
110
|
function useSampleCart() {
|
|
161
111
|
const value = useContext(SampleCartContext);
|
|
@@ -180,7 +130,11 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
180
130
|
active.current = null;
|
|
181
131
|
};
|
|
182
132
|
}, [session]);
|
|
183
|
-
const quantities = state.session === session && enabled ? state.quantities :
|
|
133
|
+
const quantities = state.session === session && enabled ? state.quantities : noQuantities;
|
|
134
|
+
const items = useMemo(() => sampleProducts.flatMap((product) => quantities[product.key] ? [{
|
|
135
|
+
...product,
|
|
136
|
+
quantity: quantities[product.key]
|
|
137
|
+
}] : []), [quantities]);
|
|
184
138
|
function update(key, quantity) {
|
|
185
139
|
if (!enabled || active.current !== session || !sampleProducts.some((product) => product.key === key)) return false;
|
|
186
140
|
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
|
|
@@ -200,10 +154,7 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
200
154
|
}
|
|
201
155
|
return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
|
|
202
156
|
value: {
|
|
203
|
-
items
|
|
204
|
-
...product,
|
|
205
|
-
quantity: quantities[product.key]
|
|
206
|
-
}] : []),
|
|
157
|
+
items,
|
|
207
158
|
add: (key) => {
|
|
208
159
|
const product = sampleProducts.find((product$1) => product$1.key === key);
|
|
209
160
|
if (!product || !update(key)) return false;
|
|
@@ -228,6 +179,8 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
228
179
|
//#endregion
|
|
229
180
|
//#region src/commerce-provider.tsx
|
|
230
181
|
const CommerceContext = createContext(null);
|
|
182
|
+
/** Lets a mounted cart drawer register itself, so adds open it only when one exists. Not public. */
|
|
183
|
+
const CartDrawerRegistry = createContext(null);
|
|
231
184
|
function useCommerce() {
|
|
232
185
|
const context = useContext(CommerceContext);
|
|
233
186
|
if (!context) throw new Error("Mount CommerceProvider once around the header and page outlet, inside the router.");
|
|
@@ -247,7 +200,7 @@ async function loadConfig(signal) {
|
|
|
247
200
|
currencyCode: config.currencyCode
|
|
248
201
|
};
|
|
249
202
|
}
|
|
250
|
-
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {} }) {
|
|
203
|
+
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, onItemAdded }) {
|
|
251
204
|
const config = useQuery({
|
|
252
205
|
retry: false,
|
|
253
206
|
queryKey: ["commerce", "configuration"],
|
|
@@ -276,10 +229,11 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
276
229
|
config.refetch();
|
|
277
230
|
},
|
|
278
231
|
theme,
|
|
232
|
+
onItemAdded,
|
|
279
233
|
children
|
|
280
234
|
});
|
|
281
235
|
}
|
|
282
|
-
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme }) {
|
|
236
|
+
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, onItemAdded }) {
|
|
283
237
|
const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
|
|
284
238
|
const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
|
|
285
239
|
const ready = connection === "ready";
|
|
@@ -303,6 +257,23 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
303
257
|
const queue = useRef(Promise.resolve(true));
|
|
304
258
|
const alive = useRef(true);
|
|
305
259
|
const opener = useRef(null);
|
|
260
|
+
const added = useRef(onItemAdded);
|
|
261
|
+
added.current = onItemAdded;
|
|
262
|
+
const drawers = useRef(0);
|
|
263
|
+
const registerDrawer = useCallback(() => {
|
|
264
|
+
drawers.current += 1;
|
|
265
|
+
if (process.env.NODE_ENV !== "production" && drawers.current === 2) console.warn("[gd-commerce-storefront] More than one cart drawer is mounted, so every add opens all of them. CommerceStorefront already mounts CartDrawer; use CommerceProvider to mount your own CartDrawerRoot.");
|
|
266
|
+
return () => {
|
|
267
|
+
drawers.current -= 1;
|
|
268
|
+
if (drawers.current === 0) setOpen(false);
|
|
269
|
+
};
|
|
270
|
+
}, []);
|
|
271
|
+
/** Opens the drawer for an add, if one is mounted. */
|
|
272
|
+
function openDrawer() {
|
|
273
|
+
if (drawers.current === 0) return;
|
|
274
|
+
rememberOpener();
|
|
275
|
+
setOpen(true);
|
|
276
|
+
}
|
|
306
277
|
function rememberOpener() {
|
|
307
278
|
opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
308
279
|
}
|
|
@@ -432,7 +403,12 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
432
403
|
if (!isActive()) return;
|
|
433
404
|
commit(result.cart);
|
|
434
405
|
setAnnouncement(`${item.name} added to your cart.`);
|
|
435
|
-
|
|
406
|
+
openDrawer();
|
|
407
|
+
added.current?.({
|
|
408
|
+
name: item.name,
|
|
409
|
+
quantity: item.quantity,
|
|
410
|
+
isSample: false
|
|
411
|
+
});
|
|
436
412
|
});
|
|
437
413
|
}
|
|
438
414
|
function mutateItem(itemId, method, quantity) {
|
|
@@ -501,6 +477,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
501
477
|
cart,
|
|
502
478
|
open,
|
|
503
479
|
setOpen: (next) => {
|
|
480
|
+
if (next && drawers.current === 0) return;
|
|
504
481
|
if (next) rememberOpener();
|
|
505
482
|
setOpen(next);
|
|
506
483
|
},
|
|
@@ -519,15 +496,22 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
519
496
|
refresh,
|
|
520
497
|
checkout
|
|
521
498
|
},
|
|
522
|
-
children: [/* @__PURE__ */ jsx(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
499
|
+
children: [/* @__PURE__ */ jsx(CartDrawerRegistry.Provider, {
|
|
500
|
+
value: registerDrawer,
|
|
501
|
+
children: /* @__PURE__ */ jsx(SampleCartProvider, {
|
|
502
|
+
enabled: connection === "unbound",
|
|
503
|
+
announce: setAnnouncement,
|
|
504
|
+
onAdd: (name) => {
|
|
505
|
+
setAnnouncement(`${name} added to your sample cart.`);
|
|
506
|
+
openDrawer();
|
|
507
|
+
added.current?.({
|
|
508
|
+
name,
|
|
509
|
+
quantity: 1,
|
|
510
|
+
isSample: true
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
children
|
|
514
|
+
})
|
|
531
515
|
}), /* @__PURE__ */ jsx("span", {
|
|
532
516
|
className: "commerce-sr-only",
|
|
533
517
|
role: "status",
|
|
@@ -537,6 +521,117 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
537
521
|
});
|
|
538
522
|
}
|
|
539
523
|
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/cart-state.ts
|
|
526
|
+
/** Read the cart from anywhere inside `CommerceProvider`, for example to build a custom cart button. */
|
|
527
|
+
function useCart() {
|
|
528
|
+
const { cart, config, connection, hydrating } = useCommerce();
|
|
529
|
+
const sample = useSampleCart();
|
|
530
|
+
const isSample = connection === "unbound";
|
|
531
|
+
const sampleItems$1 = sample.items;
|
|
532
|
+
return useMemo(() => {
|
|
533
|
+
if (isSample) return {
|
|
534
|
+
items: sampleItems$1.map((item) => ({
|
|
535
|
+
id: item.key,
|
|
536
|
+
name: item.name,
|
|
537
|
+
quantity: item.quantity,
|
|
538
|
+
imageUrl: null,
|
|
539
|
+
options: [],
|
|
540
|
+
subtotal: item.price * item.quantity,
|
|
541
|
+
currency: "USD"
|
|
542
|
+
})),
|
|
543
|
+
itemCount: sampleItems$1.reduce((total, item) => total + item.quantity, 0),
|
|
544
|
+
subtotal: sampleItems$1.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
545
|
+
currency: "USD",
|
|
546
|
+
isSample,
|
|
547
|
+
loading: false
|
|
548
|
+
};
|
|
549
|
+
const currency = cart?.totals?.total?.currencyCode ?? config.currencyCode;
|
|
550
|
+
const lines = cart?.lineItems ?? [];
|
|
551
|
+
const subtotal = cart?.totals?.subTotal?.value;
|
|
552
|
+
return {
|
|
553
|
+
items: lines.map((item) => ({
|
|
554
|
+
id: item.id ?? "",
|
|
555
|
+
name: item.name ?? "",
|
|
556
|
+
quantity: item.quantity ?? 0,
|
|
557
|
+
imageUrl: item.details?.productAssetUrl || null,
|
|
558
|
+
options: (item.details?.selectedOptions ?? []).map((option) => ({
|
|
559
|
+
attribute: option.attribute ?? "",
|
|
560
|
+
values: option.values ?? []
|
|
561
|
+
})),
|
|
562
|
+
subtotal: typeof item.totals?.subTotal?.value === "number" ? item.totals.subTotal.value : null,
|
|
563
|
+
currency: item.totals?.subTotal?.currencyCode ?? currency
|
|
564
|
+
})),
|
|
565
|
+
itemCount: lines.reduce((total, item) => total + (item.quantity ?? 0), 0),
|
|
566
|
+
subtotal: typeof subtotal === "number" ? subtotal : null,
|
|
567
|
+
currency: cart?.totals?.subTotal?.currencyCode ?? currency,
|
|
568
|
+
isSample,
|
|
569
|
+
loading: hydrating
|
|
570
|
+
};
|
|
571
|
+
}, [
|
|
572
|
+
cart,
|
|
573
|
+
config.currencyCode,
|
|
574
|
+
hydrating,
|
|
575
|
+
isSample,
|
|
576
|
+
sampleItems$1
|
|
577
|
+
]);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/catalog-model.ts
|
|
582
|
+
function getSingleMatchedSku(skuGroup) {
|
|
583
|
+
const connection = skuGroup?.skus;
|
|
584
|
+
if (connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? 0) > 1) return null;
|
|
585
|
+
const edges = connection?.edges ?? [];
|
|
586
|
+
const sku = edges.length === 1 ? edges[0]?.node : null;
|
|
587
|
+
return sku?.id ? sku : null;
|
|
588
|
+
}
|
|
589
|
+
function getLabeledSkuOptions(skuGroup) {
|
|
590
|
+
if (getProductAttributes(skuGroup).length > 0) return [];
|
|
591
|
+
const connection = skuGroup?.skus;
|
|
592
|
+
const edges = connection?.edges ?? [];
|
|
593
|
+
if (edges.length < 2 || connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? edges.length) !== edges.length) return [];
|
|
594
|
+
const skus = edges.flatMap((edge) => edge?.node?.id && (edge.node.label?.trim() || edge.node.name?.trim()) ? [edge.node] : []);
|
|
595
|
+
const labels = new Set(skus.map((sku) => (sku.label?.trim() || sku.name?.trim() || "").toLowerCase()));
|
|
596
|
+
const ids = new Set(skus.map((sku) => sku.id));
|
|
597
|
+
return skus.length === edges.length && labels.size === skus.length && ids.size === skus.length ? skus : [];
|
|
598
|
+
}
|
|
599
|
+
function getAvailableInventoryQuantity(item) {
|
|
600
|
+
const edges = item?.inventoryCounts?.edges;
|
|
601
|
+
if (!edges || edges.length === 0) return null;
|
|
602
|
+
return edges.find((edge) => edge?.node?.type === "AVAILABLE")?.node?.quantity ?? 0;
|
|
603
|
+
}
|
|
604
|
+
const imageMediaTypes = new Set(["IMAGE", "PHOTO"]);
|
|
605
|
+
function getImageUrls(item) {
|
|
606
|
+
return item?.mediaObjects?.edges?.filter((edge) => imageMediaTypes.has(edge?.node?.type ?? "") && edge?.node?.url).map((edge) => edge?.node?.url).filter((url) => Boolean(url)) ?? [];
|
|
607
|
+
}
|
|
608
|
+
function getPrimaryImageUrl(item) {
|
|
609
|
+
return getImageUrls(item)[0] ?? null;
|
|
610
|
+
}
|
|
611
|
+
function getProductAttributes(skuGroup) {
|
|
612
|
+
return skuGroup?.attributes?.edges?.map((edge) => {
|
|
613
|
+
const attributeNode = edge?.node;
|
|
614
|
+
const values = attributeNode?.values?.edges?.map((valueEdge) => {
|
|
615
|
+
const valueNode = valueEdge?.node;
|
|
616
|
+
return {
|
|
617
|
+
id: valueNode?.id || "",
|
|
618
|
+
name: valueNode?.name || "",
|
|
619
|
+
label: valueNode?.label || valueNode?.name || ""
|
|
620
|
+
};
|
|
621
|
+
}) ?? [];
|
|
622
|
+
return {
|
|
623
|
+
id: attributeNode?.id || "",
|
|
624
|
+
name: attributeNode?.name || "",
|
|
625
|
+
label: attributeNode?.label || attributeNode?.name || "",
|
|
626
|
+
values
|
|
627
|
+
};
|
|
628
|
+
}) ?? [];
|
|
629
|
+
}
|
|
630
|
+
/** The product's display name: its label, then its name. */
|
|
631
|
+
function getProductName(skuGroup) {
|
|
632
|
+
return skuGroup.label ?? skuGroup.name ?? "Product";
|
|
633
|
+
}
|
|
634
|
+
|
|
540
635
|
//#endregion
|
|
541
636
|
//#region src/storefront-surface.tsx
|
|
542
637
|
function StorefrontSurface({ children, className = "" }) {
|
|
@@ -572,9 +667,8 @@ function CommerceStatus() {
|
|
|
572
667
|
const buttonClass = "inline-flex min-h-11 items-center justify-center gap-2 rounded-lg bg-commerce-accent px-5 py-2 text-sm font-semibold text-commerce-on-accent hover:bg-commerce-accent-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900 disabled:cursor-not-allowed disabled:bg-neutral-200 disabled:text-neutral-700 disabled:hover:bg-neutral-200";
|
|
573
668
|
const inputClass = "min-h-11 rounded-lg border border-neutral-400 bg-white px-3 py-2 text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900";
|
|
574
669
|
function CartButton() {
|
|
575
|
-
const {
|
|
576
|
-
const
|
|
577
|
-
const count = connection === "unbound" ? sample.items.reduce((total, item) => total + item.quantity, 0) : cart?.lineItems?.reduce((total, item) => total + (item.quantity ?? 0), 0) ?? 0;
|
|
670
|
+
const { setOpen, open, connection } = useCommerce();
|
|
671
|
+
const { itemCount } = useCart();
|
|
578
672
|
return /* @__PURE__ */ jsx(StorefrontSurface, {
|
|
579
673
|
className: "commerce-inline",
|
|
580
674
|
children: /* @__PURE__ */ jsxs("button", {
|
|
@@ -586,16 +680,32 @@ function CartButton() {
|
|
|
586
680
|
onClick: () => setOpen(true),
|
|
587
681
|
children: ["Cart ", /* @__PURE__ */ jsx("span", {
|
|
588
682
|
className: "rounded-full bg-white px-2 py-0.5 text-xs text-neutral-900",
|
|
589
|
-
children:
|
|
683
|
+
children: itemCount
|
|
590
684
|
})]
|
|
591
685
|
})
|
|
592
686
|
});
|
|
593
687
|
}
|
|
688
|
+
/** Whether tracked stock is used up. Oversold SKUs report a negative count. */
|
|
689
|
+
const isSoldOut = (available) => available !== null && available <= 0;
|
|
690
|
+
/** Why `quantity` cannot be added, or `null` when it can. `available` is `null` when stock is not tracked. */
|
|
691
|
+
function quantityProblem(quantity, available) {
|
|
692
|
+
if (!Number.isInteger(quantity) || quantity < 1) return "Enter a whole number of 1 or more.";
|
|
693
|
+
if (available !== null && available > 0 && quantity > available) return `Only ${available} available.`;
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
594
696
|
function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
595
697
|
const { addItem, pending, hydrating, error, open, connection } = useCommerce();
|
|
596
698
|
const [adding, setAdding] = useState(false);
|
|
699
|
+
const reasonId = useId();
|
|
597
700
|
const available = getAvailableInventoryQuantity(sku);
|
|
598
|
-
const
|
|
701
|
+
const soldOut = isSoldOut(available);
|
|
702
|
+
let reason = null;
|
|
703
|
+
if (connection === "unbound") reason = "Connect a store to add items to the cart.";
|
|
704
|
+
else if (connection === "error") reason = "The store could not be reached.";
|
|
705
|
+
else if (connection !== "ready") reason = "Connecting to the store…";
|
|
706
|
+
else if (!sku.id) reason = "This item cannot be added to the cart.";
|
|
707
|
+
else if (!soldOut) reason = hydrating ? "Loading your cart…" : quantityProblem(quantity, available);
|
|
708
|
+
const disabled = adding || soldOut || reason !== null;
|
|
599
709
|
async function handleAdd() {
|
|
600
710
|
if (disabled || pending || !sku.id) return;
|
|
601
711
|
setAdding(true);
|
|
@@ -611,394 +721,907 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
|
611
721
|
}
|
|
612
722
|
return /* @__PURE__ */ jsxs(StorefrontSurface, {
|
|
613
723
|
className: "commerce-inline",
|
|
614
|
-
children: [
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
724
|
+
children: [
|
|
725
|
+
/* @__PURE__ */ jsx("button", {
|
|
726
|
+
type: "button",
|
|
727
|
+
className: `${buttonClass} w-full`,
|
|
728
|
+
disabled,
|
|
729
|
+
"aria-disabled": disabled || pending,
|
|
730
|
+
"aria-busy": adding,
|
|
731
|
+
"aria-describedby": reason ? reasonId : void 0,
|
|
732
|
+
onClick: () => void handleAdd(),
|
|
733
|
+
children: soldOut ? "Out of stock" : adding ? "Adding…" : "Add to cart"
|
|
734
|
+
}),
|
|
735
|
+
reason && /* @__PURE__ */ jsx("span", {
|
|
736
|
+
id: reasonId,
|
|
737
|
+
hidden: true,
|
|
738
|
+
children: reason
|
|
739
|
+
}),
|
|
740
|
+
error && !open && /* @__PURE__ */ jsx("p", {
|
|
741
|
+
role: "alert",
|
|
742
|
+
className: "mt-3 bg-white text-sm text-red-700",
|
|
743
|
+
children: error
|
|
744
|
+
})
|
|
745
|
+
]
|
|
627
746
|
});
|
|
628
747
|
}
|
|
629
|
-
|
|
630
|
-
|
|
748
|
+
|
|
749
|
+
//#endregion
|
|
750
|
+
//#region src/part.ts
|
|
751
|
+
/** Whether `children` renders anything, so `{custom && <Part />}` with `custom` false falls back to the default. */
|
|
752
|
+
const hasChildren = (children) => Children.toArray(children).some((child) => child !== "");
|
|
753
|
+
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
754
|
+
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
755
|
+
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
756
|
+
/**
|
|
757
|
+
* Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
|
|
758
|
+
* children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
|
|
759
|
+
*/
|
|
760
|
+
function usePartRegistry(onChange) {
|
|
761
|
+
const counts = useRef(/* @__PURE__ */ new Map());
|
|
762
|
+
const has = useCallback((key) => counts.current.has(key), []);
|
|
763
|
+
const register = useCallback((key) => {
|
|
764
|
+
counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
|
|
765
|
+
onChange?.(has);
|
|
766
|
+
return () => {
|
|
767
|
+
const count = (counts.current.get(key) ?? 1) - 1;
|
|
768
|
+
if (count > 0) counts.current.set(key, count);
|
|
769
|
+
else counts.current.delete(key);
|
|
770
|
+
onChange?.(has);
|
|
771
|
+
};
|
|
772
|
+
}, [has, onChange]);
|
|
773
|
+
return useMemo(() => ({
|
|
774
|
+
register,
|
|
775
|
+
has
|
|
776
|
+
}), [register, has]);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/cart-parts.tsx
|
|
781
|
+
const CartContext = createContext(null);
|
|
782
|
+
const CartDrawerContext = createContext(null);
|
|
783
|
+
function useCartInternal(part) {
|
|
784
|
+
const context = useContext(CartContext);
|
|
785
|
+
if (!context) throw new Error(`${part} must be rendered inside <CartRoot> or <CartDrawerRoot>.`);
|
|
786
|
+
return context;
|
|
787
|
+
}
|
|
788
|
+
function CartProvider({ children }) {
|
|
789
|
+
const commerce = useCommerce();
|
|
631
790
|
const sample = useSampleCart();
|
|
791
|
+
const state = useCart();
|
|
792
|
+
const [action, setAction] = useState(null);
|
|
793
|
+
const locked = commerce.pending || commerce.hydrating || action !== null;
|
|
794
|
+
const value = {
|
|
795
|
+
...state,
|
|
796
|
+
hydrating: commerce.hydrating,
|
|
797
|
+
error: commerce.error,
|
|
798
|
+
storageWarning: commerce.storageWarning,
|
|
799
|
+
catalogPath: commerce.config.catalogPath,
|
|
800
|
+
checkoutEnabled: !!commerce.config.checkoutSuccessPath,
|
|
801
|
+
checkoutReady: commerce.cart?.totals?.total?.value != null,
|
|
802
|
+
action,
|
|
803
|
+
locked,
|
|
804
|
+
run: (next, operation) => {
|
|
805
|
+
if (locked) return;
|
|
806
|
+
setAction(next);
|
|
807
|
+
operation().finally(() => setAction(null));
|
|
808
|
+
},
|
|
809
|
+
changeQuantity: state.isSample ? sample.changeQuantity : commerce.changeQuantity,
|
|
810
|
+
removeItem: state.isSample ? sample.remove : commerce.removeItem,
|
|
811
|
+
refresh: commerce.refresh,
|
|
812
|
+
checkout: commerce.checkout,
|
|
813
|
+
close: () => commerce.setOpen(false)
|
|
814
|
+
};
|
|
815
|
+
return /* @__PURE__ */ jsx(CartContext.Provider, {
|
|
816
|
+
value,
|
|
817
|
+
children
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Provides the cart to cart parts on a page, such as a `/cart` route. While no store is connected it shows the sample
|
|
822
|
+
* cart. Use `CartDrawerRoot` for the drawer.
|
|
823
|
+
*/
|
|
824
|
+
function CartRoot({ children, className }) {
|
|
825
|
+
const { connection } = useCommerce();
|
|
632
826
|
const isSample = connection === "unbound";
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
const subtotal = isSample ? sample.items.reduce((sum, item) => sum + item.price * item.quantity, 0) : summary.subtotal;
|
|
658
|
-
async function handleAction(action, operation) {
|
|
659
|
-
if (locked) return;
|
|
660
|
-
setDrawerAction(action);
|
|
661
|
-
try {
|
|
662
|
-
await operation();
|
|
663
|
-
} finally {
|
|
664
|
-
setDrawerAction(null);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
827
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" || isSample ? /* @__PURE__ */ jsx(CartProvider, { children: /* @__PURE__ */ jsx("section", {
|
|
828
|
+
className: partClass(className),
|
|
829
|
+
"data-commerce-source": isSample ? "sample" : void 0,
|
|
830
|
+
children
|
|
831
|
+
}) }) : /* @__PURE__ */ jsx("div", {
|
|
832
|
+
className: partClass(className),
|
|
833
|
+
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
834
|
+
}) });
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* The cart dialog. Opens when an item is added or a `CartButton` is pressed, and returns focus when it closes.
|
|
838
|
+
* The panel sits at the right edge at full height; `mr-auto` on it moves it to the left. Mount one per provider,
|
|
839
|
+
* in place of `CartDrawer`.
|
|
840
|
+
*/
|
|
841
|
+
function CartDrawerRoot({ children, className }) {
|
|
842
|
+
const { open, setOpen, restoreFocus, connection } = useCommerce();
|
|
843
|
+
const registerDrawer = useContext(CartDrawerRegistry);
|
|
844
|
+
useEffect(() => registerDrawer?.(), [registerDrawer]);
|
|
845
|
+
const [described, setDescribed] = useState(false);
|
|
846
|
+
const { register, has } = usePartRegistry(useCallback((registered) => setDescribed(registered("description")), []));
|
|
847
|
+
useEffect(() => {
|
|
848
|
+
setDescribed(has("description"));
|
|
849
|
+
}, [has]);
|
|
850
|
+
const drawer = useMemo(() => ({ register }), [register]);
|
|
667
851
|
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
668
852
|
open,
|
|
669
853
|
onOpenChange: setOpen,
|
|
670
|
-
children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40" }), /* @__PURE__ */
|
|
671
|
-
"
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}), /* @__PURE__ */ jsx(Dialog.Description, {
|
|
684
|
-
className: "mt-1 text-sm text-neutral-600",
|
|
685
|
-
children: isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout."
|
|
686
|
-
})] }), /* @__PURE__ */ jsx(Dialog.Close, {
|
|
687
|
-
className: "min-h-11 min-w-11 rounded-lg border border-neutral-400 bg-white px-3 text-neutral-900 focus-visible:outline focus-visible:outline-2",
|
|
688
|
-
"aria-label": "Close cart",
|
|
689
|
-
children: "✕"
|
|
690
|
-
})]
|
|
691
|
-
}),
|
|
692
|
-
/* @__PURE__ */ jsxs("div", {
|
|
693
|
-
className: "flex-1 overflow-y-auto p-6",
|
|
694
|
-
children: [
|
|
695
|
-
storageWarning && /* @__PURE__ */ jsx("p", {
|
|
696
|
-
role: "status",
|
|
697
|
-
className: "mb-4 text-sm text-amber-800",
|
|
698
|
-
children: storageWarning
|
|
699
|
-
}),
|
|
700
|
-
error && /* @__PURE__ */ jsxs("div", {
|
|
701
|
-
role: "alert",
|
|
702
|
-
className: "mb-4 rounded-lg bg-red-50 p-4 text-red-800",
|
|
703
|
-
children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
|
|
704
|
-
type: "button",
|
|
705
|
-
className: "mt-2 min-h-11 bg-red-50 text-red-800 underline disabled:cursor-not-allowed",
|
|
706
|
-
disabled: drawerAction === "refresh",
|
|
707
|
-
"aria-disabled": locked,
|
|
708
|
-
"aria-busy": drawerAction === "refresh",
|
|
709
|
-
onClick: () => void handleAction("refresh", refresh),
|
|
710
|
-
children: "Refresh cart"
|
|
711
|
-
})]
|
|
712
|
-
}),
|
|
713
|
-
hydrating && /* @__PURE__ */ jsx("p", {
|
|
714
|
-
role: "status",
|
|
715
|
-
children: "Loading your cart…"
|
|
716
|
-
}),
|
|
717
|
-
!hydrating && !items.length && /* @__PURE__ */ jsxs("div", {
|
|
718
|
-
className: "py-16 text-center",
|
|
719
|
-
children: [
|
|
720
|
-
/* @__PURE__ */ jsx("p", {
|
|
721
|
-
className: "text-xl font-medium",
|
|
722
|
-
children: "Your cart is empty"
|
|
723
|
-
}),
|
|
724
|
-
/* @__PURE__ */ jsx("p", {
|
|
725
|
-
className: "mt-2 text-neutral-600",
|
|
726
|
-
children: isSample ? "Add a sample product to try the cart." : "Find something you love in the shop."
|
|
727
|
-
}),
|
|
728
|
-
/* @__PURE__ */ jsx(Link, {
|
|
729
|
-
className: `${buttonClass} mt-6`,
|
|
730
|
-
to: config.catalogPath,
|
|
731
|
-
onClick: () => setOpen(false),
|
|
732
|
-
children: "Browse products"
|
|
733
|
-
})
|
|
734
|
-
]
|
|
735
|
-
}),
|
|
736
|
-
/* @__PURE__ */ jsx("ul", {
|
|
737
|
-
className: "divide-y divide-neutral-200",
|
|
738
|
-
children: items.map((item) => /* @__PURE__ */ jsxs("li", {
|
|
739
|
-
className: "py-5 first:pt-0",
|
|
740
|
-
"data-testid": "cart-item",
|
|
741
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
742
|
-
className: "flex gap-4",
|
|
743
|
-
children: [item.image && /* @__PURE__ */ jsx("img", {
|
|
744
|
-
src: item.image,
|
|
745
|
-
alt: "",
|
|
746
|
-
className: "h-20 w-20 rounded-lg bg-neutral-100 object-cover"
|
|
747
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
748
|
-
className: "min-w-0 flex-1",
|
|
749
|
-
children: [
|
|
750
|
-
/* @__PURE__ */ jsx("h3", {
|
|
751
|
-
className: "font-semibold",
|
|
752
|
-
children: item.name
|
|
753
|
-
}),
|
|
754
|
-
item.options?.map((option) => /* @__PURE__ */ jsxs("p", {
|
|
755
|
-
className: "text-sm text-neutral-600",
|
|
756
|
-
children: [
|
|
757
|
-
option.attribute,
|
|
758
|
-
": ",
|
|
759
|
-
option.values?.join(", ")
|
|
760
|
-
]
|
|
761
|
-
}, `${option.attribute}:${option.values?.join(",")}`)),
|
|
762
|
-
/* @__PURE__ */ jsx("p", {
|
|
763
|
-
className: "mt-1 text-sm",
|
|
764
|
-
children: typeof item.subtotal === "number" ? money(item.subtotal, item.currency ?? currency) : "Price unavailable"
|
|
765
|
-
})
|
|
766
|
-
]
|
|
767
|
-
})]
|
|
768
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
769
|
-
className: "mt-4 flex items-center justify-between gap-2",
|
|
770
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
771
|
-
className: "flex items-center rounded-lg border border-neutral-300",
|
|
772
|
-
children: [
|
|
773
|
-
/* @__PURE__ */ jsx("button", {
|
|
774
|
-
type: "button",
|
|
775
|
-
className: "min-h-11 min-w-11 rounded-lg bg-white text-neutral-900 focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:text-neutral-600",
|
|
776
|
-
"aria-label": `Decrease quantity of ${item.name}`,
|
|
777
|
-
disabled: drawerAction === `decrease:${item.id}` || !item.id || (item.quantity ?? 0) <= 1,
|
|
778
|
-
"aria-disabled": locked || !item.id || (item.quantity ?? 0) <= 1,
|
|
779
|
-
"aria-busy": drawerAction === `decrease:${item.id}`,
|
|
780
|
-
onClick: () => void handleAction(`decrease:${item.id}`, () => changeQuantity(item.id ?? "", (item.quantity ?? 1) - 1)),
|
|
781
|
-
children: "−"
|
|
782
|
-
}),
|
|
783
|
-
/* @__PURE__ */ jsxs("span", {
|
|
784
|
-
className: "min-w-8 text-center",
|
|
785
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
786
|
-
className: "sr-only",
|
|
787
|
-
children: "Quantity: "
|
|
788
|
-
}), item.quantity]
|
|
789
|
-
}),
|
|
790
|
-
/* @__PURE__ */ jsx("button", {
|
|
791
|
-
type: "button",
|
|
792
|
-
className: "min-h-11 min-w-11 rounded-lg bg-white text-neutral-900 focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:text-neutral-600",
|
|
793
|
-
"aria-label": `Increase quantity of ${item.name}`,
|
|
794
|
-
disabled: drawerAction === `increase:${item.id}` || !item.id,
|
|
795
|
-
"aria-disabled": locked || !item.id,
|
|
796
|
-
"aria-busy": drawerAction === `increase:${item.id}`,
|
|
797
|
-
onClick: () => void handleAction(`increase:${item.id}`, () => changeQuantity(item.id ?? "", (item.quantity ?? 1) + 1)),
|
|
798
|
-
children: "+"
|
|
799
|
-
})
|
|
800
|
-
]
|
|
801
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
802
|
-
type: "button",
|
|
803
|
-
className: "min-h-11 bg-white px-2 text-sm text-neutral-900 underline focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:text-neutral-600",
|
|
804
|
-
"aria-label": `Remove ${item.name}`,
|
|
805
|
-
disabled: drawerAction === `remove:${item.id}` || !item.id,
|
|
806
|
-
"aria-disabled": locked || !item.id,
|
|
807
|
-
"aria-busy": drawerAction === `remove:${item.id}`,
|
|
808
|
-
onClick: () => void handleAction(`remove:${item.id}`, () => removeItem(item.id ?? "")),
|
|
809
|
-
children: "Remove"
|
|
810
|
-
})]
|
|
811
|
-
})]
|
|
812
|
-
}, item.id))
|
|
813
|
-
})
|
|
814
|
-
]
|
|
815
|
-
}),
|
|
816
|
-
items.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
817
|
-
className: "border-t border-neutral-200 p-6",
|
|
818
|
-
children: [
|
|
819
|
-
/* @__PURE__ */ jsx("dl", {
|
|
820
|
-
className: "space-y-2 text-sm",
|
|
821
|
-
children: (isSample || typeof cart?.totals?.subTotal?.value === "number") && /* @__PURE__ */ jsxs("div", {
|
|
822
|
-
className: "flex justify-between",
|
|
823
|
-
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money(subtotal, isSample ? "USD" : cart?.totals?.subTotal?.currencyCode ?? currency) })]
|
|
824
|
-
})
|
|
825
|
-
}),
|
|
826
|
-
!isSample && /* @__PURE__ */ jsx("p", {
|
|
827
|
-
className: "commerce-cart-checkout-adjustments-note mt-3 text-sm text-neutral-600",
|
|
828
|
-
children: "Shipping, taxes, and discounts are calculated at checkout."
|
|
829
|
-
}),
|
|
830
|
-
isSample ? /* @__PURE__ */ jsx("p", {
|
|
831
|
-
className: "text-sm text-neutral-600",
|
|
832
|
-
children: "Example total in USD. Checkout is not set up yet."
|
|
833
|
-
}) : config.checkoutSuccessPath ? /* @__PURE__ */ jsx("button", {
|
|
834
|
-
type: "button",
|
|
835
|
-
className: `${buttonClass} w-full`,
|
|
836
|
-
disabled: checkingOut || hydrating || cart?.totals?.total?.value == null,
|
|
837
|
-
"aria-disabled": locked || cart?.totals?.total?.value == null,
|
|
838
|
-
"aria-busy": checkingOut,
|
|
839
|
-
onClick: () => void handleAction("checkout", checkout),
|
|
840
|
-
children: checkingOut ? "Opening checkout…" : "Proceed to Checkout"
|
|
841
|
-
}) : /* @__PURE__ */ jsx("p", {
|
|
842
|
-
className: "text-sm text-neutral-600",
|
|
843
|
-
children: "Checkout is not available yet."
|
|
844
|
-
})
|
|
845
|
-
]
|
|
854
|
+
children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40" }), /* @__PURE__ */ jsx("div", {
|
|
855
|
+
className: "pointer-events-none fixed inset-0 z-50 flex justify-end",
|
|
856
|
+
children: /* @__PURE__ */ jsx(Dialog.Content, {
|
|
857
|
+
"data-commerce-source": connection === "unbound" ? "sample" : void 0,
|
|
858
|
+
...described ? {} : { "aria-describedby": void 0 },
|
|
859
|
+
className: partClass("pointer-events-auto", className),
|
|
860
|
+
onCloseAutoFocus: (event) => {
|
|
861
|
+
event.preventDefault();
|
|
862
|
+
restoreFocus();
|
|
863
|
+
},
|
|
864
|
+
children: /* @__PURE__ */ jsx(CartDrawerContext.Provider, {
|
|
865
|
+
value: drawer,
|
|
866
|
+
children: /* @__PURE__ */ jsx(CartProvider, { children })
|
|
846
867
|
})
|
|
847
|
-
|
|
868
|
+
})
|
|
848
869
|
})] }) })
|
|
849
870
|
});
|
|
850
871
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
onError: () => setFailed(true)
|
|
862
|
-
}) : /* @__PURE__ */ jsx("div", {
|
|
863
|
-
className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
|
|
864
|
-
role: "img",
|
|
865
|
-
"aria-label": `${name}: image unavailable`,
|
|
866
|
-
children: "No image available"
|
|
872
|
+
/** "Your cart", or "Sample cart" before a store is connected, as an H2. In the drawer it names the dialog. */
|
|
873
|
+
function CartTitle({ className }) {
|
|
874
|
+
const { isSample } = useCartInternal("CartTitle");
|
|
875
|
+
const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
|
|
876
|
+
return /* @__PURE__ */ jsx("div", {
|
|
877
|
+
className: partClass(className),
|
|
878
|
+
children: /* @__PURE__ */ jsx(Heading, {
|
|
879
|
+
className: "text-2xl font-semibold",
|
|
880
|
+
children: isSample ? "Sample cart" : "Your cart"
|
|
881
|
+
})
|
|
867
882
|
});
|
|
868
883
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
tabIndex: -1,
|
|
886
|
-
"aria-hidden": "true",
|
|
887
|
-
className: "block aspect-square overflow-hidden",
|
|
888
|
-
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
889
|
-
url: getPrimaryImageUrl(product),
|
|
890
|
-
name,
|
|
891
|
-
className: "motion-safe:transition-transform motion-safe:duration-200 motion-safe:group-hover:scale-105"
|
|
892
|
-
})
|
|
893
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
894
|
-
className: "flex flex-1 flex-col p-6",
|
|
895
|
-
children: [
|
|
896
|
-
/* @__PURE__ */ jsx("h2", {
|
|
897
|
-
className: "break-words text-2xl font-semibold leading-tight",
|
|
898
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
899
|
-
className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
900
|
-
to: href,
|
|
901
|
-
children: name
|
|
902
|
-
})
|
|
903
|
-
}),
|
|
904
|
-
product.description && /* @__PURE__ */ jsx("p", {
|
|
905
|
-
className: "mt-3 line-clamp-3 text-sm leading-6",
|
|
906
|
-
children: product.description
|
|
907
|
-
}),
|
|
908
|
-
/* @__PURE__ */ jsx("div", {
|
|
909
|
-
className: "mt-auto pt-6",
|
|
910
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
911
|
-
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
912
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
913
|
-
className: "min-w-0 flex-1 text-lg font-semibold leading-7",
|
|
914
|
-
children: typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable"
|
|
915
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
916
|
-
className: "max-w-full",
|
|
917
|
-
children: !hasVariants && selectedSku ? /* @__PURE__ */ jsx(AddToCartButton, {
|
|
918
|
-
sku: selectedSku,
|
|
919
|
-
name
|
|
920
|
-
}) : /* @__PURE__ */ jsx(Link, {
|
|
921
|
-
className: buttonClass,
|
|
922
|
-
to: href,
|
|
923
|
-
children: hasVariants ? "Choose options" : "View product"
|
|
924
|
-
})
|
|
925
|
-
})]
|
|
926
|
-
})
|
|
927
|
-
})
|
|
928
|
-
]
|
|
929
|
-
})]
|
|
884
|
+
/** One line about the cart. In the drawer it describes the dialog. */
|
|
885
|
+
function CartDescription({ className }) {
|
|
886
|
+
const { isSample } = useCartInternal("CartDescription");
|
|
887
|
+
const drawer = useContext(CartDrawerContext);
|
|
888
|
+
const register = drawer?.register;
|
|
889
|
+
useEffect(() => register?.("description"), [register]);
|
|
890
|
+
const text = isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout.";
|
|
891
|
+
return /* @__PURE__ */ jsx("div", {
|
|
892
|
+
className: partClass(className),
|
|
893
|
+
children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
|
|
894
|
+
className: "text-sm text-neutral-600",
|
|
895
|
+
children: text
|
|
896
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
897
|
+
className: "text-sm text-neutral-600",
|
|
898
|
+
children: text
|
|
899
|
+
})
|
|
930
900
|
});
|
|
931
901
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const products = useQuery({
|
|
943
|
-
retry: false,
|
|
944
|
-
queryKey: [
|
|
945
|
-
"commerce",
|
|
946
|
-
config.cartScope,
|
|
947
|
-
"products",
|
|
948
|
-
after
|
|
949
|
-
],
|
|
950
|
-
queryFn: ({ signal }) => request(`/products?${urlParams}`, {
|
|
951
|
-
signal,
|
|
952
|
-
headers: { "X-Commerce-Scope": config.cartScope }
|
|
902
|
+
/** The drawer's close button. Renders nothing outside the drawer. */
|
|
903
|
+
function CartClose({ className }) {
|
|
904
|
+
useCartInternal("CartClose");
|
|
905
|
+
if (!useContext(CartDrawerContext)) return null;
|
|
906
|
+
return /* @__PURE__ */ jsx("div", {
|
|
907
|
+
className: partClass(className),
|
|
908
|
+
children: /* @__PURE__ */ jsx(Dialog.Close, {
|
|
909
|
+
className: "min-h-11 min-w-11 rounded-lg border border-neutral-400 bg-white px-3 text-neutral-900 focus-visible:outline focus-visible:outline-2",
|
|
910
|
+
"aria-label": "Close cart",
|
|
911
|
+
children: "✕"
|
|
953
912
|
})
|
|
954
913
|
});
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
className: "
|
|
971
|
-
children:
|
|
914
|
+
}
|
|
915
|
+
/** A storage warning, and a cart error with a refresh button. Renders nothing when there are neither. */
|
|
916
|
+
function CartNotices({ className }) {
|
|
917
|
+
const { storageWarning, error, action, locked, run, refresh } = useCartInternal("CartNotices");
|
|
918
|
+
if (!storageWarning && !error) return null;
|
|
919
|
+
return /* @__PURE__ */ jsx("div", {
|
|
920
|
+
className: partClass(className),
|
|
921
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
922
|
+
className: "flex flex-col gap-4",
|
|
923
|
+
children: [storageWarning && /* @__PURE__ */ jsx("p", {
|
|
924
|
+
role: "status",
|
|
925
|
+
className: "text-sm text-amber-800",
|
|
926
|
+
children: storageWarning
|
|
927
|
+
}), error && /* @__PURE__ */ jsxs("div", {
|
|
928
|
+
role: "alert",
|
|
929
|
+
className: "rounded-lg bg-red-50 p-4 text-red-800",
|
|
930
|
+
children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
|
|
931
|
+
type: "button",
|
|
932
|
+
className: "mt-2 min-h-11 bg-red-50 text-red-800 underline disabled:cursor-not-allowed",
|
|
933
|
+
disabled: action === "refresh",
|
|
934
|
+
"aria-disabled": locked,
|
|
935
|
+
"aria-busy": action === "refresh",
|
|
936
|
+
onClick: () => run("refresh", refresh),
|
|
937
|
+
children: "Refresh cart"
|
|
938
|
+
})]
|
|
972
939
|
})]
|
|
973
|
-
})
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
940
|
+
})
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const CartItemsContext = createContext(null);
|
|
944
|
+
const CartItemsRegistry = createContext(null);
|
|
945
|
+
const CartItemContext = createContext(null);
|
|
946
|
+
function useCartItemInternal(part) {
|
|
947
|
+
const item = useContext(CartItemContext);
|
|
948
|
+
if (!item) throw new Error(`${part} must be rendered inside <CartItemRoot>.`);
|
|
949
|
+
return item;
|
|
950
|
+
}
|
|
951
|
+
/** Read the line from inside `CartItemRoot`, for custom item parts. */
|
|
952
|
+
function useCartItem() {
|
|
953
|
+
return useCartItemInternal("useCartItem");
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* The cart lines as a list. `className` goes on the list element whose children are the items. While the saved cart
|
|
957
|
+
* loads, a status message renders just before it.
|
|
958
|
+
*/
|
|
959
|
+
function CartItems({ className, children }) {
|
|
960
|
+
const { items, hydrating } = useCartInternal("CartItems");
|
|
961
|
+
const template = hasChildren(children) ? children : packagedItem;
|
|
962
|
+
const registry = usePartRegistry();
|
|
963
|
+
const { has } = registry;
|
|
964
|
+
useEffect(() => {
|
|
965
|
+
if (process.env.NODE_ENV === "production" || items.length === 0 || has("item")) return;
|
|
966
|
+
console.warn("[gd-commerce-storefront] The <CartItems> template renders no <CartItemRoot>, so the cart list has no items. Wrap each line in <CartItemRoot>.");
|
|
967
|
+
}, [items.length, has]);
|
|
968
|
+
return /* @__PURE__ */ jsxs(CartItemsRegistry.Provider, {
|
|
969
|
+
value: registry.register,
|
|
970
|
+
children: [hydrating && /* @__PURE__ */ jsx("p", {
|
|
971
|
+
role: "status",
|
|
972
|
+
children: "Loading your cart…"
|
|
973
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
974
|
+
role: "list",
|
|
975
|
+
className: partClass(className),
|
|
976
|
+
children: items.map((item, index) => /* @__PURE__ */ jsx(CartItemsContext.Provider, {
|
|
977
|
+
value: item,
|
|
978
|
+
children: template
|
|
979
|
+
}, item.id || `line-${index}`))
|
|
980
|
+
})]
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
/** One cart line. Render it as the child of `CartItems`, which renders it once for every line. */
|
|
984
|
+
function CartItemRoot({ children, className }) {
|
|
985
|
+
const item = useContext(CartItemsContext);
|
|
986
|
+
const register = useContext(CartItemsRegistry);
|
|
987
|
+
useEffect(() => register?.("item"), [register]);
|
|
988
|
+
if (!item) throw new Error("CartItemRoot must be rendered inside <CartItems>.");
|
|
989
|
+
return /* @__PURE__ */ jsx(CartItemContext.Provider, {
|
|
990
|
+
value: item,
|
|
991
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
992
|
+
role: "listitem",
|
|
993
|
+
className: partClass(className),
|
|
994
|
+
"data-testid": "cart-item",
|
|
995
|
+
children: /* @__PURE__ */ jsx(CartItemsContext.Provider, {
|
|
996
|
+
value: null,
|
|
997
|
+
children
|
|
980
998
|
})
|
|
981
|
-
})
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
999
|
+
})
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
/** The line's product image. Renders nothing without one. */
|
|
1003
|
+
function CartItemImage({ className }) {
|
|
1004
|
+
const { imageUrl } = useCartItemInternal("CartItemImage");
|
|
1005
|
+
if (!imageUrl) return null;
|
|
1006
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1007
|
+
className: partClass(className),
|
|
1008
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
1009
|
+
src: imageUrl,
|
|
1010
|
+
alt: "",
|
|
1011
|
+
className: "h-20 w-20 rounded-lg bg-neutral-100 object-cover"
|
|
1012
|
+
})
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
/** The product name as an H3. */
|
|
1016
|
+
function CartItemName({ className }) {
|
|
1017
|
+
const { name } = useCartItemInternal("CartItemName");
|
|
1018
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1019
|
+
className: partClass(className),
|
|
1020
|
+
children: /* @__PURE__ */ jsx("h3", {
|
|
1021
|
+
className: "font-semibold",
|
|
1022
|
+
children: name
|
|
1023
|
+
})
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
/** The selected options, one per line, such as "Color: Blue". Renders nothing without options. */
|
|
1027
|
+
function CartItemOptions({ className }) {
|
|
1028
|
+
const { options } = useCartItemInternal("CartItemOptions");
|
|
1029
|
+
if (!options.length) return null;
|
|
1030
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1031
|
+
className: partClass(className),
|
|
1032
|
+
children: options.map((option) => /* @__PURE__ */ jsxs("p", {
|
|
1033
|
+
className: "text-sm text-neutral-600",
|
|
985
1034
|
children: [
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
]
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1035
|
+
option.attribute,
|
|
1036
|
+
": ",
|
|
1037
|
+
option.values.join(", ")
|
|
1038
|
+
]
|
|
1039
|
+
}, `${option.attribute}:${option.values.join(",")}`))
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
/** The line total. */
|
|
1043
|
+
function CartItemPrice({ className }) {
|
|
1044
|
+
const { subtotal, currency } = useCartItemInternal("CartItemPrice");
|
|
1045
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1046
|
+
className: partClass(className),
|
|
1047
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1048
|
+
className: "text-sm",
|
|
1049
|
+
children: subtotal === null ? "Price unavailable" : money(subtotal, currency)
|
|
1050
|
+
})
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
const stepperButtonClass = "min-h-11 min-w-11 rounded-lg bg-white text-neutral-900 focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:text-neutral-600";
|
|
1054
|
+
/** Decrease and increase buttons around the line's quantity. */
|
|
1055
|
+
function CartItemQuantity({ className }) {
|
|
1056
|
+
const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
|
|
1057
|
+
const { action, locked, run, changeQuantity } = useCartInternal("CartItemQuantity");
|
|
1058
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1059
|
+
className: partClass(className),
|
|
1060
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1061
|
+
className: "flex items-center rounded-lg border border-neutral-300",
|
|
1062
|
+
children: [
|
|
1063
|
+
/* @__PURE__ */ jsx("button", {
|
|
1064
|
+
type: "button",
|
|
1065
|
+
className: stepperButtonClass,
|
|
1066
|
+
"aria-label": `Decrease quantity of ${name}`,
|
|
1067
|
+
disabled: action === `decrease:${id}` || !id || quantity <= 1,
|
|
1068
|
+
"aria-disabled": locked || !id || quantity <= 1,
|
|
1069
|
+
"aria-busy": action === `decrease:${id}`,
|
|
1070
|
+
onClick: () => run(`decrease:${id}`, () => changeQuantity(id, quantity - 1)),
|
|
1071
|
+
children: "−"
|
|
1072
|
+
}),
|
|
1073
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1074
|
+
className: "min-w-8 text-center",
|
|
1075
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1076
|
+
className: "sr-only",
|
|
1077
|
+
children: "Quantity: "
|
|
1078
|
+
}), quantity]
|
|
1079
|
+
}),
|
|
1080
|
+
/* @__PURE__ */ jsx("button", {
|
|
1081
|
+
type: "button",
|
|
1082
|
+
className: stepperButtonClass,
|
|
1083
|
+
"aria-label": `Increase quantity of ${name}`,
|
|
1084
|
+
disabled: action === `increase:${id}` || !id,
|
|
1085
|
+
"aria-disabled": locked || !id,
|
|
1086
|
+
"aria-busy": action === `increase:${id}`,
|
|
1087
|
+
onClick: () => run(`increase:${id}`, () => changeQuantity(id, quantity + 1)),
|
|
1088
|
+
children: "+"
|
|
1089
|
+
})
|
|
1090
|
+
]
|
|
1091
|
+
})
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
/** Removes the line. */
|
|
1095
|
+
function CartItemRemove({ className }) {
|
|
1096
|
+
const { id, name } = useCartItemInternal("CartItemRemove");
|
|
1097
|
+
const { action, locked, run, removeItem } = useCartInternal("CartItemRemove");
|
|
1098
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1099
|
+
className: partClass(className),
|
|
1100
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
1101
|
+
type: "button",
|
|
1102
|
+
className: "min-h-11 bg-white px-2 text-sm text-neutral-900 underline focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:text-neutral-600",
|
|
1103
|
+
"aria-label": `Remove ${name}`,
|
|
1104
|
+
disabled: action === `remove:${id}` || !id,
|
|
1105
|
+
"aria-disabled": locked || !id,
|
|
1106
|
+
"aria-busy": action === `remove:${id}`,
|
|
1107
|
+
onClick: () => run(`remove:${id}`, () => removeItem(id)),
|
|
1108
|
+
children: "Remove"
|
|
1109
|
+
})
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
const packagedItem = /* @__PURE__ */ jsxs(CartItemRoot, {
|
|
1113
|
+
className: "py-5 first:pt-0",
|
|
1114
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1115
|
+
className: "flex gap-4",
|
|
1116
|
+
children: [/* @__PURE__ */ jsx(CartItemImage, {}), /* @__PURE__ */ jsxs("div", {
|
|
1117
|
+
className: "min-w-0 flex-1",
|
|
1118
|
+
children: [
|
|
1119
|
+
/* @__PURE__ */ jsx(CartItemName, {}),
|
|
1120
|
+
/* @__PURE__ */ jsx(CartItemOptions, {}),
|
|
1121
|
+
/* @__PURE__ */ jsx(CartItemPrice, { className: "mt-1" })
|
|
1122
|
+
]
|
|
1123
|
+
})]
|
|
1124
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1125
|
+
className: "mt-4 flex items-center justify-between gap-2",
|
|
1126
|
+
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1127
|
+
})]
|
|
1128
|
+
});
|
|
1129
|
+
/** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
|
|
1130
|
+
function CartEmpty({ className, children }) {
|
|
1131
|
+
const { items, hydrating, isSample, catalogPath, close } = useCartInternal("CartEmpty");
|
|
1132
|
+
if (hydrating || items.length) return null;
|
|
1133
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1134
|
+
className: partClass(className),
|
|
1135
|
+
children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1136
|
+
/* @__PURE__ */ jsx("p", {
|
|
1137
|
+
className: "text-xl font-medium",
|
|
1138
|
+
children: "Your cart is empty"
|
|
1139
|
+
}),
|
|
1140
|
+
/* @__PURE__ */ jsx("p", {
|
|
1141
|
+
className: "mt-2 text-neutral-600",
|
|
1142
|
+
children: isSample ? "Add a sample product to try the cart." : "Find something you love in the shop."
|
|
1143
|
+
}),
|
|
1144
|
+
/* @__PURE__ */ jsx(Link, {
|
|
1145
|
+
className: `${buttonClass} mt-6`,
|
|
1146
|
+
to: catalogPath,
|
|
1147
|
+
onClick: close,
|
|
1148
|
+
children: "Browse products"
|
|
1149
|
+
})
|
|
1150
|
+
] })
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
/** A container for the subtotal and checkout. Renders only when the cart has items. */
|
|
1154
|
+
function CartSummary({ className, children }) {
|
|
1155
|
+
const { items } = useCartInternal("CartSummary");
|
|
1156
|
+
if (!items.length) return null;
|
|
1157
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1158
|
+
className: partClass(className),
|
|
1159
|
+
children
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
/** The subtotal, labeled "Sample subtotal" for samples. Renders nothing until the server prices the cart. */
|
|
1163
|
+
function CartSubtotal({ className }) {
|
|
1164
|
+
const { items, subtotal, currency, isSample } = useCartInternal("CartSubtotal");
|
|
1165
|
+
if (!items.length || subtotal === null) return null;
|
|
1166
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1167
|
+
className: partClass(className),
|
|
1168
|
+
children: /* @__PURE__ */ jsx("dl", {
|
|
1169
|
+
className: "space-y-2 text-sm",
|
|
1170
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1171
|
+
className: "flex justify-between",
|
|
1172
|
+
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money(subtotal, currency) })]
|
|
1173
|
+
})
|
|
1174
|
+
})
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* "Shipping, taxes, and discounts are calculated at checkout." Has the stable
|
|
1179
|
+
* `commerce-cart-checkout-adjustments-note` class. Renders nothing for samples or an empty cart.
|
|
1180
|
+
*/
|
|
1181
|
+
function CartCheckoutNote({ className }) {
|
|
1182
|
+
const { items, isSample } = useCartInternal("CartCheckoutNote");
|
|
1183
|
+
if (isSample || !items.length) return null;
|
|
1184
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1185
|
+
className: partClass("commerce-cart-checkout-adjustments-note", className),
|
|
1186
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1187
|
+
className: "text-sm text-neutral-600",
|
|
1188
|
+
children: "Shipping, taxes, and discounts are calculated at checkout."
|
|
1189
|
+
})
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* The checkout button when `checkoutSuccessPath` is set; otherwise a message that checkout is unavailable.
|
|
1194
|
+
* Samples never check out. Renders nothing for an empty cart.
|
|
1195
|
+
*/
|
|
1196
|
+
function CartCheckout({ className }) {
|
|
1197
|
+
const { items, isSample, checkoutEnabled, checkoutReady, hydrating, action, locked, run, checkout } = useCartInternal("CartCheckout");
|
|
1198
|
+
if (!items.length) return null;
|
|
1199
|
+
let content;
|
|
1200
|
+
if (isSample) content = /* @__PURE__ */ jsx("p", {
|
|
1201
|
+
className: "text-sm text-neutral-600",
|
|
1202
|
+
children: "Example total in USD. Checkout is not set up yet."
|
|
1203
|
+
});
|
|
1204
|
+
else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
|
|
1205
|
+
className: "text-sm text-neutral-600",
|
|
1206
|
+
children: "Checkout is not available yet."
|
|
1207
|
+
});
|
|
1208
|
+
else content = /* @__PURE__ */ jsx("button", {
|
|
1209
|
+
type: "button",
|
|
1210
|
+
className: `${buttonClass} w-full`,
|
|
1211
|
+
disabled: action === "checkout" || hydrating || !checkoutReady,
|
|
1212
|
+
"aria-disabled": locked || !checkoutReady,
|
|
1213
|
+
"aria-busy": action === "checkout",
|
|
1214
|
+
onClick: () => run("checkout", checkout),
|
|
1215
|
+
children: action === "checkout" ? "Opening checkout…" : "Proceed to Checkout"
|
|
1216
|
+
});
|
|
1217
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1218
|
+
className: partClass(className),
|
|
1219
|
+
children: content
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
/** The packaged cart drawer. Compose `CartDrawerRoot` and the cart parts for a custom drawer. */
|
|
1223
|
+
function CartDrawer() {
|
|
1224
|
+
return /* @__PURE__ */ jsxs(CartDrawerRoot, {
|
|
1225
|
+
className: "flex w-full max-w-lg flex-col bg-white text-neutral-900 shadow-xl",
|
|
1226
|
+
children: [
|
|
1227
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1228
|
+
className: "flex items-start justify-between border-b border-neutral-200 p-6",
|
|
1229
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(CartTitle, {}), /* @__PURE__ */ jsx(CartDescription, { className: "mt-1" })] }), /* @__PURE__ */ jsx(CartClose, {})]
|
|
1230
|
+
}),
|
|
1231
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1232
|
+
className: "flex-1 overflow-y-auto p-6",
|
|
1233
|
+
children: [
|
|
1234
|
+
/* @__PURE__ */ jsx(CartNotices, { className: "mb-4" }),
|
|
1235
|
+
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-neutral-200" }),
|
|
1236
|
+
/* @__PURE__ */ jsx(CartEmpty, { className: "py-16 text-center" })
|
|
1237
|
+
]
|
|
1238
|
+
}),
|
|
1239
|
+
/* @__PURE__ */ jsxs(CartSummary, {
|
|
1240
|
+
className: "border-t border-neutral-200 p-6",
|
|
1241
|
+
children: [
|
|
1242
|
+
/* @__PURE__ */ jsx(CartSubtotal, {}),
|
|
1243
|
+
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-3" }),
|
|
1244
|
+
/* @__PURE__ */ jsx(CartCheckout, {})
|
|
1245
|
+
]
|
|
1246
|
+
})
|
|
1247
|
+
]
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/product-card.tsx
|
|
1253
|
+
function ProductImage({ url, name, className = "" }) {
|
|
1254
|
+
const [failed, setFailed] = useState(false);
|
|
1255
|
+
return url && !failed ? /* @__PURE__ */ jsx("img", {
|
|
1256
|
+
src: url,
|
|
1257
|
+
alt: name,
|
|
1258
|
+
loading: "lazy",
|
|
1259
|
+
className: `h-full w-full object-cover ${className}`,
|
|
1260
|
+
onError: () => setFailed(true)
|
|
1261
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1262
|
+
className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
|
|
1263
|
+
role: "img",
|
|
1264
|
+
"aria-label": `${name}: image unavailable`,
|
|
1265
|
+
children: "No image available"
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
/** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
|
|
1269
|
+
const CatalogItemContext = createContext(null);
|
|
1270
|
+
const titleLink = "title-link";
|
|
1271
|
+
function cardValue(item, productPath, links) {
|
|
1272
|
+
if (item.source === "sample") return {
|
|
1273
|
+
...links,
|
|
1274
|
+
source: "sample",
|
|
1275
|
+
sample: item.sample,
|
|
1276
|
+
product: null,
|
|
1277
|
+
name: item.sample.name,
|
|
1278
|
+
href: null,
|
|
1279
|
+
isSample: true
|
|
1280
|
+
};
|
|
1281
|
+
return {
|
|
1282
|
+
...links,
|
|
1283
|
+
source: "live",
|
|
1284
|
+
product: item.product,
|
|
1285
|
+
name: getProductName(item.product),
|
|
1286
|
+
href: `${productPath}/${encodeURIComponent(item.product.id ?? "")}`,
|
|
1287
|
+
isSample: false
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
const ProductCardContext = createContext(null);
|
|
1291
|
+
function useProductCardInternal(part) {
|
|
1292
|
+
const context = useContext(ProductCardContext);
|
|
1293
|
+
if (!context) throw new Error(`${part} must be rendered inside <ProductCardRoot>.`);
|
|
1294
|
+
return context;
|
|
1295
|
+
}
|
|
1296
|
+
/** Read the card's product, name, and link from inside `ProductCardRoot`. */
|
|
1297
|
+
function useProductCard() {
|
|
1298
|
+
const { product, name, href, isSample } = useProductCardInternal("useProductCard");
|
|
1299
|
+
return useMemo(() => ({
|
|
1300
|
+
product,
|
|
1301
|
+
name,
|
|
1302
|
+
href,
|
|
1303
|
+
isSample
|
|
1304
|
+
}), [
|
|
1305
|
+
product,
|
|
1306
|
+
name,
|
|
1307
|
+
href,
|
|
1308
|
+
isSample
|
|
1309
|
+
]);
|
|
1310
|
+
}
|
|
1311
|
+
/** Provides one product to card parts. Inside `CatalogProducts` it renders the current catalog entry. */
|
|
1312
|
+
function ProductCardRoot({ children, product, className }) {
|
|
1313
|
+
const catalogItem = useContext(CatalogItemContext);
|
|
1314
|
+
const { config } = useCommerce();
|
|
1315
|
+
const [imageLinked, setImageLinked] = useState(false);
|
|
1316
|
+
const { register, has } = usePartRegistry(useCallback((registered) => setImageLinked(!registered(titleLink)), []));
|
|
1317
|
+
useEffect(() => {
|
|
1318
|
+
setImageLinked(!has(titleLink));
|
|
1319
|
+
}, [has]);
|
|
1320
|
+
const value = useMemo(() => {
|
|
1321
|
+
const item = catalogItem ?? (product ? {
|
|
1322
|
+
source: "live",
|
|
1323
|
+
product
|
|
1324
|
+
} : null);
|
|
1325
|
+
return item && cardValue(item, config.productPath, {
|
|
1326
|
+
imageLinked,
|
|
1327
|
+
register
|
|
1328
|
+
});
|
|
1329
|
+
}, [
|
|
1330
|
+
catalogItem,
|
|
1331
|
+
product,
|
|
1332
|
+
config.productPath,
|
|
1333
|
+
imageLinked,
|
|
1334
|
+
register
|
|
1335
|
+
]);
|
|
1336
|
+
if (catalogItem && product) throw new Error("Inside <CatalogProducts>, ProductCardRoot shows each catalog product. Remove its product prop.");
|
|
1337
|
+
if (!value) throw new Error("ProductCardRoot needs a product prop outside <CatalogProducts>.");
|
|
1338
|
+
const card = /* @__PURE__ */ jsx(ProductCardContext.Provider, {
|
|
1339
|
+
value,
|
|
1340
|
+
children: /* @__PURE__ */ jsx("article", {
|
|
1341
|
+
className: partClass("group", className),
|
|
1342
|
+
children: /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1343
|
+
value: null,
|
|
1344
|
+
children
|
|
1345
|
+
})
|
|
1346
|
+
})
|
|
1347
|
+
});
|
|
1348
|
+
return catalogItem ? card : /* @__PURE__ */ jsx(StorefrontSurface, { children: card });
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* The product's first image, linked to its page. Samples show a placeholder. With a `ProductCardTitle` in the card,
|
|
1352
|
+
* the title is the one link keyboard and screen reader users get; without one, the image link is.
|
|
1353
|
+
*/
|
|
1354
|
+
function ProductCardImage({ className }) {
|
|
1355
|
+
const card = useProductCardInternal("ProductCardImage");
|
|
1356
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1357
|
+
className: partClass(className),
|
|
1358
|
+
children: card.source === "live" ? /* @__PURE__ */ jsx(Link, {
|
|
1359
|
+
to: card.href,
|
|
1360
|
+
tabIndex: card.imageLinked ? void 0 : -1,
|
|
1361
|
+
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
1362
|
+
className: "block aspect-square overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-4",
|
|
1363
|
+
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1364
|
+
url: getPrimaryImageUrl(card.product),
|
|
1365
|
+
name: card.name,
|
|
1366
|
+
className: "motion-safe:transition-transform motion-safe:duration-200 motion-safe:group-hover:scale-105"
|
|
1367
|
+
})
|
|
1368
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1369
|
+
"aria-hidden": "true",
|
|
1370
|
+
className: "flex aspect-square items-center justify-center bg-neutral-100",
|
|
1371
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-20 w-20 rounded-lg border border-neutral-300" })
|
|
1372
|
+
})
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
/** The product name as an H2, linked to its page. */
|
|
1376
|
+
function ProductCardTitle({ className }) {
|
|
1377
|
+
const { name, href, register } = useProductCardInternal("ProductCardTitle");
|
|
1378
|
+
useEffect(() => href ? register(titleLink) : void 0, [href, register]);
|
|
1379
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1380
|
+
className: partClass(className),
|
|
1381
|
+
children: /* @__PURE__ */ jsx("h2", {
|
|
1382
|
+
className: "break-words text-2xl font-semibold leading-tight",
|
|
1383
|
+
children: href ? /* @__PURE__ */ jsx(Link, {
|
|
1384
|
+
className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
1385
|
+
to: href,
|
|
1386
|
+
children: name
|
|
1387
|
+
}) : name
|
|
1388
|
+
})
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
/** Up to three lines of the product description. Renders nothing without one. */
|
|
1392
|
+
function ProductCardDescription({ className }) {
|
|
1393
|
+
const { product } = useProductCardInternal("ProductCardDescription");
|
|
1394
|
+
if (!product?.description) return null;
|
|
1395
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1396
|
+
className: partClass(className),
|
|
1397
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1398
|
+
className: "line-clamp-3 text-sm leading-6",
|
|
1399
|
+
children: product.description
|
|
1400
|
+
})
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
/** The product's price, or its price range when variants differ. */
|
|
1404
|
+
function ProductCardPrice({ className }) {
|
|
1405
|
+
const card = useProductCardInternal("ProductCardPrice");
|
|
1406
|
+
const { config } = useCommerce();
|
|
1407
|
+
let text;
|
|
1408
|
+
if (card.source === "sample") text = money(card.sample.price, "USD");
|
|
1409
|
+
else {
|
|
1410
|
+
const min = card.product.priceRange?.min;
|
|
1411
|
+
const max = card.product.priceRange?.max;
|
|
1412
|
+
text = typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable";
|
|
1413
|
+
}
|
|
1414
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1415
|
+
className: partClass(className),
|
|
1416
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1417
|
+
className: "text-lg font-semibold leading-7",
|
|
1418
|
+
children: text
|
|
1419
|
+
})
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
|
|
1424
|
+
* Samples add to the sample cart.
|
|
1425
|
+
*/
|
|
1426
|
+
function ProductCardAction({ className }) {
|
|
1427
|
+
const card = useProductCardInternal("ProductCardAction");
|
|
1428
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1429
|
+
className: partClass(className),
|
|
1430
|
+
children: card.source === "live" ? /* @__PURE__ */ jsx(LiveAction, {
|
|
1431
|
+
product: card.product,
|
|
1432
|
+
name: card.name,
|
|
1433
|
+
href: card.href
|
|
1434
|
+
}) : /* @__PURE__ */ jsx(SampleAction, { sample: card.sample })
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
function LiveAction({ product, name, href }) {
|
|
1438
|
+
const selectedSku = getSingleMatchedSku(product);
|
|
1439
|
+
const hasVariants = getProductAttributes(product).length > 0;
|
|
1440
|
+
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCartButton, {
|
|
1441
|
+
sku: selectedSku,
|
|
1442
|
+
name
|
|
1443
|
+
});
|
|
1444
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
1445
|
+
className: buttonClass,
|
|
1446
|
+
to: href,
|
|
1447
|
+
children: hasVariants ? "Choose options" : "View product"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
function SampleAction({ sample }) {
|
|
1451
|
+
const { add } = useSampleCart();
|
|
1452
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1453
|
+
type: "button",
|
|
1454
|
+
className: buttonClass,
|
|
1455
|
+
"aria-label": `Add ${sample.name} to sample cart`,
|
|
1456
|
+
onClick: () => add(sample.key),
|
|
1457
|
+
children: "Add to cart"
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
const packagedCardClass = "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200";
|
|
1461
|
+
/** The packaged card's parts, inside a root that has `packagedCardClass`. */
|
|
1462
|
+
function PackagedCardParts() {
|
|
1463
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsxs("div", {
|
|
1464
|
+
className: "flex flex-1 flex-col p-6",
|
|
1465
|
+
children: [
|
|
1466
|
+
/* @__PURE__ */ jsx(ProductCardTitle, {}),
|
|
1467
|
+
/* @__PURE__ */ jsx(ProductCardDescription, { className: "mt-3" }),
|
|
1468
|
+
/* @__PURE__ */ jsx("div", {
|
|
1469
|
+
className: "mt-auto pt-6",
|
|
1470
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1471
|
+
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
1472
|
+
children: [/* @__PURE__ */ jsx(ProductCardPrice, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ProductCardAction, { className: "max-w-full" })]
|
|
1473
|
+
})
|
|
1474
|
+
})
|
|
1475
|
+
]
|
|
1476
|
+
})] });
|
|
1477
|
+
}
|
|
1478
|
+
/** The packaged card for one `SKUGroup`. Compose `ProductCardRoot` and the card parts for a custom card. */
|
|
1479
|
+
function ProductCard({ product }) {
|
|
1480
|
+
return /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
1481
|
+
product,
|
|
1482
|
+
className: packagedCardClass,
|
|
1483
|
+
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/catalog.tsx
|
|
1489
|
+
const CatalogContext = createContext(null);
|
|
1490
|
+
const sampleCatalog = { source: "sample" };
|
|
1491
|
+
function useCatalogInternal(part) {
|
|
1492
|
+
const context = useContext(CatalogContext);
|
|
1493
|
+
if (!context) throw new Error(`${part} must be rendered inside <CatalogRoot>.`);
|
|
1494
|
+
return context;
|
|
1495
|
+
}
|
|
1496
|
+
/** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
|
|
1497
|
+
function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false }) {
|
|
1498
|
+
const { connection } = useCommerce();
|
|
1499
|
+
let content;
|
|
1500
|
+
if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
|
|
1501
|
+
className,
|
|
1502
|
+
children
|
|
1503
|
+
});
|
|
1504
|
+
else if (connection === "unbound" && sampleProducts$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1505
|
+
value: sampleCatalog,
|
|
1506
|
+
children: /* @__PURE__ */ jsx("section", {
|
|
1507
|
+
className: partClass(className),
|
|
1508
|
+
"data-commerce-source": "sample",
|
|
1509
|
+
children
|
|
1510
|
+
})
|
|
1511
|
+
});
|
|
1512
|
+
else content = /* @__PURE__ */ jsx("div", {
|
|
1513
|
+
className: partClass(className),
|
|
1514
|
+
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1515
|
+
});
|
|
1516
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: content });
|
|
1517
|
+
}
|
|
1518
|
+
function CatalogLoader({ children, className }) {
|
|
1519
|
+
const { config } = useCommerce();
|
|
1520
|
+
const [params, setParams] = useSearchParams();
|
|
1521
|
+
const after = params.get("after") ?? "";
|
|
1522
|
+
const urlParams = new URLSearchParams({ first: "6" });
|
|
1523
|
+
if (after) urlParams.set("after", after);
|
|
1524
|
+
const products = useQuery({
|
|
1525
|
+
retry: false,
|
|
1526
|
+
queryKey: [
|
|
1527
|
+
"commerce",
|
|
1528
|
+
config.cartScope,
|
|
1529
|
+
"products",
|
|
1530
|
+
after
|
|
1531
|
+
],
|
|
1532
|
+
queryFn: ({ signal }) => request(`/products?${urlParams}`, {
|
|
1533
|
+
signal,
|
|
1534
|
+
headers: { "X-Commerce-Scope": config.cartScope }
|
|
1535
|
+
})
|
|
1536
|
+
});
|
|
1537
|
+
const goToPage = useCallback((cursor) => {
|
|
1538
|
+
const next = new URLSearchParams(params);
|
|
1539
|
+
if (cursor) next.set("after", cursor);
|
|
1540
|
+
else next.delete("after");
|
|
1541
|
+
setParams(next);
|
|
1542
|
+
}, [params, setParams]);
|
|
1543
|
+
const nodes = useMemo(() => products.data?.skuGroups?.edges?.flatMap((edge) => edge?.node?.id ? [edge.node] : []) ?? [], [products.data]);
|
|
1544
|
+
const value = useMemo(() => ({
|
|
1545
|
+
source: "live",
|
|
1546
|
+
products,
|
|
1547
|
+
nodes,
|
|
1548
|
+
after,
|
|
1549
|
+
goToPage
|
|
1550
|
+
}), [
|
|
1551
|
+
products,
|
|
1552
|
+
nodes,
|
|
1553
|
+
after,
|
|
1554
|
+
goToPage
|
|
1555
|
+
]);
|
|
1556
|
+
return /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1557
|
+
value,
|
|
1558
|
+
children: /* @__PURE__ */ jsx("section", {
|
|
1559
|
+
className: partClass(className),
|
|
1560
|
+
children
|
|
1561
|
+
})
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
/** The catalog title as the page's H1, with a description. */
|
|
1565
|
+
function CatalogHeader({ title = "Shop all products", description = "Explore the collection and find your favorites.", className }) {
|
|
1566
|
+
useCatalogInternal("CatalogHeader");
|
|
1567
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
1568
|
+
className: partClass(className),
|
|
1569
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
1570
|
+
className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
|
|
1571
|
+
children: title
|
|
1572
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1573
|
+
className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
|
|
1574
|
+
children: description
|
|
1575
|
+
})]
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
/** Announces loading, failure, or how many products the page shows. */
|
|
1579
|
+
function CatalogSummary({ className }) {
|
|
1580
|
+
const catalog = useCatalogInternal("CatalogSummary");
|
|
1581
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1582
|
+
className: partClass(className),
|
|
1583
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1584
|
+
className: "text-sm",
|
|
1585
|
+
role: "status",
|
|
1586
|
+
children: summary(catalog)
|
|
1587
|
+
})
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
function summary(catalog) {
|
|
1591
|
+
if (catalog.source === "sample") return "Sample products · Example prices in USD";
|
|
1592
|
+
if (catalog.products.isPending) return "Loading products…";
|
|
1593
|
+
if (catalog.products.isError) return "Products unavailable";
|
|
1594
|
+
return `Showing ${catalog.nodes.length} product${catalog.nodes.length === 1 ? "" : "s"}`;
|
|
1595
|
+
}
|
|
1596
|
+
const sampleItems = sampleProducts.map((sample) => ({
|
|
1597
|
+
key: sample.key,
|
|
1598
|
+
item: {
|
|
1599
|
+
source: "sample",
|
|
1600
|
+
sample
|
|
1601
|
+
}
|
|
1602
|
+
}));
|
|
1603
|
+
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
1604
|
+
className: packagedCardClass,
|
|
1605
|
+
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1606
|
+
});
|
|
1607
|
+
/**
|
|
1608
|
+
* The product cards. `className` goes on the element that contains the cards, such as a grid. While loading, that
|
|
1609
|
+
* element holds placeholder cards. The error with its retry and the empty message render just before it.
|
|
1610
|
+
*/
|
|
1611
|
+
function CatalogProducts({ className, children }) {
|
|
1612
|
+
const catalog = useCatalogInternal("CatalogProducts");
|
|
1613
|
+
const card = hasChildren(children) ? children : packagedCard;
|
|
1614
|
+
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
1615
|
+
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
1616
|
+
key: product.id ?? "",
|
|
1617
|
+
item: {
|
|
1618
|
+
source: "live",
|
|
1619
|
+
product
|
|
1620
|
+
}
|
|
1621
|
+
})) : sampleItems, [nodes]);
|
|
1622
|
+
const products = catalog.source === "live" ? catalog.products : null;
|
|
1623
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1624
|
+
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
1002
1625
|
role: "alert",
|
|
1003
1626
|
className: "rounded-lg bg-red-50 p-5 text-red-800",
|
|
1004
1627
|
children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
|
|
@@ -1008,17 +1631,45 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
|
|
|
1008
1631
|
children: "Retry products"
|
|
1009
1632
|
})]
|
|
1010
1633
|
}),
|
|
1011
|
-
products
|
|
1634
|
+
products?.isSuccess && !items.length && /* @__PURE__ */ jsx("p", {
|
|
1012
1635
|
className: "py-16 text-center",
|
|
1013
1636
|
children: "No products available."
|
|
1014
1637
|
}),
|
|
1015
1638
|
/* @__PURE__ */ jsx("div", {
|
|
1016
|
-
className:
|
|
1017
|
-
children:
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1639
|
+
className: partClass(className),
|
|
1640
|
+
children: products?.isPending ? [
|
|
1641
|
+
0,
|
|
1642
|
+
1,
|
|
1643
|
+
2
|
|
1644
|
+
].map((placeholder) => /* @__PURE__ */ jsxs("div", {
|
|
1645
|
+
"aria-hidden": "true",
|
|
1646
|
+
className: "overflow-hidden rounded-lg border border-neutral-200 motion-safe:animate-pulse",
|
|
1647
|
+
children: [/* @__PURE__ */ jsx("div", { className: "aspect-square bg-neutral-100" }), /* @__PURE__ */ jsxs("div", {
|
|
1648
|
+
className: "space-y-4 p-6",
|
|
1649
|
+
children: [
|
|
1650
|
+
/* @__PURE__ */ jsx("div", { className: "h-6 w-2/3 rounded bg-neutral-100" }),
|
|
1651
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 rounded bg-neutral-100" }),
|
|
1652
|
+
/* @__PURE__ */ jsx("div", { className: "h-11 rounded bg-neutral-100" })
|
|
1653
|
+
]
|
|
1654
|
+
})]
|
|
1655
|
+
}, placeholder)) : items.map(({ key, item }) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1656
|
+
value: item,
|
|
1657
|
+
children: card
|
|
1658
|
+
}, key))
|
|
1659
|
+
})
|
|
1660
|
+
] });
|
|
1661
|
+
}
|
|
1662
|
+
/** First-page and next-page buttons. Renders nothing for samples, which have one page. */
|
|
1663
|
+
function CatalogPagination({ className }) {
|
|
1664
|
+
const catalog = useCatalogInternal("CatalogPagination");
|
|
1665
|
+
if (catalog.source === "sample") return null;
|
|
1666
|
+
const { after, goToPage } = catalog;
|
|
1667
|
+
const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
|
|
1668
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
1669
|
+
"aria-label": "Product pages",
|
|
1670
|
+
className: partClass(className),
|
|
1671
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1672
|
+
className: "flex justify-between",
|
|
1022
1673
|
children: [after ? /* @__PURE__ */ jsx("button", {
|
|
1023
1674
|
type: "button",
|
|
1024
1675
|
className: buttonClass,
|
|
@@ -1031,60 +1682,21 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
|
|
|
1031
1682
|
children: "Next page"
|
|
1032
1683
|
})]
|
|
1033
1684
|
})
|
|
1034
|
-
|
|
1685
|
+
});
|
|
1035
1686
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return /* @__PURE__ */ jsxs(
|
|
1039
|
-
|
|
1687
|
+
/** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
|
|
1688
|
+
function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1 }) {
|
|
1689
|
+
return /* @__PURE__ */ jsxs(CatalogRoot, {
|
|
1690
|
+
sampleProducts: sampleProducts$1,
|
|
1040
1691
|
children: [
|
|
1041
|
-
showHeader && /* @__PURE__ */
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
children: title
|
|
1046
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
1047
|
-
className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
|
|
1048
|
-
children: description
|
|
1049
|
-
})]
|
|
1050
|
-
}),
|
|
1051
|
-
/* @__PURE__ */ jsx("p", {
|
|
1052
|
-
className: "border-b border-neutral-200 py-6 text-sm",
|
|
1053
|
-
role: "status",
|
|
1054
|
-
children: "Sample products · Example prices in USD"
|
|
1692
|
+
showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
|
|
1693
|
+
title,
|
|
1694
|
+
description,
|
|
1695
|
+
className: "border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8"
|
|
1055
1696
|
}),
|
|
1056
|
-
/* @__PURE__ */ jsx("
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
className: "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200",
|
|
1060
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1061
|
-
"aria-hidden": "true",
|
|
1062
|
-
className: "flex aspect-square items-center justify-center bg-neutral-100",
|
|
1063
|
-
children: /* @__PURE__ */ jsx("div", { className: "h-20 w-20 rounded-lg border border-neutral-300" })
|
|
1064
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1065
|
-
className: "flex flex-1 flex-col p-6",
|
|
1066
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
1067
|
-
className: "break-words text-2xl font-semibold leading-tight",
|
|
1068
|
-
children: product.name
|
|
1069
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1070
|
-
className: "mt-auto pt-6",
|
|
1071
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1072
|
-
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
1073
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
1074
|
-
className: "text-lg font-semibold leading-7",
|
|
1075
|
-
children: money(product.price, "USD")
|
|
1076
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1077
|
-
type: "button",
|
|
1078
|
-
className: buttonClass,
|
|
1079
|
-
"aria-label": `Add ${product.name} to sample cart`,
|
|
1080
|
-
onClick: () => add(product.key),
|
|
1081
|
-
children: "Add to cart"
|
|
1082
|
-
})]
|
|
1083
|
-
})
|
|
1084
|
-
})]
|
|
1085
|
-
})]
|
|
1086
|
-
}, product.key))
|
|
1087
|
-
})
|
|
1697
|
+
/* @__PURE__ */ jsx(CatalogSummary, { className: "border-b border-neutral-200 py-6 sm:py-8" }),
|
|
1698
|
+
/* @__PURE__ */ jsx(CatalogProducts, { className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10" }),
|
|
1699
|
+
/* @__PURE__ */ jsx(CatalogPagination, { className: "border-t border-neutral-200 pt-6" })
|
|
1088
1700
|
]
|
|
1089
1701
|
});
|
|
1090
1702
|
}
|
|
@@ -1101,9 +1713,6 @@ function CommerceStorefront({ children,...props }) {
|
|
|
1101
1713
|
|
|
1102
1714
|
//#endregion
|
|
1103
1715
|
//#region src/product-details-context.tsx
|
|
1104
|
-
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
1105
|
-
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
1106
|
-
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
1107
1716
|
const ProductDetailsContext = createContext(null);
|
|
1108
1717
|
function useProductDetailsInternal(part) {
|
|
1109
1718
|
const context = useContext(ProductDetailsContext);
|
|
@@ -1162,7 +1771,10 @@ function ProductDetailsRoot({ children, className }) {
|
|
|
1162
1771
|
productId,
|
|
1163
1772
|
className,
|
|
1164
1773
|
children
|
|
1165
|
-
}, productId) : /* @__PURE__ */ jsx(
|
|
1774
|
+
}, productId) : /* @__PURE__ */ jsx("div", {
|
|
1775
|
+
className: partClass(className),
|
|
1776
|
+
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1777
|
+
}) });
|
|
1166
1778
|
}
|
|
1167
1779
|
function ProductDetailsLoader({ productId, className, children }) {
|
|
1168
1780
|
const { config } = useCommerce();
|
|
@@ -1221,29 +1833,24 @@ function ProductDetailsLoader({ productId, className, children }) {
|
|
|
1221
1833
|
setQuantity(1);
|
|
1222
1834
|
setImageIndex(0);
|
|
1223
1835
|
};
|
|
1224
|
-
const
|
|
1225
|
-
const register = useCallback((control) => {
|
|
1226
|
-
const counts = registered.current;
|
|
1227
|
-
counts.set(control, (counts.get(control) ?? 0) + 1);
|
|
1228
|
-
return () => {
|
|
1229
|
-
const count = (counts.get(control) ?? 1) - 1;
|
|
1230
|
-
if (count > 0) counts.set(control, count);
|
|
1231
|
-
else counts.delete(control);
|
|
1232
|
-
};
|
|
1233
|
-
}, []);
|
|
1836
|
+
const { register, has } = usePartRegistry();
|
|
1234
1837
|
const requiredControls = [...attributes.map((attribute) => `attribute:${attribute.name}`), ...skuOptions.length ? ["sku"] : []].join("\n");
|
|
1235
1838
|
useEffect(() => {
|
|
1236
1839
|
if (process.env.NODE_ENV === "production") return;
|
|
1237
|
-
const missing = requiredControls.split("\n").filter((control) => control && !
|
|
1840
|
+
const missing = requiredControls.split("\n").filter((control) => control && !has(control));
|
|
1238
1841
|
if (missing.length === 0) return;
|
|
1239
1842
|
console.warn(`[gd-commerce-storefront] No control for ${missing.map((control) => control === "sku" ? "the SKU option" : `"${control.slice(10)}"`).join(", ")} is rendered right now, so this product cannot be added to the cart until one is. Render <ProductDetailsOptions />, <ProductDetailsOption attribute='…' /> for each attribute, or <ProductDetailsPurchase />, or ignore this if the control mounts later.`);
|
|
1240
|
-
}, [requiredControls]);
|
|
1241
|
-
if (product.isPending) return /* @__PURE__ */ jsx("
|
|
1242
|
-
|
|
1243
|
-
children: "
|
|
1843
|
+
}, [requiredControls, has]);
|
|
1844
|
+
if (product.isPending) return /* @__PURE__ */ jsx("div", {
|
|
1845
|
+
className: partClass(className),
|
|
1846
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1847
|
+
role: "status",
|
|
1848
|
+
children: "Loading product…"
|
|
1849
|
+
})
|
|
1244
1850
|
});
|
|
1245
1851
|
if (product.isError && !productNotFound) return /* @__PURE__ */ jsxs("div", {
|
|
1246
1852
|
role: "alert",
|
|
1853
|
+
className: partClass(className),
|
|
1247
1854
|
children: [
|
|
1248
1855
|
/* @__PURE__ */ jsx("h1", {
|
|
1249
1856
|
className: "text-2xl font-semibold",
|
|
@@ -1261,17 +1868,20 @@ function ProductDetailsLoader({ productId, className, children }) {
|
|
|
1261
1868
|
})
|
|
1262
1869
|
]
|
|
1263
1870
|
});
|
|
1264
|
-
if (productNotFound || !group) return /* @__PURE__ */ jsxs("div", {
|
|
1265
|
-
className:
|
|
1266
|
-
children:
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1871
|
+
if (productNotFound || !group) return /* @__PURE__ */ jsxs("div", {
|
|
1872
|
+
className: partClass(className),
|
|
1873
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
1874
|
+
className: "text-2xl font-semibold",
|
|
1875
|
+
children: "Product not found"
|
|
1876
|
+
}), /* @__PURE__ */ jsx(Link, {
|
|
1877
|
+
className: `${buttonClass} mt-4`,
|
|
1878
|
+
to: config.catalogPath,
|
|
1879
|
+
children: "Back to shop"
|
|
1880
|
+
})]
|
|
1881
|
+
});
|
|
1272
1882
|
const value = {
|
|
1273
1883
|
product: group,
|
|
1274
|
-
name: group
|
|
1884
|
+
name: getProductName(group),
|
|
1275
1885
|
selectedSku,
|
|
1276
1886
|
quantity,
|
|
1277
1887
|
availableQuantity: getAvailableInventoryQuantity(selectedSku),
|
|
@@ -1313,16 +1923,16 @@ const disabledReasons = {
|
|
|
1313
1923
|
unavailable: "This combination is unavailable. Choose different options.",
|
|
1314
1924
|
"not-purchasable": "This product is not currently available to purchase."
|
|
1315
1925
|
};
|
|
1316
|
-
const flush = "mb-0";
|
|
1317
1926
|
/** The packaged purchase area: options, status, availability, quantity, and add to cart. */
|
|
1318
1927
|
function ProductDetailsPurchase({ className }) {
|
|
1319
|
-
useProductDetailsInternal("ProductDetailsPurchase");
|
|
1928
|
+
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
|
|
1929
|
+
const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
|
|
1320
1930
|
return /* @__PURE__ */ jsxs("div", {
|
|
1321
1931
|
className: partClass("space-y-6", className),
|
|
1322
1932
|
children: [
|
|
1323
1933
|
/* @__PURE__ */ jsx(OptionControls, {}),
|
|
1324
|
-
/* @__PURE__ */ jsx(
|
|
1325
|
-
/* @__PURE__ */ jsx(ProductDetailsAvailability, { className:
|
|
1934
|
+
/* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
|
|
1935
|
+
/* @__PURE__ */ jsx(ProductDetailsAvailability, { className: belowOptions }),
|
|
1326
1936
|
/* @__PURE__ */ jsx(ProductDetailsQuantity, {}),
|
|
1327
1937
|
/* @__PURE__ */ jsx(ProductDetailsAddToCart, {})
|
|
1328
1938
|
]
|
|
@@ -1339,32 +1949,53 @@ function ProductDetailsOptions({ className }) {
|
|
|
1339
1949
|
}
|
|
1340
1950
|
function OptionControls() {
|
|
1341
1951
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1342
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1952
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
|
|
1953
|
+
}
|
|
1954
|
+
function Select({ children,...props }) {
|
|
1955
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1956
|
+
className: "relative",
|
|
1957
|
+
children: [/* @__PURE__ */ jsx("select", {
|
|
1958
|
+
className: `${inputClass} w-full appearance-none pr-10`,
|
|
1959
|
+
...props,
|
|
1960
|
+
children
|
|
1961
|
+
}), /* @__PURE__ */ jsx("svg", {
|
|
1962
|
+
"aria-hidden": "true",
|
|
1963
|
+
viewBox: "0 0 16 16",
|
|
1964
|
+
className: "pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-600",
|
|
1965
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1966
|
+
d: "M4 6l4 4 4-4",
|
|
1967
|
+
fill: "none",
|
|
1968
|
+
stroke: "currentColor",
|
|
1969
|
+
strokeWidth: "1.5",
|
|
1970
|
+
strokeLinecap: "round",
|
|
1971
|
+
strokeLinejoin: "round"
|
|
1972
|
+
})
|
|
1973
|
+
})]
|
|
1974
|
+
});
|
|
1346
1975
|
}
|
|
1347
1976
|
function SkuOption() {
|
|
1348
1977
|
const { skuOptions, explicitSkuId, selectSku } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1349
1978
|
const id = useId();
|
|
1350
1979
|
useSkuOptionRegistration();
|
|
1351
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1980
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1981
|
+
className: partClass(),
|
|
1982
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
1983
|
+
htmlFor: id,
|
|
1984
|
+
className: "mb-2 block font-medium",
|
|
1985
|
+
children: "Option"
|
|
1986
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
1987
|
+
id,
|
|
1988
|
+
value: explicitSkuId,
|
|
1989
|
+
onChange: (event) => selectSku(event.target.value),
|
|
1990
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
1991
|
+
value: "",
|
|
1992
|
+
children: "Select an option"
|
|
1993
|
+
}), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
|
|
1994
|
+
value: sku.id ?? "",
|
|
1995
|
+
children: sku.label?.trim() || sku.name?.trim()
|
|
1996
|
+
}, sku.id))]
|
|
1997
|
+
})]
|
|
1998
|
+
});
|
|
1368
1999
|
}
|
|
1369
2000
|
/** A select for one product attribute. Use `useProductDetailsOption` for swatches or other custom controls. */
|
|
1370
2001
|
function ProductDetailsOption({ attribute, className }) {
|
|
@@ -1375,8 +2006,7 @@ function ProductDetailsOption({ attribute, className }) {
|
|
|
1375
2006
|
children: /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", {
|
|
1376
2007
|
className: "mb-2 font-medium",
|
|
1377
2008
|
children: option.label
|
|
1378
|
-
}), /* @__PURE__ */ jsxs(
|
|
1379
|
-
className: `${inputClass} w-full`,
|
|
2009
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
1380
2010
|
"aria-label": option.label,
|
|
1381
2011
|
value: option.selected ?? "",
|
|
1382
2012
|
onChange: (event) => option.select(event.target.value || null),
|
|
@@ -1392,45 +2022,48 @@ function ProductDetailsOption({ attribute, className }) {
|
|
|
1392
2022
|
}
|
|
1393
2023
|
/** The single message explaining why the selection is not yet purchasable. Renders nothing when ready. */
|
|
1394
2024
|
function ProductDetailsPurchaseStatus({ className }) {
|
|
1395
|
-
return /* @__PURE__ */ jsx(PurchaseStatusMessage, { className });
|
|
1396
|
-
}
|
|
1397
|
-
function PurchaseStatusMessage({ className, textClassName }) {
|
|
1398
2025
|
const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
|
|
1399
|
-
|
|
1400
|
-
className: partClass(partClassName, className),
|
|
1401
|
-
children
|
|
1402
|
-
});
|
|
2026
|
+
let content;
|
|
1403
2027
|
switch (status) {
|
|
1404
2028
|
case "ready": return null;
|
|
1405
|
-
case "variant-error":
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
case "
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2029
|
+
case "variant-error":
|
|
2030
|
+
content = /* @__PURE__ */ jsxs("div", {
|
|
2031
|
+
role: "alert",
|
|
2032
|
+
className: "text-red-700",
|
|
2033
|
+
children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
|
|
2034
|
+
type: "button",
|
|
2035
|
+
className: "min-h-11 bg-white text-red-700 underline",
|
|
2036
|
+
onClick: retryVariant,
|
|
2037
|
+
children: "Retry availability"
|
|
2038
|
+
})]
|
|
2039
|
+
});
|
|
2040
|
+
break;
|
|
2041
|
+
case "unavailable":
|
|
2042
|
+
content = /* @__PURE__ */ jsx("p", {
|
|
2043
|
+
role: "status",
|
|
2044
|
+
className: "text-sm text-red-700",
|
|
2045
|
+
children: disabledReasons[status]
|
|
2046
|
+
});
|
|
2047
|
+
break;
|
|
2048
|
+
case "not-purchasable":
|
|
2049
|
+
content = /* @__PURE__ */ jsx("p", {
|
|
2050
|
+
className: "text-sm",
|
|
2051
|
+
children: disabledReasons[status]
|
|
2052
|
+
});
|
|
2053
|
+
break;
|
|
2054
|
+
case "incomplete":
|
|
2055
|
+
case "checking":
|
|
2056
|
+
content = /* @__PURE__ */ jsx("p", {
|
|
2057
|
+
role: "status",
|
|
2058
|
+
className: "text-sm",
|
|
2059
|
+
children: disabledReasons[status]
|
|
2060
|
+
});
|
|
2061
|
+
break;
|
|
1433
2062
|
}
|
|
2063
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2064
|
+
className: partClass(className),
|
|
2065
|
+
children: content
|
|
2066
|
+
});
|
|
1434
2067
|
}
|
|
1435
2068
|
/** Stock for the verified SKU. Renders nothing until a SKU is verified. */
|
|
1436
2069
|
function ProductDetailsAvailability({ className }) {
|
|
@@ -1441,31 +2074,43 @@ function ProductDetailsAvailability({ className }) {
|
|
|
1441
2074
|
children: /* @__PURE__ */ jsx("p", {
|
|
1442
2075
|
className: "text-sm",
|
|
1443
2076
|
"aria-live": "polite",
|
|
1444
|
-
children: availableQuantity === null ? "Available to order" : availableQuantity
|
|
2077
|
+
children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
|
|
1445
2078
|
})
|
|
1446
2079
|
});
|
|
1447
2080
|
}
|
|
1448
|
-
/** Quantity for the verified SKU. Renders nothing until a SKU is verified. */
|
|
2081
|
+
/** Quantity for the verified SKU, with a message when it cannot be added. Renders nothing until a SKU is verified. */
|
|
1449
2082
|
function ProductDetailsQuantity({ className }) {
|
|
1450
2083
|
const { selectedSku, availableQuantity, quantity, setQuantity } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
1451
2084
|
const id = useId();
|
|
2085
|
+
const problemId = useId();
|
|
1452
2086
|
if (!selectedSku) return null;
|
|
2087
|
+
const problem = isSoldOut(availableQuantity) ? null : quantityProblem(quantity, availableQuantity);
|
|
1453
2088
|
return /* @__PURE__ */ jsxs("div", {
|
|
1454
2089
|
className: partClass(className),
|
|
1455
|
-
children: [
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
2090
|
+
children: [
|
|
2091
|
+
/* @__PURE__ */ jsx("label", {
|
|
2092
|
+
htmlFor: id,
|
|
2093
|
+
className: "mb-2 block font-medium",
|
|
2094
|
+
children: "Quantity"
|
|
2095
|
+
}),
|
|
2096
|
+
/* @__PURE__ */ jsx("input", {
|
|
2097
|
+
id,
|
|
2098
|
+
type: "number",
|
|
2099
|
+
min: 1,
|
|
2100
|
+
max: availableQuantity ?? void 0,
|
|
2101
|
+
step: 1,
|
|
2102
|
+
className: `${inputClass} w-28`,
|
|
2103
|
+
value: Number.isNaN(quantity) ? "" : quantity,
|
|
2104
|
+
"aria-invalid": problem ? true : void 0,
|
|
2105
|
+
"aria-describedby": problem ? problemId : void 0,
|
|
2106
|
+
onChange: (event) => setQuantity(event.target.valueAsNumber)
|
|
2107
|
+
}),
|
|
2108
|
+
problem && /* @__PURE__ */ jsx("p", {
|
|
2109
|
+
id: problemId,
|
|
2110
|
+
className: "mt-2 text-sm text-red-700",
|
|
2111
|
+
children: problem
|
|
2112
|
+
})
|
|
2113
|
+
]
|
|
1469
2114
|
});
|
|
1470
2115
|
}
|
|
1471
2116
|
/** Adds the verified SKU and quantity. While disabled, its accessible description says why. */
|
|
@@ -1563,14 +2208,17 @@ function ProductDetailsTitle({ className }) {
|
|
|
1563
2208
|
function ProductDetailsPrice({ className }) {
|
|
1564
2209
|
const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
|
|
1565
2210
|
const { config } = useCommerce();
|
|
1566
|
-
return /* @__PURE__ */
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
children: money(price.
|
|
1573
|
-
|
|
2211
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2212
|
+
className: partClass(className),
|
|
2213
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2214
|
+
"aria-live": "polite",
|
|
2215
|
+
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium",
|
|
2216
|
+
"data-testid": "product-price",
|
|
2217
|
+
children: [price?.value?.value != null ? money(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money(product.priceRange.min, config.currencyCode)}` : "Price unavailable", price?.compareAtValue?.value != null && price.value?.value != null && price.compareAtValue.value > price.value.value && /* @__PURE__ */ jsx("del", {
|
|
2218
|
+
className: "text-lg font-normal",
|
|
2219
|
+
children: money(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
|
|
2220
|
+
})]
|
|
2221
|
+
})
|
|
1574
2222
|
});
|
|
1575
2223
|
}
|
|
1576
2224
|
/** The verified SKU's description, or the product's. */
|
|
@@ -1586,4 +2234,4 @@ function ProductDetailsDescription({ className }) {
|
|
|
1586
2234
|
}
|
|
1587
2235
|
|
|
1588
2236
|
//#endregion
|
|
1589
|
-
export { AddToCartButton, ApiError, CartButton, CartDrawer, Catalog, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCommerce, useProductDetails, useProductDetailsOption };
|
|
2237
|
+
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useProductCard, useProductDetails, useProductDetailsOption };
|