@godaddy/gd-commerce-storefront 0.4.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 +116 -11
- package/dist/index.d.ts +186 -5
- package/dist/index.js +729 -354
- package/dist/styles.css +27 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as Dialog from "@radix-ui/react-dialog";
|
|
2
1
|
import { Children, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Link, useParams, useSearchParams } from "react-router";
|
|
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,61 +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
|
-
/** The product's display name: its label, then its name. */
|
|
152
|
-
function getProductName(skuGroup) {
|
|
153
|
-
return skuGroup.label ?? skuGroup.name ?? "Product";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
101
|
//#endregion
|
|
157
102
|
//#region src/sample-cart.tsx
|
|
158
103
|
const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
|
|
@@ -160,6 +105,7 @@ const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
|
|
|
160
105
|
name: `Product ${index + 1}`,
|
|
161
106
|
price: (index + 1) * 1e3
|
|
162
107
|
}));
|
|
108
|
+
const noQuantities = {};
|
|
163
109
|
const SampleCartContext = createContext(null);
|
|
164
110
|
function useSampleCart() {
|
|
165
111
|
const value = useContext(SampleCartContext);
|
|
@@ -184,7 +130,11 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
184
130
|
active.current = null;
|
|
185
131
|
};
|
|
186
132
|
}, [session]);
|
|
187
|
-
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]);
|
|
188
138
|
function update(key, quantity) {
|
|
189
139
|
if (!enabled || active.current !== session || !sampleProducts.some((product) => product.key === key)) return false;
|
|
190
140
|
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
|
|
@@ -204,10 +154,7 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
204
154
|
}
|
|
205
155
|
return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
|
|
206
156
|
value: {
|
|
207
|
-
items
|
|
208
|
-
...product,
|
|
209
|
-
quantity: quantities[product.key]
|
|
210
|
-
}] : []),
|
|
157
|
+
items,
|
|
211
158
|
add: (key) => {
|
|
212
159
|
const product = sampleProducts.find((product$1) => product$1.key === key);
|
|
213
160
|
if (!product || !update(key)) return false;
|
|
@@ -232,6 +179,8 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
232
179
|
//#endregion
|
|
233
180
|
//#region src/commerce-provider.tsx
|
|
234
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);
|
|
235
184
|
function useCommerce() {
|
|
236
185
|
const context = useContext(CommerceContext);
|
|
237
186
|
if (!context) throw new Error("Mount CommerceProvider once around the header and page outlet, inside the router.");
|
|
@@ -251,7 +200,7 @@ async function loadConfig(signal) {
|
|
|
251
200
|
currencyCode: config.currencyCode
|
|
252
201
|
};
|
|
253
202
|
}
|
|
254
|
-
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {} }) {
|
|
203
|
+
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, onItemAdded }) {
|
|
255
204
|
const config = useQuery({
|
|
256
205
|
retry: false,
|
|
257
206
|
queryKey: ["commerce", "configuration"],
|
|
@@ -280,10 +229,11 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
280
229
|
config.refetch();
|
|
281
230
|
},
|
|
282
231
|
theme,
|
|
232
|
+
onItemAdded,
|
|
283
233
|
children
|
|
284
234
|
});
|
|
285
235
|
}
|
|
286
|
-
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme }) {
|
|
236
|
+
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, onItemAdded }) {
|
|
287
237
|
const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
|
|
288
238
|
const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
|
|
289
239
|
const ready = connection === "ready";
|
|
@@ -307,6 +257,23 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
307
257
|
const queue = useRef(Promise.resolve(true));
|
|
308
258
|
const alive = useRef(true);
|
|
309
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
|
+
}
|
|
310
277
|
function rememberOpener() {
|
|
311
278
|
opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
312
279
|
}
|
|
@@ -436,7 +403,12 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
436
403
|
if (!isActive()) return;
|
|
437
404
|
commit(result.cart);
|
|
438
405
|
setAnnouncement(`${item.name} added to your cart.`);
|
|
439
|
-
|
|
406
|
+
openDrawer();
|
|
407
|
+
added.current?.({
|
|
408
|
+
name: item.name,
|
|
409
|
+
quantity: item.quantity,
|
|
410
|
+
isSample: false
|
|
411
|
+
});
|
|
440
412
|
});
|
|
441
413
|
}
|
|
442
414
|
function mutateItem(itemId, method, quantity) {
|
|
@@ -505,6 +477,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
505
477
|
cart,
|
|
506
478
|
open,
|
|
507
479
|
setOpen: (next) => {
|
|
480
|
+
if (next && drawers.current === 0) return;
|
|
508
481
|
if (next) rememberOpener();
|
|
509
482
|
setOpen(next);
|
|
510
483
|
},
|
|
@@ -523,15 +496,22 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
523
496
|
refresh,
|
|
524
497
|
checkout
|
|
525
498
|
},
|
|
526
|
-
children: [/* @__PURE__ */ jsx(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
})
|
|
535
515
|
}), /* @__PURE__ */ jsx("span", {
|
|
536
516
|
className: "commerce-sr-only",
|
|
537
517
|
role: "status",
|
|
@@ -541,6 +521,117 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
541
521
|
});
|
|
542
522
|
}
|
|
543
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
|
+
|
|
544
635
|
//#endregion
|
|
545
636
|
//#region src/storefront-surface.tsx
|
|
546
637
|
function StorefrontSurface({ children, className = "" }) {
|
|
@@ -576,9 +667,8 @@ function CommerceStatus() {
|
|
|
576
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";
|
|
577
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";
|
|
578
669
|
function CartButton() {
|
|
579
|
-
const {
|
|
580
|
-
const
|
|
581
|
-
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();
|
|
582
672
|
return /* @__PURE__ */ jsx(StorefrontSurface, {
|
|
583
673
|
className: "commerce-inline",
|
|
584
674
|
children: /* @__PURE__ */ jsxs("button", {
|
|
@@ -590,16 +680,32 @@ function CartButton() {
|
|
|
590
680
|
onClick: () => setOpen(true),
|
|
591
681
|
children: ["Cart ", /* @__PURE__ */ jsx("span", {
|
|
592
682
|
className: "rounded-full bg-white px-2 py-0.5 text-xs text-neutral-900",
|
|
593
|
-
children:
|
|
683
|
+
children: itemCount
|
|
594
684
|
})]
|
|
595
685
|
})
|
|
596
686
|
});
|
|
597
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
|
+
}
|
|
598
696
|
function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
599
697
|
const { addItem, pending, hydrating, error, open, connection } = useCommerce();
|
|
600
698
|
const [adding, setAdding] = useState(false);
|
|
699
|
+
const reasonId = useId();
|
|
601
700
|
const available = getAvailableInventoryQuantity(sku);
|
|
602
|
-
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;
|
|
603
709
|
async function handleAdd() {
|
|
604
710
|
if (disabled || pending || !sku.id) return;
|
|
605
711
|
setAdding(true);
|
|
@@ -615,246 +721,35 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
|
615
721
|
}
|
|
616
722
|
return /* @__PURE__ */ jsxs(StorefrontSurface, {
|
|
617
723
|
className: "commerce-inline",
|
|
618
|
-
children: [
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const checkingOut = drawerAction === "checkout";
|
|
641
|
-
const locked = pending || hydrating || drawerAction !== null;
|
|
642
|
-
const items = isSample ? sample.items.map((item) => ({
|
|
643
|
-
id: item.key,
|
|
644
|
-
name: item.name,
|
|
645
|
-
quantity: item.quantity,
|
|
646
|
-
image: void 0,
|
|
647
|
-
options: void 0,
|
|
648
|
-
subtotal: item.price * item.quantity,
|
|
649
|
-
currency: "USD"
|
|
650
|
-
})) : (cart?.lineItems ?? []).map((item) => ({
|
|
651
|
-
id: item.id,
|
|
652
|
-
name: item.name,
|
|
653
|
-
quantity: item.quantity,
|
|
654
|
-
image: item.details?.productAssetUrl,
|
|
655
|
-
options: item.details?.selectedOptions,
|
|
656
|
-
subtotal: item.totals?.subTotal?.value,
|
|
657
|
-
currency: item.totals?.subTotal?.currencyCode
|
|
658
|
-
}));
|
|
659
|
-
const summary$1 = getCartSummaryTotals(cart);
|
|
660
|
-
const currency = isSample ? "USD" : cart?.totals?.total?.currencyCode ?? config.currencyCode;
|
|
661
|
-
const subtotal = isSample ? sample.items.reduce((sum, item) => sum + item.price * item.quantity, 0) : summary$1.subtotal;
|
|
662
|
-
async function handleAction(action, operation) {
|
|
663
|
-
if (locked) return;
|
|
664
|
-
setDrawerAction(action);
|
|
665
|
-
try {
|
|
666
|
-
await operation();
|
|
667
|
-
} finally {
|
|
668
|
-
setDrawerAction(null);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
672
|
-
open,
|
|
673
|
-
onOpenChange: setOpen,
|
|
674
|
-
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__ */ jsxs(Dialog.Content, {
|
|
675
|
-
"data-commerce-source": isSample ? "sample" : void 0,
|
|
676
|
-
className: "fixed inset-y-0 right-0 z-50 flex w-full max-w-lg flex-col bg-white text-neutral-900 shadow-xl",
|
|
677
|
-
onCloseAutoFocus: (event) => {
|
|
678
|
-
event.preventDefault();
|
|
679
|
-
restoreFocus();
|
|
680
|
-
},
|
|
681
|
-
children: [
|
|
682
|
-
/* @__PURE__ */ jsxs("div", {
|
|
683
|
-
className: "flex items-start justify-between border-b border-neutral-200 p-6",
|
|
684
|
-
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Dialog.Title, {
|
|
685
|
-
className: "text-2xl font-semibold",
|
|
686
|
-
children: isSample ? "Sample cart" : "Your cart"
|
|
687
|
-
}), /* @__PURE__ */ jsx(Dialog.Description, {
|
|
688
|
-
className: "mt-1 text-sm text-neutral-600",
|
|
689
|
-
children: isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout."
|
|
690
|
-
})] }), /* @__PURE__ */ jsx(Dialog.Close, {
|
|
691
|
-
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",
|
|
692
|
-
"aria-label": "Close cart",
|
|
693
|
-
children: "✕"
|
|
694
|
-
})]
|
|
695
|
-
}),
|
|
696
|
-
/* @__PURE__ */ jsxs("div", {
|
|
697
|
-
className: "flex-1 overflow-y-auto p-6",
|
|
698
|
-
children: [
|
|
699
|
-
storageWarning && /* @__PURE__ */ jsx("p", {
|
|
700
|
-
role: "status",
|
|
701
|
-
className: "mb-4 text-sm text-amber-800",
|
|
702
|
-
children: storageWarning
|
|
703
|
-
}),
|
|
704
|
-
error && /* @__PURE__ */ jsxs("div", {
|
|
705
|
-
role: "alert",
|
|
706
|
-
className: "mb-4 rounded-lg bg-red-50 p-4 text-red-800",
|
|
707
|
-
children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
|
|
708
|
-
type: "button",
|
|
709
|
-
className: "mt-2 min-h-11 bg-red-50 text-red-800 underline disabled:cursor-not-allowed",
|
|
710
|
-
disabled: drawerAction === "refresh",
|
|
711
|
-
"aria-disabled": locked,
|
|
712
|
-
"aria-busy": drawerAction === "refresh",
|
|
713
|
-
onClick: () => void handleAction("refresh", refresh),
|
|
714
|
-
children: "Refresh cart"
|
|
715
|
-
})]
|
|
716
|
-
}),
|
|
717
|
-
hydrating && /* @__PURE__ */ jsx("p", {
|
|
718
|
-
role: "status",
|
|
719
|
-
children: "Loading your cart…"
|
|
720
|
-
}),
|
|
721
|
-
!hydrating && !items.length && /* @__PURE__ */ jsxs("div", {
|
|
722
|
-
className: "py-16 text-center",
|
|
723
|
-
children: [
|
|
724
|
-
/* @__PURE__ */ jsx("p", {
|
|
725
|
-
className: "text-xl font-medium",
|
|
726
|
-
children: "Your cart is empty"
|
|
727
|
-
}),
|
|
728
|
-
/* @__PURE__ */ jsx("p", {
|
|
729
|
-
className: "mt-2 text-neutral-600",
|
|
730
|
-
children: isSample ? "Add a sample product to try the cart." : "Find something you love in the shop."
|
|
731
|
-
}),
|
|
732
|
-
/* @__PURE__ */ jsx(Link, {
|
|
733
|
-
className: `${buttonClass} mt-6`,
|
|
734
|
-
to: config.catalogPath,
|
|
735
|
-
onClick: () => setOpen(false),
|
|
736
|
-
children: "Browse products"
|
|
737
|
-
})
|
|
738
|
-
]
|
|
739
|
-
}),
|
|
740
|
-
/* @__PURE__ */ jsx("ul", {
|
|
741
|
-
className: "divide-y divide-neutral-200",
|
|
742
|
-
children: items.map((item) => /* @__PURE__ */ jsxs("li", {
|
|
743
|
-
className: "py-5 first:pt-0",
|
|
744
|
-
"data-testid": "cart-item",
|
|
745
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
746
|
-
className: "flex gap-4",
|
|
747
|
-
children: [item.image && /* @__PURE__ */ jsx("img", {
|
|
748
|
-
src: item.image,
|
|
749
|
-
alt: "",
|
|
750
|
-
className: "h-20 w-20 rounded-lg bg-neutral-100 object-cover"
|
|
751
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
752
|
-
className: "min-w-0 flex-1",
|
|
753
|
-
children: [
|
|
754
|
-
/* @__PURE__ */ jsx("h3", {
|
|
755
|
-
className: "font-semibold",
|
|
756
|
-
children: item.name
|
|
757
|
-
}),
|
|
758
|
-
item.options?.map((option) => /* @__PURE__ */ jsxs("p", {
|
|
759
|
-
className: "text-sm text-neutral-600",
|
|
760
|
-
children: [
|
|
761
|
-
option.attribute,
|
|
762
|
-
": ",
|
|
763
|
-
option.values?.join(", ")
|
|
764
|
-
]
|
|
765
|
-
}, `${option.attribute}:${option.values?.join(",")}`)),
|
|
766
|
-
/* @__PURE__ */ jsx("p", {
|
|
767
|
-
className: "mt-1 text-sm",
|
|
768
|
-
children: typeof item.subtotal === "number" ? money(item.subtotal, item.currency ?? currency) : "Price unavailable"
|
|
769
|
-
})
|
|
770
|
-
]
|
|
771
|
-
})]
|
|
772
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
773
|
-
className: "mt-4 flex items-center justify-between gap-2",
|
|
774
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
775
|
-
className: "flex items-center rounded-lg border border-neutral-300",
|
|
776
|
-
children: [
|
|
777
|
-
/* @__PURE__ */ jsx("button", {
|
|
778
|
-
type: "button",
|
|
779
|
-
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",
|
|
780
|
-
"aria-label": `Decrease quantity of ${item.name}`,
|
|
781
|
-
disabled: drawerAction === `decrease:${item.id}` || !item.id || (item.quantity ?? 0) <= 1,
|
|
782
|
-
"aria-disabled": locked || !item.id || (item.quantity ?? 0) <= 1,
|
|
783
|
-
"aria-busy": drawerAction === `decrease:${item.id}`,
|
|
784
|
-
onClick: () => void handleAction(`decrease:${item.id}`, () => changeQuantity(item.id ?? "", (item.quantity ?? 1) - 1)),
|
|
785
|
-
children: "−"
|
|
786
|
-
}),
|
|
787
|
-
/* @__PURE__ */ jsxs("span", {
|
|
788
|
-
className: "min-w-8 text-center",
|
|
789
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
790
|
-
className: "sr-only",
|
|
791
|
-
children: "Quantity: "
|
|
792
|
-
}), item.quantity]
|
|
793
|
-
}),
|
|
794
|
-
/* @__PURE__ */ jsx("button", {
|
|
795
|
-
type: "button",
|
|
796
|
-
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",
|
|
797
|
-
"aria-label": `Increase quantity of ${item.name}`,
|
|
798
|
-
disabled: drawerAction === `increase:${item.id}` || !item.id,
|
|
799
|
-
"aria-disabled": locked || !item.id,
|
|
800
|
-
"aria-busy": drawerAction === `increase:${item.id}`,
|
|
801
|
-
onClick: () => void handleAction(`increase:${item.id}`, () => changeQuantity(item.id ?? "", (item.quantity ?? 1) + 1)),
|
|
802
|
-
children: "+"
|
|
803
|
-
})
|
|
804
|
-
]
|
|
805
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
806
|
-
type: "button",
|
|
807
|
-
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",
|
|
808
|
-
"aria-label": `Remove ${item.name}`,
|
|
809
|
-
disabled: drawerAction === `remove:${item.id}` || !item.id,
|
|
810
|
-
"aria-disabled": locked || !item.id,
|
|
811
|
-
"aria-busy": drawerAction === `remove:${item.id}`,
|
|
812
|
-
onClick: () => void handleAction(`remove:${item.id}`, () => removeItem(item.id ?? "")),
|
|
813
|
-
children: "Remove"
|
|
814
|
-
})]
|
|
815
|
-
})]
|
|
816
|
-
}, item.id))
|
|
817
|
-
})
|
|
818
|
-
]
|
|
819
|
-
}),
|
|
820
|
-
items.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
821
|
-
className: "border-t border-neutral-200 p-6",
|
|
822
|
-
children: [
|
|
823
|
-
/* @__PURE__ */ jsx("dl", {
|
|
824
|
-
className: "space-y-2 text-sm",
|
|
825
|
-
children: (isSample || typeof cart?.totals?.subTotal?.value === "number") && /* @__PURE__ */ jsxs("div", {
|
|
826
|
-
className: "flex justify-between",
|
|
827
|
-
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money(subtotal, isSample ? "USD" : cart?.totals?.subTotal?.currencyCode ?? currency) })]
|
|
828
|
-
})
|
|
829
|
-
}),
|
|
830
|
-
!isSample && /* @__PURE__ */ jsx("p", {
|
|
831
|
-
className: "commerce-cart-checkout-adjustments-note mt-3 text-sm text-neutral-600",
|
|
832
|
-
children: "Shipping, taxes, and discounts are calculated at checkout."
|
|
833
|
-
}),
|
|
834
|
-
isSample ? /* @__PURE__ */ jsx("p", {
|
|
835
|
-
className: "text-sm text-neutral-600",
|
|
836
|
-
children: "Example total in USD. Checkout is not set up yet."
|
|
837
|
-
}) : config.checkoutSuccessPath ? /* @__PURE__ */ jsx("button", {
|
|
838
|
-
type: "button",
|
|
839
|
-
className: `${buttonClass} w-full`,
|
|
840
|
-
disabled: checkingOut || hydrating || cart?.totals?.total?.value == null,
|
|
841
|
-
"aria-disabled": locked || cart?.totals?.total?.value == null,
|
|
842
|
-
"aria-busy": checkingOut,
|
|
843
|
-
onClick: () => void handleAction("checkout", checkout),
|
|
844
|
-
children: checkingOut ? "Opening checkout…" : "Proceed to Checkout"
|
|
845
|
-
}) : /* @__PURE__ */ jsx("p", {
|
|
846
|
-
className: "text-sm text-neutral-600",
|
|
847
|
-
children: "Checkout is not available yet."
|
|
848
|
-
})
|
|
849
|
-
]
|
|
850
|
-
})
|
|
851
|
-
]
|
|
852
|
-
})] }) })
|
|
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
|
+
]
|
|
853
746
|
});
|
|
854
747
|
}
|
|
855
748
|
|
|
856
749
|
//#endregion
|
|
857
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 !== "");
|
|
858
753
|
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
859
754
|
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
860
755
|
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
@@ -881,6 +776,478 @@ function usePartRegistry(onChange) {
|
|
|
881
776
|
}), [register, has]);
|
|
882
777
|
}
|
|
883
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();
|
|
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();
|
|
826
|
+
const isSample = connection === "unbound";
|
|
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]);
|
|
851
|
+
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
852
|
+
open,
|
|
853
|
+
onOpenChange: setOpen,
|
|
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 })
|
|
867
|
+
})
|
|
868
|
+
})
|
|
869
|
+
})] }) })
|
|
870
|
+
});
|
|
871
|
+
}
|
|
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
|
+
})
|
|
882
|
+
});
|
|
883
|
+
}
|
|
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
|
+
})
|
|
900
|
+
});
|
|
901
|
+
}
|
|
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: "✕"
|
|
912
|
+
})
|
|
913
|
+
});
|
|
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
|
+
})]
|
|
939
|
+
})]
|
|
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
|
|
998
|
+
})
|
|
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",
|
|
1034
|
+
children: [
|
|
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
|
+
|
|
884
1251
|
//#endregion
|
|
885
1252
|
//#region src/product-card.tsx
|
|
886
1253
|
function ProductImage({ url, name, className = "" }) {
|
|
@@ -1238,12 +1605,12 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
|
1238
1605
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1239
1606
|
});
|
|
1240
1607
|
/**
|
|
1241
|
-
* The product cards. `className` goes on the element that contains the cards, such as a grid.
|
|
1242
|
-
* placeholder
|
|
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.
|
|
1243
1610
|
*/
|
|
1244
1611
|
function CatalogProducts({ className, children }) {
|
|
1245
1612
|
const catalog = useCatalogInternal("CatalogProducts");
|
|
1246
|
-
const card =
|
|
1613
|
+
const card = hasChildren(children) ? children : packagedCard;
|
|
1247
1614
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
1248
1615
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
1249
1616
|
key: product.id ?? "",
|
|
@@ -1254,25 +1621,6 @@ function CatalogProducts({ className, children }) {
|
|
|
1254
1621
|
})) : sampleItems, [nodes]);
|
|
1255
1622
|
const products = catalog.source === "live" ? catalog.products : null;
|
|
1256
1623
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1257
|
-
products?.isPending && /* @__PURE__ */ jsx("div", {
|
|
1258
|
-
"aria-hidden": "true",
|
|
1259
|
-
className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3",
|
|
1260
|
-
children: [
|
|
1261
|
-
0,
|
|
1262
|
-
1,
|
|
1263
|
-
2
|
|
1264
|
-
].map((item) => /* @__PURE__ */ jsxs("div", {
|
|
1265
|
-
className: "overflow-hidden rounded-lg border border-neutral-200 motion-safe:animate-pulse",
|
|
1266
|
-
children: [/* @__PURE__ */ jsx("div", { className: "aspect-square bg-neutral-100" }), /* @__PURE__ */ jsxs("div", {
|
|
1267
|
-
className: "space-y-4 p-6",
|
|
1268
|
-
children: [
|
|
1269
|
-
/* @__PURE__ */ jsx("div", { className: "h-6 w-2/3 rounded bg-neutral-100" }),
|
|
1270
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 rounded bg-neutral-100" }),
|
|
1271
|
-
/* @__PURE__ */ jsx("div", { className: "h-11 rounded bg-neutral-100" })
|
|
1272
|
-
]
|
|
1273
|
-
})]
|
|
1274
|
-
}, item))
|
|
1275
|
-
}),
|
|
1276
1624
|
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
1277
1625
|
role: "alert",
|
|
1278
1626
|
className: "rounded-lg bg-red-50 p-5 text-red-800",
|
|
@@ -1289,7 +1637,22 @@ function CatalogProducts({ className, children }) {
|
|
|
1289
1637
|
}),
|
|
1290
1638
|
/* @__PURE__ */ jsx("div", {
|
|
1291
1639
|
className: partClass(className),
|
|
1292
|
-
children:
|
|
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, {
|
|
1293
1656
|
value: item,
|
|
1294
1657
|
children: card
|
|
1295
1658
|
}, key))
|
|
@@ -1711,31 +2074,43 @@ function ProductDetailsAvailability({ className }) {
|
|
|
1711
2074
|
children: /* @__PURE__ */ jsx("p", {
|
|
1712
2075
|
className: "text-sm",
|
|
1713
2076
|
"aria-live": "polite",
|
|
1714
|
-
children: availableQuantity === null ? "Available to order" : availableQuantity
|
|
2077
|
+
children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
|
|
1715
2078
|
})
|
|
1716
2079
|
});
|
|
1717
2080
|
}
|
|
1718
|
-
/** 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. */
|
|
1719
2082
|
function ProductDetailsQuantity({ className }) {
|
|
1720
2083
|
const { selectedSku, availableQuantity, quantity, setQuantity } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
1721
2084
|
const id = useId();
|
|
2085
|
+
const problemId = useId();
|
|
1722
2086
|
if (!selectedSku) return null;
|
|
2087
|
+
const problem = isSoldOut(availableQuantity) ? null : quantityProblem(quantity, availableQuantity);
|
|
1723
2088
|
return /* @__PURE__ */ jsxs("div", {
|
|
1724
2089
|
className: partClass(className),
|
|
1725
|
-
children: [
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
+
]
|
|
1739
2114
|
});
|
|
1740
2115
|
}
|
|
1741
2116
|
/** Adds the verified SKU and quantity. While disabled, its accessible description says why. */
|
|
@@ -1859,4 +2234,4 @@ function ProductDetailsDescription({ className }) {
|
|
|
1859
2234
|
}
|
|
1860
2235
|
|
|
1861
2236
|
//#endregion
|
|
1862
|
-
export { AddToCartButton, ApiError, CartButton, CartDrawer, 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, useCommerce, useProductCard, 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 };
|