@godaddy/gd-commerce-storefront 0.4.0 → 0.6.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 +198 -15
- package/dist/index.d.ts +381 -25
- package/dist/index.js +1094 -479
- package/dist/styles.css +30 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
1
4
|
import * as Dialog from "@radix-ui/react-dialog";
|
|
2
|
-
import { Children, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
5
|
import { Link, useParams, useSearchParams } from "react-router";
|
|
4
|
-
import { useQuery } from "@tanstack/react-query";
|
|
5
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
|
|
7
7
|
//#region src/api.ts
|
|
8
8
|
var CartIdStorage = class {
|
|
@@ -61,8 +61,9 @@ async function request(path, init) {
|
|
|
61
61
|
throw cause;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
/** Formats minor units, such as cents, as currency. Inside the provider, prefer `useMoney`, which follows its `locale`. */
|
|
65
|
+
function money(value, currency, locale = "en-US") {
|
|
66
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
66
67
|
style: "currency",
|
|
67
68
|
currency
|
|
68
69
|
});
|
|
@@ -98,61 +99,6 @@ function getCartSummaryTotals(order) {
|
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
|
|
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
102
|
//#endregion
|
|
157
103
|
//#region src/sample-cart.tsx
|
|
158
104
|
const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
|
|
@@ -160,13 +106,14 @@ const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
|
|
|
160
106
|
name: `Product ${index + 1}`,
|
|
161
107
|
price: (index + 1) * 1e3
|
|
162
108
|
}));
|
|
109
|
+
const noQuantities = {};
|
|
163
110
|
const SampleCartContext = createContext(null);
|
|
164
111
|
function useSampleCart() {
|
|
165
112
|
const value = useContext(SampleCartContext);
|
|
166
113
|
if (!value) throw new Error("Mount CommerceProvider around sample products.");
|
|
167
114
|
return value;
|
|
168
115
|
}
|
|
169
|
-
function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
116
|
+
function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
|
|
170
117
|
const session = useMemo(() => ({ enabled }), [enabled]);
|
|
171
118
|
const active = useRef(session);
|
|
172
119
|
active.current = session;
|
|
@@ -184,7 +131,11 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
184
131
|
active.current = null;
|
|
185
132
|
};
|
|
186
133
|
}, [session]);
|
|
187
|
-
const quantities = state.session === session && enabled ? state.quantities :
|
|
134
|
+
const quantities = state.session === session && enabled ? state.quantities : noQuantities;
|
|
135
|
+
const items = useMemo(() => sampleProducts.flatMap((product) => quantities[product.key] ? [{
|
|
136
|
+
...product,
|
|
137
|
+
quantity: quantities[product.key]
|
|
138
|
+
}] : []), [quantities]);
|
|
188
139
|
function update(key, quantity) {
|
|
189
140
|
if (!enabled || active.current !== session || !sampleProducts.some((product) => product.key === key)) return false;
|
|
190
141
|
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
|
|
@@ -204,10 +155,7 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
204
155
|
}
|
|
205
156
|
return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
|
|
206
157
|
value: {
|
|
207
|
-
items
|
|
208
|
-
...product,
|
|
209
|
-
quantity: quantities[product.key]
|
|
210
|
-
}] : []),
|
|
158
|
+
items,
|
|
211
159
|
add: (key) => {
|
|
212
160
|
const product = sampleProducts.find((product$1) => product$1.key === key);
|
|
213
161
|
if (!product || !update(key)) return false;
|
|
@@ -216,12 +164,27 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
216
164
|
},
|
|
217
165
|
changeQuantity: async (key, quantity) => {
|
|
218
166
|
const updated = quantity >= 1 && update(key, quantity);
|
|
219
|
-
if (updated)
|
|
167
|
+
if (updated) {
|
|
168
|
+
announce("Sample cart quantity updated.");
|
|
169
|
+
emit({
|
|
170
|
+
type: "quantity-changed",
|
|
171
|
+
itemId: key,
|
|
172
|
+
quantity,
|
|
173
|
+
isSample: true
|
|
174
|
+
});
|
|
175
|
+
}
|
|
220
176
|
return updated;
|
|
221
177
|
},
|
|
222
178
|
remove: async (key) => {
|
|
223
179
|
const updated = update(key, 0);
|
|
224
|
-
if (updated)
|
|
180
|
+
if (updated) {
|
|
181
|
+
announce("Sample item removed from cart.");
|
|
182
|
+
emit({
|
|
183
|
+
type: "item-removed",
|
|
184
|
+
itemId: key,
|
|
185
|
+
isSample: true
|
|
186
|
+
});
|
|
187
|
+
}
|
|
225
188
|
return updated;
|
|
226
189
|
}
|
|
227
190
|
},
|
|
@@ -232,12 +195,46 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
232
195
|
//#endregion
|
|
233
196
|
//#region src/commerce-provider.tsx
|
|
234
197
|
const CommerceContext = createContext(null);
|
|
198
|
+
/** Lets a mounted cart drawer register itself, so adds open it only when one exists. Not public. */
|
|
199
|
+
const CartDrawerRegistry = createContext(null);
|
|
235
200
|
function useCommerce() {
|
|
236
201
|
const context = useContext(CommerceContext);
|
|
237
202
|
if (!context) throw new Error("Mount CommerceProvider once around the header and page outlet, inside the router.");
|
|
238
203
|
return context;
|
|
239
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Formats minor units, such as cents, in the provider's `locale`, the way the package's own parts do. Uses the
|
|
207
|
+
* store's currency unless `currency` is given.
|
|
208
|
+
*/
|
|
209
|
+
function useMoney() {
|
|
210
|
+
const { locale, config } = useCommerce();
|
|
211
|
+
const { currencyCode } = config;
|
|
212
|
+
return useCallback((value, currency = currencyCode) => money(value, currency, locale), [locale, currencyCode]);
|
|
213
|
+
}
|
|
240
214
|
const CONNECTING_FAILURE_GRACE_MS = 6e4;
|
|
215
|
+
/** `locale` as a canonical tag, or `en-US` when it is not a valid one, so one bad prop cannot break every price. */
|
|
216
|
+
function resolveLocale(locale) {
|
|
217
|
+
if (!locale) return "en-US";
|
|
218
|
+
try {
|
|
219
|
+
return Intl.getCanonicalLocales(locale)[0] ?? "en-US";
|
|
220
|
+
} catch {
|
|
221
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[gd-commerce-storefront] "${locale}" is not a valid locale, so prices use en-US.`);
|
|
222
|
+
return "en-US";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const logCallbackFailure = (cause) => console.error("[gd-commerce-storefront] A storefront callback threw.", cause);
|
|
226
|
+
/**
|
|
227
|
+
* Runs a host callback so that its failure cannot fail the cart operation that triggered it. An async callback is
|
|
228
|
+
* not awaited; its rejection is logged instead of becoming an unhandled rejection.
|
|
229
|
+
*/
|
|
230
|
+
function callHost(callback) {
|
|
231
|
+
try {
|
|
232
|
+
const result = callback();
|
|
233
|
+
if (typeof result?.then === "function") Promise.resolve(result).catch(logCallbackFailure);
|
|
234
|
+
} catch (cause) {
|
|
235
|
+
logCallbackFailure(cause);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
241
238
|
async function loadConfig(signal) {
|
|
242
239
|
const config = await request("/config", { signal });
|
|
243
240
|
if (config?.state === "unbound" || config?.state === "connecting") {
|
|
@@ -251,7 +248,8 @@ async function loadConfig(signal) {
|
|
|
251
248
|
currencyCode: config.currencyCode
|
|
252
249
|
};
|
|
253
250
|
}
|
|
254
|
-
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {} }) {
|
|
251
|
+
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, locale, onItemAdded, onEvent }) {
|
|
252
|
+
const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
|
|
255
253
|
const config = useQuery({
|
|
256
254
|
retry: false,
|
|
257
255
|
queryKey: ["commerce", "configuration"],
|
|
@@ -280,10 +278,13 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
280
278
|
config.refetch();
|
|
281
279
|
},
|
|
282
280
|
theme,
|
|
281
|
+
locale: resolvedLocale,
|
|
282
|
+
onItemAdded,
|
|
283
|
+
onEvent,
|
|
283
284
|
children
|
|
284
285
|
});
|
|
285
286
|
}
|
|
286
|
-
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme }) {
|
|
287
|
+
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, locale, onItemAdded, onEvent }) {
|
|
287
288
|
const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
|
|
288
289
|
const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
|
|
289
290
|
const ready = connection === "ready";
|
|
@@ -307,6 +308,41 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
307
308
|
const queue = useRef(Promise.resolve(true));
|
|
308
309
|
const alive = useRef(true);
|
|
309
310
|
const opener = useRef(null);
|
|
311
|
+
const callbacks = useRef({
|
|
312
|
+
onItemAdded,
|
|
313
|
+
onEvent
|
|
314
|
+
});
|
|
315
|
+
callbacks.current = {
|
|
316
|
+
onItemAdded,
|
|
317
|
+
onEvent
|
|
318
|
+
};
|
|
319
|
+
/** Reports an event to the host. A callback that throws is logged, and the operation still succeeds. */
|
|
320
|
+
function emit(event) {
|
|
321
|
+
if (event.type === "item-added") {
|
|
322
|
+
const { name, quantity, isSample } = event;
|
|
323
|
+
callHost(() => callbacks.current.onItemAdded?.({
|
|
324
|
+
name,
|
|
325
|
+
quantity,
|
|
326
|
+
isSample
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
callHost(() => callbacks.current.onEvent?.(event));
|
|
330
|
+
}
|
|
331
|
+
const drawers = useRef(0);
|
|
332
|
+
const registerDrawer = useCallback(() => {
|
|
333
|
+
drawers.current += 1;
|
|
334
|
+
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.");
|
|
335
|
+
return () => {
|
|
336
|
+
drawers.current -= 1;
|
|
337
|
+
if (drawers.current === 0) setOpen(false);
|
|
338
|
+
};
|
|
339
|
+
}, []);
|
|
340
|
+
/** Opens the drawer for an add, if one is mounted. */
|
|
341
|
+
function openDrawer() {
|
|
342
|
+
if (drawers.current === 0) return;
|
|
343
|
+
rememberOpener();
|
|
344
|
+
setOpen(true);
|
|
345
|
+
}
|
|
310
346
|
function rememberOpener() {
|
|
311
347
|
opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
312
348
|
}
|
|
@@ -358,7 +394,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
358
394
|
throw cause;
|
|
359
395
|
}
|
|
360
396
|
}
|
|
361
|
-
function run(operation, loading = false) {
|
|
397
|
+
function run(kind, operation, loading = false) {
|
|
362
398
|
const task = queue.current.then(async () => {
|
|
363
399
|
if (!isActive()) return false;
|
|
364
400
|
setPending(true);
|
|
@@ -371,7 +407,15 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
371
407
|
else await operation();
|
|
372
408
|
return isActive();
|
|
373
409
|
} catch (cause) {
|
|
374
|
-
if (isActive())
|
|
410
|
+
if (isActive()) {
|
|
411
|
+
const text = message(cause);
|
|
412
|
+
setError(text);
|
|
413
|
+
emit({
|
|
414
|
+
type: "error",
|
|
415
|
+
operation: kind,
|
|
416
|
+
message: text
|
|
417
|
+
});
|
|
418
|
+
}
|
|
375
419
|
return false;
|
|
376
420
|
} finally {
|
|
377
421
|
if (isActive()) {
|
|
@@ -384,7 +428,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
384
428
|
return task;
|
|
385
429
|
}
|
|
386
430
|
function refresh() {
|
|
387
|
-
return run(hydrate, true);
|
|
431
|
+
return run("refresh", hydrate, true);
|
|
388
432
|
}
|
|
389
433
|
useEffect(() => {
|
|
390
434
|
alive.current = true;
|
|
@@ -415,7 +459,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
415
459
|
}, [cartIdStorage, connection]);
|
|
416
460
|
function addItem(item) {
|
|
417
461
|
rememberOpener();
|
|
418
|
-
return run(async () => {
|
|
462
|
+
return run("add", async () => {
|
|
419
463
|
if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new Error("Select a product variant and a positive quantity.");
|
|
420
464
|
await hydrate();
|
|
421
465
|
if (!isActive()) return;
|
|
@@ -436,11 +480,18 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
436
480
|
if (!isActive()) return;
|
|
437
481
|
commit(result.cart);
|
|
438
482
|
setAnnouncement(`${item.name} added to your cart.`);
|
|
439
|
-
|
|
483
|
+
openDrawer();
|
|
484
|
+
emit({
|
|
485
|
+
type: "item-added",
|
|
486
|
+
skuId: item.skuId,
|
|
487
|
+
name: item.name,
|
|
488
|
+
quantity: item.quantity,
|
|
489
|
+
isSample: false
|
|
490
|
+
});
|
|
440
491
|
});
|
|
441
492
|
}
|
|
442
493
|
function mutateItem(itemId, method, quantity) {
|
|
443
|
-
return run(async () => {
|
|
494
|
+
return run(method === "DELETE" ? "remove" : "change-quantity", async () => {
|
|
444
495
|
await hydrate();
|
|
445
496
|
if (!isActive()) return;
|
|
446
497
|
if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new Error("This item is no longer in the cart.");
|
|
@@ -453,10 +504,20 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
453
504
|
if (!isActive()) return;
|
|
454
505
|
commit(result.cart);
|
|
455
506
|
setAnnouncement(method === "DELETE" ? "Item removed from cart." : "Cart quantity updated.");
|
|
507
|
+
emit(method === "DELETE" ? {
|
|
508
|
+
type: "item-removed",
|
|
509
|
+
itemId,
|
|
510
|
+
isSample: false
|
|
511
|
+
} : {
|
|
512
|
+
type: "quantity-changed",
|
|
513
|
+
itemId,
|
|
514
|
+
quantity: quantity ?? 0,
|
|
515
|
+
isSample: false
|
|
516
|
+
});
|
|
456
517
|
});
|
|
457
518
|
}
|
|
458
519
|
function applyDiscount(code) {
|
|
459
|
-
return run(async () => {
|
|
520
|
+
return run("discount", async () => {
|
|
460
521
|
await hydrate();
|
|
461
522
|
if (!isActive()) return;
|
|
462
523
|
if (!id.current || !code.trim()) throw new Error("Add an item and enter a promotion code.");
|
|
@@ -469,10 +530,14 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
469
530
|
if (!isActive()) return;
|
|
470
531
|
commit(result.cart);
|
|
471
532
|
setAnnouncement("Promotion codes updated.");
|
|
533
|
+
emit({
|
|
534
|
+
type: "discount-applied",
|
|
535
|
+
code: code.trim()
|
|
536
|
+
});
|
|
472
537
|
});
|
|
473
538
|
}
|
|
474
539
|
function checkout() {
|
|
475
|
-
return run(async () => {
|
|
540
|
+
return run("checkout", async () => {
|
|
476
541
|
if (!config.checkoutSuccessPath) throw new Error("Checkout has not been enabled for this store.");
|
|
477
542
|
await hydrate();
|
|
478
543
|
if (!isActive()) return;
|
|
@@ -492,6 +557,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
492
557
|
if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new Error("Commerce did not return a checkout URL.");
|
|
493
558
|
const url = new URL(session$1.url, origin);
|
|
494
559
|
if (url.protocol !== "https:") throw new Error("Commerce returned an invalid checkout URL.");
|
|
560
|
+
emit({
|
|
561
|
+
type: "checkout-started",
|
|
562
|
+
cartId: id.current
|
|
563
|
+
});
|
|
495
564
|
window.location.assign(url.href);
|
|
496
565
|
});
|
|
497
566
|
}
|
|
@@ -502,9 +571,11 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
502
571
|
connectionError,
|
|
503
572
|
retryConnection,
|
|
504
573
|
theme,
|
|
574
|
+
locale,
|
|
505
575
|
cart,
|
|
506
576
|
open,
|
|
507
577
|
setOpen: (next) => {
|
|
578
|
+
if (next && drawers.current === 0) return;
|
|
508
579
|
if (next) rememberOpener();
|
|
509
580
|
setOpen(next);
|
|
510
581
|
},
|
|
@@ -523,15 +594,25 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
523
594
|
refresh,
|
|
524
595
|
checkout
|
|
525
596
|
},
|
|
526
|
-
children: [/* @__PURE__ */ jsx(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
597
|
+
children: [/* @__PURE__ */ jsx(CartDrawerRegistry.Provider, {
|
|
598
|
+
value: registerDrawer,
|
|
599
|
+
children: /* @__PURE__ */ jsx(SampleCartProvider, {
|
|
600
|
+
enabled: connection === "unbound",
|
|
601
|
+
announce: setAnnouncement,
|
|
602
|
+
emit,
|
|
603
|
+
onAdd: (name) => {
|
|
604
|
+
setAnnouncement(`${name} added to your sample cart.`);
|
|
605
|
+
openDrawer();
|
|
606
|
+
emit({
|
|
607
|
+
type: "item-added",
|
|
608
|
+
skuId: null,
|
|
609
|
+
name,
|
|
610
|
+
quantity: 1,
|
|
611
|
+
isSample: true
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
children
|
|
615
|
+
})
|
|
535
616
|
}), /* @__PURE__ */ jsx("span", {
|
|
536
617
|
className: "commerce-sr-only",
|
|
537
618
|
role: "status",
|
|
@@ -541,6 +622,171 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
541
622
|
});
|
|
542
623
|
}
|
|
543
624
|
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/cart-state.ts
|
|
627
|
+
/** Read the cart from anywhere inside `CommerceProvider`, for example to build a custom cart button. */
|
|
628
|
+
function useCart() {
|
|
629
|
+
const { cart, config, connection, hydrating } = useCommerce();
|
|
630
|
+
const sample = useSampleCart();
|
|
631
|
+
const isSample = connection === "unbound";
|
|
632
|
+
const sampleItems$1 = sample.items;
|
|
633
|
+
return useMemo(() => {
|
|
634
|
+
if (isSample) return {
|
|
635
|
+
items: sampleItems$1.map((item) => ({
|
|
636
|
+
id: item.key,
|
|
637
|
+
name: item.name,
|
|
638
|
+
quantity: item.quantity,
|
|
639
|
+
imageUrl: null,
|
|
640
|
+
options: [],
|
|
641
|
+
subtotal: item.price * item.quantity,
|
|
642
|
+
currency: "USD"
|
|
643
|
+
})),
|
|
644
|
+
itemCount: sampleItems$1.reduce((total, item) => total + item.quantity, 0),
|
|
645
|
+
subtotal: sampleItems$1.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
646
|
+
currency: "USD",
|
|
647
|
+
isSample,
|
|
648
|
+
loading: false
|
|
649
|
+
};
|
|
650
|
+
const currency = cart?.totals?.total?.currencyCode ?? config.currencyCode;
|
|
651
|
+
const lines = cart?.lineItems ?? [];
|
|
652
|
+
const subtotal = cart?.totals?.subTotal?.value;
|
|
653
|
+
return {
|
|
654
|
+
items: lines.map((item) => ({
|
|
655
|
+
id: item.id ?? "",
|
|
656
|
+
name: item.name ?? "",
|
|
657
|
+
quantity: item.quantity ?? 0,
|
|
658
|
+
imageUrl: item.details?.productAssetUrl || null,
|
|
659
|
+
options: (item.details?.selectedOptions ?? []).map((option) => ({
|
|
660
|
+
attribute: option.attribute ?? "",
|
|
661
|
+
values: option.values ?? []
|
|
662
|
+
})),
|
|
663
|
+
subtotal: typeof item.totals?.subTotal?.value === "number" ? item.totals.subTotal.value : null,
|
|
664
|
+
currency: item.totals?.subTotal?.currencyCode ?? currency
|
|
665
|
+
})),
|
|
666
|
+
itemCount: lines.reduce((total, item) => total + (item.quantity ?? 0), 0),
|
|
667
|
+
subtotal: typeof subtotal === "number" ? subtotal : null,
|
|
668
|
+
currency: cart?.totals?.subTotal?.currencyCode ?? currency,
|
|
669
|
+
isSample,
|
|
670
|
+
loading: hydrating
|
|
671
|
+
};
|
|
672
|
+
}, [
|
|
673
|
+
cart,
|
|
674
|
+
config.currencyCode,
|
|
675
|
+
hydrating,
|
|
676
|
+
isSample,
|
|
677
|
+
sampleItems$1
|
|
678
|
+
]);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region src/catalog-model.ts
|
|
683
|
+
function getSingleMatchedSku(skuGroup) {
|
|
684
|
+
const connection = skuGroup?.skus;
|
|
685
|
+
if (connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? 0) > 1) return null;
|
|
686
|
+
const edges = connection?.edges ?? [];
|
|
687
|
+
const sku = edges.length === 1 ? edges[0]?.node : null;
|
|
688
|
+
return sku?.id ? sku : null;
|
|
689
|
+
}
|
|
690
|
+
function getLabeledSkuOptions(skuGroup) {
|
|
691
|
+
if (getProductAttributes(skuGroup).length > 0) return [];
|
|
692
|
+
const connection = skuGroup?.skus;
|
|
693
|
+
const edges = connection?.edges ?? [];
|
|
694
|
+
if (edges.length < 2 || connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? edges.length) !== edges.length) return [];
|
|
695
|
+
const skus = edges.flatMap((edge) => edge?.node?.id && (edge.node.label?.trim() || edge.node.name?.trim()) ? [edge.node] : []);
|
|
696
|
+
const labels = new Set(skus.map((sku) => (sku.label?.trim() || sku.name?.trim() || "").toLowerCase()));
|
|
697
|
+
const ids = new Set(skus.map((sku) => sku.id));
|
|
698
|
+
return skus.length === edges.length && labels.size === skus.length && ids.size === skus.length ? skus : [];
|
|
699
|
+
}
|
|
700
|
+
function getAvailableInventoryQuantity(item) {
|
|
701
|
+
const edges = item?.inventoryCounts?.edges;
|
|
702
|
+
if (!edges || edges.length === 0) return null;
|
|
703
|
+
return edges.find((edge) => edge?.node?.type === "AVAILABLE")?.node?.quantity ?? 0;
|
|
704
|
+
}
|
|
705
|
+
const imageMediaTypes = new Set(["IMAGE", "PHOTO"]);
|
|
706
|
+
function getImageUrls(item) {
|
|
707
|
+
return item?.mediaObjects?.edges?.filter((edge) => imageMediaTypes.has(edge?.node?.type ?? "") && edge?.node?.url).map((edge) => edge?.node?.url).filter((url) => Boolean(url)) ?? [];
|
|
708
|
+
}
|
|
709
|
+
function getPrimaryImageUrl(item) {
|
|
710
|
+
return getImageUrls(item)[0] ?? null;
|
|
711
|
+
}
|
|
712
|
+
function getProductAttributes(skuGroup) {
|
|
713
|
+
return skuGroup?.attributes?.edges?.map((edge) => {
|
|
714
|
+
const attributeNode = edge?.node;
|
|
715
|
+
const values = attributeNode?.values?.edges?.map((valueEdge) => {
|
|
716
|
+
const valueNode = valueEdge?.node;
|
|
717
|
+
return {
|
|
718
|
+
id: valueNode?.id || "",
|
|
719
|
+
name: valueNode?.name || "",
|
|
720
|
+
label: valueNode?.label || valueNode?.name || ""
|
|
721
|
+
};
|
|
722
|
+
}) ?? [];
|
|
723
|
+
return {
|
|
724
|
+
id: attributeNode?.id || "",
|
|
725
|
+
name: attributeNode?.name || "",
|
|
726
|
+
label: attributeNode?.label || attributeNode?.name || "",
|
|
727
|
+
values
|
|
728
|
+
};
|
|
729
|
+
}) ?? [];
|
|
730
|
+
}
|
|
731
|
+
/** The product's display name: its label, then its name. */
|
|
732
|
+
function getProductName(skuGroup) {
|
|
733
|
+
return skuGroup.label ?? skuGroup.name ?? "Product";
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/part.ts
|
|
738
|
+
/**
|
|
739
|
+
* Whether `children` renders anything, so `{custom && <Part />}` with `custom` false falls back to the default. Blank
|
|
740
|
+
* text and fragments holding nothing count as nothing. A component element counts as content: what it renders is
|
|
741
|
+
* not known until it renders.
|
|
742
|
+
*/
|
|
743
|
+
const hasChildren = (children) => Children.toArray(children).some((child) => {
|
|
744
|
+
if (typeof child === "string") return child.trim() !== "";
|
|
745
|
+
if (isValidElement(child) && child.type === Fragment) return hasChildren(child.props.children);
|
|
746
|
+
return true;
|
|
747
|
+
});
|
|
748
|
+
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
749
|
+
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
750
|
+
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
751
|
+
/**
|
|
752
|
+
* Props for a part's outer element: `partClass` plus `data-commerce-part`, the part's name in kebab case, such as
|
|
753
|
+
* `cart-checkout`. The attribute is a stable styling hook; internal utility classes are not.
|
|
754
|
+
*/
|
|
755
|
+
const partProps = (name, ...classes) => ({
|
|
756
|
+
className: partClass(...classes),
|
|
757
|
+
"data-commerce-part": name
|
|
758
|
+
});
|
|
759
|
+
/** The host's label for `state`, or `fallback` when the host gave none or it renders nothing. */
|
|
760
|
+
function renderLabel(label, state, fallback) {
|
|
761
|
+
if (typeof label === "function") {
|
|
762
|
+
const content = label(state);
|
|
763
|
+
return hasChildren(content) ? content : fallback;
|
|
764
|
+
}
|
|
765
|
+
return hasChildren(label) ? label : fallback;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
|
|
769
|
+
* children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
|
|
770
|
+
*/
|
|
771
|
+
function usePartRegistry(onChange) {
|
|
772
|
+
const counts = useRef(/* @__PURE__ */ new Map());
|
|
773
|
+
const has = useCallback((key) => counts.current.has(key), []);
|
|
774
|
+
const register = useCallback((key) => {
|
|
775
|
+
counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
|
|
776
|
+
onChange?.(has);
|
|
777
|
+
return () => {
|
|
778
|
+
const count = (counts.current.get(key) ?? 1) - 1;
|
|
779
|
+
if (count > 0) counts.current.set(key, count);
|
|
780
|
+
else counts.current.delete(key);
|
|
781
|
+
onChange?.(has);
|
|
782
|
+
};
|
|
783
|
+
}, [has, onChange]);
|
|
784
|
+
return useMemo(() => ({
|
|
785
|
+
register,
|
|
786
|
+
has
|
|
787
|
+
}), [register, has]);
|
|
788
|
+
}
|
|
789
|
+
|
|
544
790
|
//#endregion
|
|
545
791
|
//#region src/storefront-surface.tsx
|
|
546
792
|
function StorefrontSurface({ children, className = "" }) {
|
|
@@ -575,31 +821,52 @@ function CommerceStatus() {
|
|
|
575
821
|
//#region src/cart.tsx
|
|
576
822
|
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
823
|
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
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
const
|
|
824
|
+
/** Opens the cart drawer and shows the item count. */
|
|
825
|
+
function CartButton({ children } = {}) {
|
|
826
|
+
const { setOpen, open, connection } = useCommerce();
|
|
827
|
+
const { itemCount } = useCart();
|
|
582
828
|
return /* @__PURE__ */ jsx(StorefrontSurface, {
|
|
583
829
|
className: "commerce-inline",
|
|
584
|
-
children: /* @__PURE__ */
|
|
830
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
585
831
|
type: "button",
|
|
586
832
|
className: buttonClass,
|
|
587
833
|
disabled: connection !== "ready" && connection !== "unbound",
|
|
588
834
|
"aria-haspopup": "dialog",
|
|
589
835
|
"aria-expanded": open,
|
|
836
|
+
"data-commerce-part": "cart-button",
|
|
590
837
|
onClick: () => setOpen(true),
|
|
591
|
-
children:
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
838
|
+
children: renderLabel(children, { itemCount }, /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
839
|
+
"Cart",
|
|
840
|
+
" ",
|
|
841
|
+
/* @__PURE__ */ jsx("span", {
|
|
842
|
+
className: "rounded-full bg-white px-2 py-0.5 text-xs text-neutral-900",
|
|
843
|
+
children: itemCount
|
|
844
|
+
})
|
|
845
|
+
] }))
|
|
595
846
|
})
|
|
596
847
|
});
|
|
597
848
|
}
|
|
598
|
-
|
|
849
|
+
/** Whether tracked stock is used up. Oversold SKUs report a negative count. */
|
|
850
|
+
const isSoldOut = (available) => available !== null && available <= 0;
|
|
851
|
+
/** Why `quantity` cannot be added, or `null` when it can. `available` is `null` when stock is not tracked. */
|
|
852
|
+
function quantityProblem(quantity, available) {
|
|
853
|
+
if (!Number.isInteger(quantity) || quantity < 1) return "Enter a whole number of 1 or more.";
|
|
854
|
+
if (available !== null && available > 0 && quantity > available) return `Only ${available} available.`;
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
function AddToCartButton({ sku, name, quantity = 1, children }) {
|
|
599
858
|
const { addItem, pending, hydrating, error, open, connection } = useCommerce();
|
|
600
859
|
const [adding, setAdding] = useState(false);
|
|
860
|
+
const reasonId = useId();
|
|
601
861
|
const available = getAvailableInventoryQuantity(sku);
|
|
602
|
-
const
|
|
862
|
+
const soldOut = isSoldOut(available);
|
|
863
|
+
let reason = null;
|
|
864
|
+
if (connection === "unbound") reason = "Connect a store to add items to the cart.";
|
|
865
|
+
else if (connection === "error") reason = "The store could not be reached.";
|
|
866
|
+
else if (connection !== "ready") reason = "Connecting to the store…";
|
|
867
|
+
else if (!sku.id) reason = "This item cannot be added to the cart.";
|
|
868
|
+
else if (!soldOut) reason = hydrating ? "Loading your cart…" : quantityProblem(quantity, available);
|
|
869
|
+
const disabled = adding || soldOut || reason !== null;
|
|
603
870
|
async function handleAdd() {
|
|
604
871
|
if (disabled || pending || !sku.id) return;
|
|
605
872
|
setAdding(true);
|
|
@@ -615,282 +882,527 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
|
615
882
|
}
|
|
616
883
|
return /* @__PURE__ */ jsxs(StorefrontSurface, {
|
|
617
884
|
className: "commerce-inline",
|
|
618
|
-
children: [
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
885
|
+
children: [
|
|
886
|
+
/* @__PURE__ */ jsx("button", {
|
|
887
|
+
type: "button",
|
|
888
|
+
className: `${buttonClass} w-full`,
|
|
889
|
+
disabled,
|
|
890
|
+
"aria-disabled": disabled || pending,
|
|
891
|
+
"aria-busy": adding,
|
|
892
|
+
"aria-describedby": reason ? reasonId : void 0,
|
|
893
|
+
"data-commerce-part": "add-to-cart-button",
|
|
894
|
+
onClick: () => void handleAdd(),
|
|
895
|
+
children: renderLabel(children, {
|
|
896
|
+
busy: adding,
|
|
897
|
+
soldOut
|
|
898
|
+
}, soldOut ? "Out of stock" : adding ? "Adding…" : "Add to cart")
|
|
899
|
+
}),
|
|
900
|
+
reason && /* @__PURE__ */ jsx("span", {
|
|
901
|
+
id: reasonId,
|
|
902
|
+
hidden: true,
|
|
903
|
+
children: reason
|
|
904
|
+
}),
|
|
905
|
+
error && !open && /* @__PURE__ */ jsx("p", {
|
|
906
|
+
role: "alert",
|
|
907
|
+
className: "mt-3 bg-white text-sm text-red-700",
|
|
908
|
+
children: error
|
|
909
|
+
})
|
|
910
|
+
]
|
|
631
911
|
});
|
|
632
912
|
}
|
|
633
|
-
|
|
634
|
-
|
|
913
|
+
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/cart-parts.tsx
|
|
916
|
+
const CartContext = createContext(null);
|
|
917
|
+
const CartDrawerContext = createContext(null);
|
|
918
|
+
function useCartInternal(part) {
|
|
919
|
+
const context = useContext(CartContext);
|
|
920
|
+
if (!context) throw new Error(`${part} must be rendered inside <CartRoot> or <CartDrawerRoot>.`);
|
|
921
|
+
return context;
|
|
922
|
+
}
|
|
923
|
+
function CartProvider({ children }) {
|
|
924
|
+
const commerce = useCommerce();
|
|
635
925
|
const sample = useSampleCart();
|
|
926
|
+
const state = useCart();
|
|
927
|
+
const [action, setAction] = useState(null);
|
|
928
|
+
const locked = commerce.pending || commerce.hydrating || action !== null;
|
|
929
|
+
const value = {
|
|
930
|
+
...state,
|
|
931
|
+
hydrating: commerce.hydrating,
|
|
932
|
+
error: commerce.error,
|
|
933
|
+
storageWarning: commerce.storageWarning,
|
|
934
|
+
catalogPath: commerce.config.catalogPath,
|
|
935
|
+
checkoutEnabled: !!commerce.config.checkoutSuccessPath,
|
|
936
|
+
checkoutReady: commerce.cart?.totals?.total?.value != null,
|
|
937
|
+
action,
|
|
938
|
+
locked,
|
|
939
|
+
run: (next, operation) => {
|
|
940
|
+
if (locked) return;
|
|
941
|
+
setAction(next);
|
|
942
|
+
operation().finally(() => setAction(null));
|
|
943
|
+
},
|
|
944
|
+
changeQuantity: state.isSample ? sample.changeQuantity : commerce.changeQuantity,
|
|
945
|
+
removeItem: state.isSample ? sample.remove : commerce.removeItem,
|
|
946
|
+
refresh: commerce.refresh,
|
|
947
|
+
checkout: commerce.checkout,
|
|
948
|
+
close: () => commerce.setOpen(false)
|
|
949
|
+
};
|
|
950
|
+
return /* @__PURE__ */ jsx(CartContext.Provider, {
|
|
951
|
+
value,
|
|
952
|
+
children
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Provides the cart to cart parts on a page, such as a `/cart` route. While no store is connected it shows the sample
|
|
957
|
+
* cart. Use `CartDrawerRoot` for the drawer.
|
|
958
|
+
*/
|
|
959
|
+
function CartRoot({ children, className }) {
|
|
960
|
+
const { connection } = useCommerce();
|
|
636
961
|
const isSample = connection === "unbound";
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const
|
|
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
|
-
}
|
|
962
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" || isSample ? /* @__PURE__ */ jsx(CartProvider, { children: /* @__PURE__ */ jsx("section", {
|
|
963
|
+
...partProps("cart-root", className),
|
|
964
|
+
"data-commerce-source": isSample ? "sample" : void 0,
|
|
965
|
+
children
|
|
966
|
+
}) }) : /* @__PURE__ */ jsx("div", {
|
|
967
|
+
...partProps("cart-root", className),
|
|
968
|
+
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
969
|
+
}) });
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* The cart dialog. Opens when an item is added or a `CartButton` is pressed, and returns focus when it closes.
|
|
973
|
+
* The panel sits at the right edge at full height; `mr-auto` on it moves it to the left. Mount one per provider,
|
|
974
|
+
* in place of `CartDrawer`.
|
|
975
|
+
*/
|
|
976
|
+
function CartDrawerRoot({ children, className }) {
|
|
977
|
+
const { open, setOpen, restoreFocus, connection } = useCommerce();
|
|
978
|
+
const registerDrawer = useContext(CartDrawerRegistry);
|
|
979
|
+
useEffect(() => registerDrawer?.(), [registerDrawer]);
|
|
980
|
+
const [described, setDescribed] = useState(false);
|
|
981
|
+
const { register, has } = usePartRegistry(useCallback((registered) => setDescribed(registered("description")), []));
|
|
982
|
+
useEffect(() => {
|
|
983
|
+
setDescribed(has("description"));
|
|
984
|
+
}, [has]);
|
|
985
|
+
const drawer = useMemo(() => ({ register }), [register]);
|
|
671
986
|
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
672
987
|
open,
|
|
673
988
|
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__ */
|
|
675
|
-
"
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
989
|
+
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", {
|
|
990
|
+
className: "pointer-events-none fixed inset-0 z-50 flex justify-end",
|
|
991
|
+
children: /* @__PURE__ */ jsx(Dialog.Content, {
|
|
992
|
+
"data-commerce-source": connection === "unbound" ? "sample" : void 0,
|
|
993
|
+
...described ? {} : { "aria-describedby": void 0 },
|
|
994
|
+
...partProps("cart-drawer-root", "pointer-events-auto", className),
|
|
995
|
+
onCloseAutoFocus: (event) => {
|
|
996
|
+
event.preventDefault();
|
|
997
|
+
restoreFocus();
|
|
998
|
+
},
|
|
999
|
+
children: /* @__PURE__ */ jsx(CartDrawerContext.Provider, {
|
|
1000
|
+
value: drawer,
|
|
1001
|
+
children: /* @__PURE__ */ jsx(CartProvider, { children })
|
|
1002
|
+
})
|
|
1003
|
+
})
|
|
1004
|
+
})] }) })
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
/** "Your cart", or "Sample cart" before a store is connected, as an H2. In the drawer it names the dialog. */
|
|
1008
|
+
function CartTitle({ className, children }) {
|
|
1009
|
+
const { isSample } = useCartInternal("CartTitle");
|
|
1010
|
+
const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
|
|
1011
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1012
|
+
...partProps("cart-title", className),
|
|
1013
|
+
children: /* @__PURE__ */ jsx(Heading, {
|
|
1014
|
+
className: "text-2xl font-semibold",
|
|
1015
|
+
children: renderLabel(children, { isSample }, isSample ? "Sample cart" : "Your cart")
|
|
1016
|
+
})
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
/** One line about the cart. In the drawer it describes the dialog. */
|
|
1020
|
+
function CartDescription({ className, children }) {
|
|
1021
|
+
const { isSample } = useCartInternal("CartDescription");
|
|
1022
|
+
const drawer = useContext(CartDrawerContext);
|
|
1023
|
+
const register = drawer?.register;
|
|
1024
|
+
useEffect(() => register?.("description"), [register]);
|
|
1025
|
+
const text = renderLabel(children, { isSample }, isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout.");
|
|
1026
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1027
|
+
...partProps("cart-description", className),
|
|
1028
|
+
children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
|
|
1029
|
+
className: "text-sm text-neutral-600",
|
|
1030
|
+
children: text
|
|
1031
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
1032
|
+
className: "text-sm text-neutral-600",
|
|
1033
|
+
children: text
|
|
1034
|
+
})
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
/** The drawer's close button. Renders nothing outside the drawer. */
|
|
1038
|
+
function CartClose({ className }) {
|
|
1039
|
+
useCartInternal("CartClose");
|
|
1040
|
+
if (!useContext(CartDrawerContext)) return null;
|
|
1041
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1042
|
+
...partProps("cart-close", className),
|
|
1043
|
+
children: /* @__PURE__ */ jsx(Dialog.Close, {
|
|
1044
|
+
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",
|
|
1045
|
+
"aria-label": "Close cart",
|
|
1046
|
+
children: "✕"
|
|
1047
|
+
})
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
/** A storage warning, and a cart error with a refresh button. Renders nothing when there are neither. */
|
|
1051
|
+
function CartNotices({ className }) {
|
|
1052
|
+
const { storageWarning, error, action, locked, run, refresh } = useCartInternal("CartNotices");
|
|
1053
|
+
if (!storageWarning && !error) return null;
|
|
1054
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1055
|
+
...partProps("cart-notices", className),
|
|
1056
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1057
|
+
className: "flex flex-col gap-4",
|
|
1058
|
+
children: [storageWarning && /* @__PURE__ */ jsx("p", {
|
|
1059
|
+
role: "status",
|
|
1060
|
+
className: "text-sm text-amber-800",
|
|
1061
|
+
children: storageWarning
|
|
1062
|
+
}), error && /* @__PURE__ */ jsxs("div", {
|
|
1063
|
+
role: "alert",
|
|
1064
|
+
className: "rounded-lg bg-red-50 p-4 text-red-800",
|
|
1065
|
+
children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
|
|
1066
|
+
type: "button",
|
|
1067
|
+
className: "mt-2 min-h-11 bg-red-50 text-red-800 underline disabled:cursor-not-allowed",
|
|
1068
|
+
disabled: action === "refresh",
|
|
1069
|
+
"aria-disabled": locked,
|
|
1070
|
+
"aria-busy": action === "refresh",
|
|
1071
|
+
onClick: () => run("refresh", refresh),
|
|
1072
|
+
children: "Refresh cart"
|
|
1073
|
+
})]
|
|
1074
|
+
})]
|
|
1075
|
+
})
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
const CartItemsContext = createContext(null);
|
|
1079
|
+
const CartItemsRegistry = createContext(null);
|
|
1080
|
+
const CartItemContext = createContext(null);
|
|
1081
|
+
function useCartItemInternal(part) {
|
|
1082
|
+
const item = useContext(CartItemContext);
|
|
1083
|
+
if (!item) throw new Error(`${part} must be rendered inside <CartItemRoot>.`);
|
|
1084
|
+
return item;
|
|
1085
|
+
}
|
|
1086
|
+
/** Read the line from inside `CartItemRoot`, for custom item parts. */
|
|
1087
|
+
function useCartItem() {
|
|
1088
|
+
return useCartItemInternal("useCartItem");
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* The cart lines as a list. `className` goes on the list element whose children are the items. While the saved cart
|
|
1092
|
+
* loads, a status message renders just before it.
|
|
1093
|
+
*/
|
|
1094
|
+
function CartItems({ className, children }) {
|
|
1095
|
+
const { items, hydrating } = useCartInternal("CartItems");
|
|
1096
|
+
const template = hasChildren(children) ? children : packagedItem;
|
|
1097
|
+
const registry = usePartRegistry();
|
|
1098
|
+
const { has } = registry;
|
|
1099
|
+
useEffect(() => {
|
|
1100
|
+
if (process.env.NODE_ENV === "production" || items.length === 0 || has("item")) return;
|
|
1101
|
+
console.warn("[gd-commerce-storefront] The <CartItems> template renders no <CartItemRoot>, so the cart list has no items. Wrap each line in <CartItemRoot>.");
|
|
1102
|
+
}, [items.length, has]);
|
|
1103
|
+
return /* @__PURE__ */ jsxs(CartItemsRegistry.Provider, {
|
|
1104
|
+
value: registry.register,
|
|
1105
|
+
children: [hydrating && /* @__PURE__ */ jsx("p", {
|
|
1106
|
+
role: "status",
|
|
1107
|
+
children: "Loading your cart…"
|
|
1108
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1109
|
+
role: "list",
|
|
1110
|
+
...partProps("cart-items", className),
|
|
1111
|
+
children: items.map((item, index) => /* @__PURE__ */ jsx(CartItemsContext.Provider, {
|
|
1112
|
+
value: item,
|
|
1113
|
+
children: template
|
|
1114
|
+
}, item.id || `line-${index}`))
|
|
1115
|
+
})]
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
/** One cart line. Render it as the child of `CartItems`, which renders it once for every line. */
|
|
1119
|
+
function CartItemRoot({ children, className }) {
|
|
1120
|
+
const item = useContext(CartItemsContext);
|
|
1121
|
+
const register = useContext(CartItemsRegistry);
|
|
1122
|
+
useEffect(() => register?.("item"), [register]);
|
|
1123
|
+
if (!item) throw new Error("CartItemRoot must be rendered inside <CartItems>.");
|
|
1124
|
+
return /* @__PURE__ */ jsx(CartItemContext.Provider, {
|
|
1125
|
+
value: item,
|
|
1126
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1127
|
+
role: "listitem",
|
|
1128
|
+
...partProps("cart-item-root", className),
|
|
1129
|
+
"data-testid": "cart-item",
|
|
1130
|
+
children: /* @__PURE__ */ jsx(CartItemsContext.Provider, {
|
|
1131
|
+
value: null,
|
|
1132
|
+
children
|
|
1133
|
+
})
|
|
1134
|
+
})
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
/** The line's product image. Renders nothing without one. */
|
|
1138
|
+
function CartItemImage({ className }) {
|
|
1139
|
+
const { imageUrl } = useCartItemInternal("CartItemImage");
|
|
1140
|
+
if (!imageUrl) return null;
|
|
1141
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1142
|
+
...partProps("cart-item-image", className),
|
|
1143
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
1144
|
+
src: imageUrl,
|
|
1145
|
+
alt: "",
|
|
1146
|
+
className: "h-20 w-20 rounded-lg bg-neutral-100 object-cover"
|
|
1147
|
+
})
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
/** The product name as an H3. */
|
|
1151
|
+
function CartItemName({ className }) {
|
|
1152
|
+
const { name } = useCartItemInternal("CartItemName");
|
|
1153
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1154
|
+
...partProps("cart-item-name", className),
|
|
1155
|
+
children: /* @__PURE__ */ jsx("h3", {
|
|
1156
|
+
className: "font-semibold",
|
|
1157
|
+
children: name
|
|
1158
|
+
})
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
/** The selected options, one per line, such as "Color: Blue". Renders nothing without options. */
|
|
1162
|
+
function CartItemOptions({ className }) {
|
|
1163
|
+
const { options } = useCartItemInternal("CartItemOptions");
|
|
1164
|
+
if (!options.length) return null;
|
|
1165
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1166
|
+
...partProps("cart-item-options", className),
|
|
1167
|
+
children: options.map((option) => /* @__PURE__ */ jsxs("p", {
|
|
1168
|
+
className: "text-sm text-neutral-600",
|
|
1169
|
+
children: [
|
|
1170
|
+
option.attribute,
|
|
1171
|
+
": ",
|
|
1172
|
+
option.values.join(", ")
|
|
1173
|
+
]
|
|
1174
|
+
}, `${option.attribute}:${option.values.join(",")}`))
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
/** The line total. */
|
|
1178
|
+
function CartItemPrice({ className }) {
|
|
1179
|
+
const { subtotal, currency } = useCartItemInternal("CartItemPrice");
|
|
1180
|
+
const money$1 = useMoney();
|
|
1181
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1182
|
+
...partProps("cart-item-price", className),
|
|
1183
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1184
|
+
className: "text-sm",
|
|
1185
|
+
children: subtotal === null ? "Price unavailable" : money$1(subtotal, currency)
|
|
1186
|
+
})
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
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";
|
|
1190
|
+
/** Decrease and increase buttons around the line's quantity. */
|
|
1191
|
+
function CartItemQuantity({ className }) {
|
|
1192
|
+
const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
|
|
1193
|
+
const { action, locked, run, changeQuantity } = useCartInternal("CartItemQuantity");
|
|
1194
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1195
|
+
...partProps("cart-item-quantity", className),
|
|
1196
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1197
|
+
className: "flex items-center rounded-lg border border-neutral-300",
|
|
681
1198
|
children: [
|
|
682
|
-
/* @__PURE__ */
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
"aria-label": "Close cart",
|
|
693
|
-
children: "✕"
|
|
694
|
-
})]
|
|
1199
|
+
/* @__PURE__ */ jsx("button", {
|
|
1200
|
+
type: "button",
|
|
1201
|
+
className: stepperButtonClass,
|
|
1202
|
+
"aria-label": `Decrease quantity of ${name}`,
|
|
1203
|
+
"data-commerce-control": "decrease",
|
|
1204
|
+
disabled: action === `decrease:${id}` || !id || quantity <= 1,
|
|
1205
|
+
"aria-disabled": locked || !id || quantity <= 1,
|
|
1206
|
+
"aria-busy": action === `decrease:${id}`,
|
|
1207
|
+
onClick: () => run(`decrease:${id}`, () => changeQuantity(id, quantity - 1)),
|
|
1208
|
+
children: "−"
|
|
695
1209
|
}),
|
|
696
|
-
/* @__PURE__ */ jsxs("
|
|
697
|
-
className: "
|
|
698
|
-
children: [
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
]
|
|
1210
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1211
|
+
className: "min-w-8 text-center",
|
|
1212
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1213
|
+
className: "sr-only",
|
|
1214
|
+
children: "Quantity: "
|
|
1215
|
+
}), quantity]
|
|
819
1216
|
}),
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
]
|
|
1217
|
+
/* @__PURE__ */ jsx("button", {
|
|
1218
|
+
type: "button",
|
|
1219
|
+
className: stepperButtonClass,
|
|
1220
|
+
"aria-label": `Increase quantity of ${name}`,
|
|
1221
|
+
"data-commerce-control": "increase",
|
|
1222
|
+
disabled: action === `increase:${id}` || !id,
|
|
1223
|
+
"aria-disabled": locked || !id,
|
|
1224
|
+
"aria-busy": action === `increase:${id}`,
|
|
1225
|
+
onClick: () => run(`increase:${id}`, () => changeQuantity(id, quantity + 1)),
|
|
1226
|
+
children: "+"
|
|
850
1227
|
})
|
|
851
1228
|
]
|
|
852
|
-
})
|
|
1229
|
+
})
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
/** Removes the line. */
|
|
1233
|
+
function CartItemRemove({ className, children }) {
|
|
1234
|
+
const { id, name } = useCartItemInternal("CartItemRemove");
|
|
1235
|
+
const { action, locked, run, removeItem } = useCartInternal("CartItemRemove");
|
|
1236
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1237
|
+
...partProps("cart-item-remove", className),
|
|
1238
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
1239
|
+
type: "button",
|
|
1240
|
+
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",
|
|
1241
|
+
disabled: action === `remove:${id}` || !id,
|
|
1242
|
+
"aria-disabled": locked || !id,
|
|
1243
|
+
"aria-busy": action === `remove:${id}`,
|
|
1244
|
+
onClick: () => run(`remove:${id}`, () => removeItem(id)),
|
|
1245
|
+
children: [renderLabel(children, { busy: action === `remove:${id}` }, "Remove"), /* @__PURE__ */ jsxs("span", {
|
|
1246
|
+
className: "sr-only",
|
|
1247
|
+
children: [" ", name]
|
|
1248
|
+
})]
|
|
1249
|
+
})
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
const packagedItem = /* @__PURE__ */ jsxs(CartItemRoot, {
|
|
1253
|
+
className: "py-5 first:pt-0",
|
|
1254
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1255
|
+
className: "flex gap-4",
|
|
1256
|
+
children: [/* @__PURE__ */ jsx(CartItemImage, {}), /* @__PURE__ */ jsxs("div", {
|
|
1257
|
+
className: "min-w-0 flex-1",
|
|
1258
|
+
children: [
|
|
1259
|
+
/* @__PURE__ */ jsx(CartItemName, {}),
|
|
1260
|
+
/* @__PURE__ */ jsx(CartItemOptions, {}),
|
|
1261
|
+
/* @__PURE__ */ jsx(CartItemPrice, { className: "mt-1" })
|
|
1262
|
+
]
|
|
1263
|
+
})]
|
|
1264
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1265
|
+
className: "mt-4 flex items-center justify-between gap-2",
|
|
1266
|
+
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1267
|
+
})]
|
|
1268
|
+
});
|
|
1269
|
+
/** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
|
|
1270
|
+
function CartEmpty({ className, children }) {
|
|
1271
|
+
const { items, hydrating, isSample, catalogPath, close } = useCartInternal("CartEmpty");
|
|
1272
|
+
if (hydrating || items.length) return null;
|
|
1273
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1274
|
+
...partProps("cart-empty", className),
|
|
1275
|
+
children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1276
|
+
/* @__PURE__ */ jsx("p", {
|
|
1277
|
+
className: "text-xl font-medium",
|
|
1278
|
+
children: "Your cart is empty"
|
|
1279
|
+
}),
|
|
1280
|
+
/* @__PURE__ */ jsx("p", {
|
|
1281
|
+
className: "mt-2 text-neutral-600",
|
|
1282
|
+
children: isSample ? "Add a sample product to try the cart." : "Find something you love in the shop."
|
|
1283
|
+
}),
|
|
1284
|
+
/* @__PURE__ */ jsx(Link, {
|
|
1285
|
+
className: `${buttonClass} mt-6`,
|
|
1286
|
+
to: catalogPath,
|
|
1287
|
+
onClick: close,
|
|
1288
|
+
children: "Browse products"
|
|
1289
|
+
})
|
|
1290
|
+
] })
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
/** A container for the subtotal and checkout. Renders only when the cart has items. */
|
|
1294
|
+
function CartSummary({ className, children }) {
|
|
1295
|
+
const { items } = useCartInternal("CartSummary");
|
|
1296
|
+
if (!items.length) return null;
|
|
1297
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1298
|
+
...partProps("cart-summary", className),
|
|
1299
|
+
children
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
/** The subtotal, labeled "Sample subtotal" for samples. Renders nothing until the server prices the cart. */
|
|
1303
|
+
function CartSubtotal({ className }) {
|
|
1304
|
+
const { items, subtotal, currency, isSample } = useCartInternal("CartSubtotal");
|
|
1305
|
+
const money$1 = useMoney();
|
|
1306
|
+
if (!items.length || subtotal === null) return null;
|
|
1307
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1308
|
+
...partProps("cart-subtotal", className),
|
|
1309
|
+
children: /* @__PURE__ */ jsx("dl", {
|
|
1310
|
+
className: "space-y-2 text-sm",
|
|
1311
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1312
|
+
className: "flex justify-between",
|
|
1313
|
+
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money$1(subtotal, currency) })]
|
|
1314
|
+
})
|
|
1315
|
+
})
|
|
853
1316
|
});
|
|
854
1317
|
}
|
|
855
|
-
|
|
856
|
-
//#endregion
|
|
857
|
-
//#region src/part.ts
|
|
858
|
-
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
859
|
-
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
860
|
-
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
861
1318
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
1319
|
+
* "Shipping, taxes, and discounts are calculated at checkout." Has the stable
|
|
1320
|
+
* `commerce-cart-checkout-adjustments-note` class. Renders nothing for samples or an empty cart.
|
|
864
1321
|
*/
|
|
865
|
-
function
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1322
|
+
function CartCheckoutNote({ className }) {
|
|
1323
|
+
const { items, isSample } = useCartInternal("CartCheckoutNote");
|
|
1324
|
+
if (isSample || !items.length) return null;
|
|
1325
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1326
|
+
...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
|
|
1327
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1328
|
+
className: "text-sm text-neutral-600",
|
|
1329
|
+
children: "Shipping, taxes, and discounts are calculated at checkout."
|
|
1330
|
+
})
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* The checkout button when `checkoutSuccessPath` is set; otherwise a message that checkout is unavailable.
|
|
1335
|
+
* Samples never check out. Renders nothing for an empty cart.
|
|
1336
|
+
*/
|
|
1337
|
+
function CartCheckout({ className, children }) {
|
|
1338
|
+
const { items, isSample, checkoutEnabled, checkoutReady, hydrating, action, locked, run, checkout } = useCartInternal("CartCheckout");
|
|
1339
|
+
if (!items.length) return null;
|
|
1340
|
+
let content;
|
|
1341
|
+
if (isSample) content = /* @__PURE__ */ jsx("p", {
|
|
1342
|
+
className: "text-sm text-neutral-600",
|
|
1343
|
+
children: "Example total in USD. Checkout is not set up yet."
|
|
1344
|
+
});
|
|
1345
|
+
else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
|
|
1346
|
+
className: "text-sm text-neutral-600",
|
|
1347
|
+
children: "Checkout is not available yet."
|
|
1348
|
+
});
|
|
1349
|
+
else content = /* @__PURE__ */ jsx("button", {
|
|
1350
|
+
type: "button",
|
|
1351
|
+
className: `${buttonClass} w-full`,
|
|
1352
|
+
disabled: action === "checkout" || hydrating || !checkoutReady,
|
|
1353
|
+
"aria-disabled": locked || !checkoutReady,
|
|
1354
|
+
"aria-busy": action === "checkout",
|
|
1355
|
+
onClick: () => run("checkout", checkout),
|
|
1356
|
+
children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to Checkout")
|
|
1357
|
+
});
|
|
1358
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1359
|
+
...partProps("cart-checkout", className),
|
|
1360
|
+
children: content
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
/** The packaged cart drawer. Compose `CartDrawerRoot` and the cart parts for a custom drawer. */
|
|
1364
|
+
function CartDrawer() {
|
|
1365
|
+
return /* @__PURE__ */ jsxs(CartDrawerRoot, {
|
|
1366
|
+
className: "flex w-full max-w-lg flex-col bg-white text-neutral-900 shadow-xl",
|
|
1367
|
+
children: [
|
|
1368
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1369
|
+
className: "flex items-start justify-between border-b border-neutral-200 p-6",
|
|
1370
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(CartTitle, {}), /* @__PURE__ */ jsx(CartDescription, { className: "mt-1" })] }), /* @__PURE__ */ jsx(CartClose, {})]
|
|
1371
|
+
}),
|
|
1372
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1373
|
+
className: "flex-1 overflow-y-auto p-6",
|
|
1374
|
+
children: [
|
|
1375
|
+
/* @__PURE__ */ jsx(CartNotices, { className: "mb-4" }),
|
|
1376
|
+
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-neutral-200" }),
|
|
1377
|
+
/* @__PURE__ */ jsx(CartEmpty, { className: "py-16 text-center" })
|
|
1378
|
+
]
|
|
1379
|
+
}),
|
|
1380
|
+
/* @__PURE__ */ jsxs(CartSummary, {
|
|
1381
|
+
className: "border-t border-neutral-200 p-6",
|
|
1382
|
+
children: [
|
|
1383
|
+
/* @__PURE__ */ jsx(CartSubtotal, {}),
|
|
1384
|
+
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-3" }),
|
|
1385
|
+
/* @__PURE__ */ jsx(CartCheckout, {})
|
|
1386
|
+
]
|
|
1387
|
+
})
|
|
1388
|
+
]
|
|
1389
|
+
});
|
|
882
1390
|
}
|
|
883
1391
|
|
|
884
1392
|
//#endregion
|
|
885
1393
|
//#region src/product-card.tsx
|
|
1394
|
+
/**
|
|
1395
|
+
* A product image that fills its container, with a labeled placeholder when there is no URL or it fails to load.
|
|
1396
|
+
* Use it in custom parts so they handle missing images like the package's own.
|
|
1397
|
+
*/
|
|
886
1398
|
function ProductImage({ url, name, className = "" }) {
|
|
887
|
-
const [
|
|
888
|
-
return url &&
|
|
1399
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
1400
|
+
return url && url !== failedUrl ? /* @__PURE__ */ jsx("img", {
|
|
889
1401
|
src: url,
|
|
890
1402
|
alt: name,
|
|
891
1403
|
loading: "lazy",
|
|
892
1404
|
className: `h-full w-full object-cover ${className}`,
|
|
893
|
-
onError: () =>
|
|
1405
|
+
onError: () => setFailedUrl(url)
|
|
894
1406
|
}) : /* @__PURE__ */ jsx("div", {
|
|
895
1407
|
className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
|
|
896
1408
|
role: "img",
|
|
@@ -971,7 +1483,7 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
971
1483
|
const card = /* @__PURE__ */ jsx(ProductCardContext.Provider, {
|
|
972
1484
|
value,
|
|
973
1485
|
children: /* @__PURE__ */ jsx("article", {
|
|
974
|
-
|
|
1486
|
+
...partProps("product-card-root", "group", className),
|
|
975
1487
|
children: /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
976
1488
|
value: null,
|
|
977
1489
|
children
|
|
@@ -987,7 +1499,7 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
987
1499
|
function ProductCardImage({ className }) {
|
|
988
1500
|
const card = useProductCardInternal("ProductCardImage");
|
|
989
1501
|
return /* @__PURE__ */ jsx("div", {
|
|
990
|
-
|
|
1502
|
+
...partProps("product-card-image", className),
|
|
991
1503
|
children: card.source === "live" ? /* @__PURE__ */ jsx(Link, {
|
|
992
1504
|
to: card.href,
|
|
993
1505
|
tabIndex: card.imageLinked ? void 0 : -1,
|
|
@@ -1010,7 +1522,7 @@ function ProductCardTitle({ className }) {
|
|
|
1010
1522
|
const { name, href, register } = useProductCardInternal("ProductCardTitle");
|
|
1011
1523
|
useEffect(() => href ? register(titleLink) : void 0, [href, register]);
|
|
1012
1524
|
return /* @__PURE__ */ jsx("div", {
|
|
1013
|
-
|
|
1525
|
+
...partProps("product-card-title", className),
|
|
1014
1526
|
children: /* @__PURE__ */ jsx("h2", {
|
|
1015
1527
|
className: "break-words text-2xl font-semibold leading-tight",
|
|
1016
1528
|
children: href ? /* @__PURE__ */ jsx(Link, {
|
|
@@ -1026,7 +1538,7 @@ function ProductCardDescription({ className }) {
|
|
|
1026
1538
|
const { product } = useProductCardInternal("ProductCardDescription");
|
|
1027
1539
|
if (!product?.description) return null;
|
|
1028
1540
|
return /* @__PURE__ */ jsx("div", {
|
|
1029
|
-
|
|
1541
|
+
...partProps("product-card-description", className),
|
|
1030
1542
|
children: /* @__PURE__ */ jsx("p", {
|
|
1031
1543
|
className: "line-clamp-3 text-sm leading-6",
|
|
1032
1544
|
children: product.description
|
|
@@ -1036,16 +1548,16 @@ function ProductCardDescription({ className }) {
|
|
|
1036
1548
|
/** The product's price, or its price range when variants differ. */
|
|
1037
1549
|
function ProductCardPrice({ className }) {
|
|
1038
1550
|
const card = useProductCardInternal("ProductCardPrice");
|
|
1039
|
-
const
|
|
1551
|
+
const money$1 = useMoney();
|
|
1040
1552
|
let text;
|
|
1041
|
-
if (card.source === "sample") text = money(card.sample.price, "USD");
|
|
1553
|
+
if (card.source === "sample") text = money$1(card.sample.price, "USD");
|
|
1042
1554
|
else {
|
|
1043
1555
|
const min = card.product.priceRange?.min;
|
|
1044
1556
|
const max = card.product.priceRange?.max;
|
|
1045
|
-
text = typeof min === "number" ? `${money(min
|
|
1557
|
+
text = typeof min === "number" ? `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}` : "Price unavailable";
|
|
1046
1558
|
}
|
|
1047
1559
|
return /* @__PURE__ */ jsx("div", {
|
|
1048
|
-
|
|
1560
|
+
...partProps("product-card-price", className),
|
|
1049
1561
|
children: /* @__PURE__ */ jsx("p", {
|
|
1050
1562
|
className: "text-lg font-semibold leading-7",
|
|
1051
1563
|
children: text
|
|
@@ -1056,44 +1568,73 @@ function ProductCardPrice({ className }) {
|
|
|
1056
1568
|
* Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
|
|
1057
1569
|
* Samples add to the sample cart.
|
|
1058
1570
|
*/
|
|
1059
|
-
function ProductCardAction({ className }) {
|
|
1571
|
+
function ProductCardAction({ className, children }) {
|
|
1060
1572
|
const card = useProductCardInternal("ProductCardAction");
|
|
1061
1573
|
return /* @__PURE__ */ jsx("div", {
|
|
1062
|
-
|
|
1574
|
+
...partProps("product-card-action", className),
|
|
1063
1575
|
children: card.source === "live" ? /* @__PURE__ */ jsx(LiveAction, {
|
|
1064
1576
|
product: card.product,
|
|
1065
1577
|
name: card.name,
|
|
1066
|
-
href: card.href
|
|
1067
|
-
|
|
1578
|
+
href: card.href,
|
|
1579
|
+
label: children
|
|
1580
|
+
}) : /* @__PURE__ */ jsx(SampleAction, {
|
|
1581
|
+
sample: card.sample,
|
|
1582
|
+
label: children
|
|
1583
|
+
})
|
|
1068
1584
|
});
|
|
1069
1585
|
}
|
|
1070
|
-
function LiveAction({ product, name, href }) {
|
|
1586
|
+
function LiveAction({ product, name, href, label }) {
|
|
1071
1587
|
const selectedSku = getSingleMatchedSku(product);
|
|
1072
1588
|
const hasVariants = getProductAttributes(product).length > 0;
|
|
1073
1589
|
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCartButton, {
|
|
1074
1590
|
sku: selectedSku,
|
|
1075
|
-
name
|
|
1591
|
+
name,
|
|
1592
|
+
children: (state) => renderLabel(label, {
|
|
1593
|
+
action: "add",
|
|
1594
|
+
...state
|
|
1595
|
+
}, null)
|
|
1076
1596
|
});
|
|
1077
1597
|
return /* @__PURE__ */ jsx(Link, {
|
|
1078
1598
|
className: buttonClass,
|
|
1079
1599
|
to: href,
|
|
1080
|
-
children:
|
|
1600
|
+
children: renderLabel(label, {
|
|
1601
|
+
action: hasVariants ? "choose-options" : "view-product",
|
|
1602
|
+
busy: false,
|
|
1603
|
+
soldOut: false
|
|
1604
|
+
}, hasVariants ? "Choose options" : "View product")
|
|
1081
1605
|
});
|
|
1082
1606
|
}
|
|
1083
|
-
function SampleAction({ sample }) {
|
|
1607
|
+
function SampleAction({ sample, label }) {
|
|
1084
1608
|
const { add } = useSampleCart();
|
|
1085
|
-
|
|
1609
|
+
const custom = renderLabel(label, {
|
|
1610
|
+
action: "add",
|
|
1611
|
+
busy: false,
|
|
1612
|
+
soldOut: false
|
|
1613
|
+
}, null);
|
|
1614
|
+
return custom === null ? /* @__PURE__ */ jsx("button", {
|
|
1086
1615
|
type: "button",
|
|
1087
1616
|
className: buttonClass,
|
|
1088
1617
|
"aria-label": `Add ${sample.name} to sample cart`,
|
|
1089
1618
|
onClick: () => add(sample.key),
|
|
1090
1619
|
children: "Add to cart"
|
|
1620
|
+
}) : /* @__PURE__ */ jsxs("button", {
|
|
1621
|
+
type: "button",
|
|
1622
|
+
className: buttonClass,
|
|
1623
|
+
onClick: () => add(sample.key),
|
|
1624
|
+
children: [custom, /* @__PURE__ */ jsxs("span", {
|
|
1625
|
+
className: "sr-only",
|
|
1626
|
+
children: [
|
|
1627
|
+
" (",
|
|
1628
|
+
sample.name,
|
|
1629
|
+
", sample)"
|
|
1630
|
+
]
|
|
1631
|
+
})]
|
|
1091
1632
|
});
|
|
1092
1633
|
}
|
|
1093
1634
|
const packagedCardClass = "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200";
|
|
1094
1635
|
/** The packaged card's parts, inside a root that has `packagedCardClass`. */
|
|
1095
1636
|
function PackagedCardParts() {
|
|
1096
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsxs("div", {
|
|
1637
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsxs("div", {
|
|
1097
1638
|
className: "flex flex-1 flex-col p-6",
|
|
1098
1639
|
children: [
|
|
1099
1640
|
/* @__PURE__ */ jsx(ProductCardTitle, {}),
|
|
@@ -1117,9 +1658,22 @@ function ProductCard({ product }) {
|
|
|
1117
1658
|
});
|
|
1118
1659
|
}
|
|
1119
1660
|
|
|
1661
|
+
//#endregion
|
|
1662
|
+
//#region src/root-params.ts
|
|
1663
|
+
/**
|
|
1664
|
+
* A root's search params: the page URL's, or with `urlState` off, the root's own. A second root on the page, such
|
|
1665
|
+
* as a quick view, then neither reads nor changes the page's params, and its state ends when it unmounts.
|
|
1666
|
+
*/
|
|
1667
|
+
function useRootParams(urlState) {
|
|
1668
|
+
const [urlParams, setUrlParams] = useSearchParams();
|
|
1669
|
+
const [localParams, setLocalParams] = useState(() => new URLSearchParams());
|
|
1670
|
+
return urlState ? [urlParams, setUrlParams] : [localParams, setLocalParams];
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1120
1673
|
//#endregion
|
|
1121
1674
|
//#region src/catalog.tsx
|
|
1122
1675
|
const CatalogContext = createContext(null);
|
|
1676
|
+
const CatalogEmptyContext = createContext(null);
|
|
1123
1677
|
const sampleCatalog = { source: "sample" };
|
|
1124
1678
|
function useCatalogInternal(part) {
|
|
1125
1679
|
const context = useContext(CatalogContext);
|
|
@@ -1127,32 +1681,47 @@ function useCatalogInternal(part) {
|
|
|
1127
1681
|
return context;
|
|
1128
1682
|
}
|
|
1129
1683
|
/** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
|
|
1130
|
-
function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false }) {
|
|
1684
|
+
function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false, pageSize = 6, urlState = true }) {
|
|
1131
1685
|
const { connection } = useCommerce();
|
|
1686
|
+
const [custom, setCustom] = useState(false);
|
|
1687
|
+
const { register, has } = usePartRegistry(useCallback((registered) => setCustom(registered("empty")), []));
|
|
1688
|
+
useEffect(() => {
|
|
1689
|
+
setCustom(has("empty"));
|
|
1690
|
+
}, [has]);
|
|
1691
|
+
const empty = useMemo(() => ({
|
|
1692
|
+
register,
|
|
1693
|
+
custom
|
|
1694
|
+
}), [register, custom]);
|
|
1695
|
+
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new Error(`CatalogRoot pageSize must be a whole number of 1 or more, not ${pageSize}.`);
|
|
1132
1696
|
let content;
|
|
1133
1697
|
if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
|
|
1134
1698
|
className,
|
|
1699
|
+
pageSize,
|
|
1700
|
+
urlState,
|
|
1135
1701
|
children
|
|
1136
1702
|
});
|
|
1137
1703
|
else if (connection === "unbound" && sampleProducts$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1138
1704
|
value: sampleCatalog,
|
|
1139
1705
|
children: /* @__PURE__ */ jsx("section", {
|
|
1140
|
-
|
|
1706
|
+
...partProps("catalog-root", className),
|
|
1141
1707
|
"data-commerce-source": "sample",
|
|
1142
1708
|
children
|
|
1143
1709
|
})
|
|
1144
1710
|
});
|
|
1145
1711
|
else content = /* @__PURE__ */ jsx("div", {
|
|
1146
|
-
|
|
1712
|
+
...partProps("catalog-root", className),
|
|
1147
1713
|
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1148
1714
|
});
|
|
1149
|
-
return /* @__PURE__ */ jsx(StorefrontSurface, { children:
|
|
1715
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsx(CatalogEmptyContext.Provider, {
|
|
1716
|
+
value: empty,
|
|
1717
|
+
children: content
|
|
1718
|
+
}) });
|
|
1150
1719
|
}
|
|
1151
|
-
function CatalogLoader({ children, className }) {
|
|
1720
|
+
function CatalogLoader({ children, className, pageSize, urlState }) {
|
|
1152
1721
|
const { config } = useCommerce();
|
|
1153
|
-
const [params, setParams] =
|
|
1722
|
+
const [params, setParams] = useRootParams(urlState);
|
|
1154
1723
|
const after = params.get("after") ?? "";
|
|
1155
|
-
const urlParams = new URLSearchParams({ first:
|
|
1724
|
+
const urlParams = new URLSearchParams({ first: String(pageSize) });
|
|
1156
1725
|
if (after) urlParams.set("after", after);
|
|
1157
1726
|
const products = useQuery({
|
|
1158
1727
|
retry: false,
|
|
@@ -1160,6 +1729,7 @@ function CatalogLoader({ children, className }) {
|
|
|
1160
1729
|
"commerce",
|
|
1161
1730
|
config.cartScope,
|
|
1162
1731
|
"products",
|
|
1732
|
+
pageSize,
|
|
1163
1733
|
after
|
|
1164
1734
|
],
|
|
1165
1735
|
queryFn: ({ signal }) => request(`/products?${urlParams}`, {
|
|
@@ -1189,7 +1759,7 @@ function CatalogLoader({ children, className }) {
|
|
|
1189
1759
|
return /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1190
1760
|
value,
|
|
1191
1761
|
children: /* @__PURE__ */ jsx("section", {
|
|
1192
|
-
|
|
1762
|
+
...partProps("catalog-root", className),
|
|
1193
1763
|
children
|
|
1194
1764
|
})
|
|
1195
1765
|
});
|
|
@@ -1198,7 +1768,7 @@ function CatalogLoader({ children, className }) {
|
|
|
1198
1768
|
function CatalogHeader({ title = "Shop all products", description = "Explore the collection and find your favorites.", className }) {
|
|
1199
1769
|
useCatalogInternal("CatalogHeader");
|
|
1200
1770
|
return /* @__PURE__ */ jsxs("header", {
|
|
1201
|
-
|
|
1771
|
+
...partProps("catalog-header", className),
|
|
1202
1772
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
1203
1773
|
className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
|
|
1204
1774
|
children: title
|
|
@@ -1212,7 +1782,7 @@ function CatalogHeader({ title = "Shop all products", description = "Explore the
|
|
|
1212
1782
|
function CatalogSummary({ className }) {
|
|
1213
1783
|
const catalog = useCatalogInternal("CatalogSummary");
|
|
1214
1784
|
return /* @__PURE__ */ jsx("div", {
|
|
1215
|
-
|
|
1785
|
+
...partProps("catalog-summary", className),
|
|
1216
1786
|
children: /* @__PURE__ */ jsx("p", {
|
|
1217
1787
|
className: "text-sm",
|
|
1218
1788
|
role: "status",
|
|
@@ -1238,12 +1808,13 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
|
1238
1808
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1239
1809
|
});
|
|
1240
1810
|
/**
|
|
1241
|
-
* The product cards. `className` goes on the element that contains the cards, such as a grid.
|
|
1242
|
-
* placeholder
|
|
1811
|
+
* The product cards. `className` goes on the element that contains the cards, such as a grid. While loading, that
|
|
1812
|
+
* element holds placeholder cards. The error with its retry and, without a `CatalogEmpty`, the empty message render just before it.
|
|
1243
1813
|
*/
|
|
1244
1814
|
function CatalogProducts({ className, children }) {
|
|
1245
1815
|
const catalog = useCatalogInternal("CatalogProducts");
|
|
1246
|
-
const
|
|
1816
|
+
const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
|
|
1817
|
+
const card = hasChildren(children) ? children : packagedCard;
|
|
1247
1818
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
1248
1819
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
1249
1820
|
key: product.id ?? "",
|
|
@@ -1253,26 +1824,7 @@ function CatalogProducts({ className, children }) {
|
|
|
1253
1824
|
}
|
|
1254
1825
|
})) : sampleItems, [nodes]);
|
|
1255
1826
|
const products = catalog.source === "live" ? catalog.products : null;
|
|
1256
|
-
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
|
-
}),
|
|
1827
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1276
1828
|
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
1277
1829
|
role: "alert",
|
|
1278
1830
|
className: "rounded-lg bg-red-50 p-5 text-red-800",
|
|
@@ -1283,19 +1835,48 @@ function CatalogProducts({ className, children }) {
|
|
|
1283
1835
|
children: "Retry products"
|
|
1284
1836
|
})]
|
|
1285
1837
|
}),
|
|
1286
|
-
products?.isSuccess && !items.length && /* @__PURE__ */ jsx("p", {
|
|
1838
|
+
products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
|
|
1287
1839
|
className: "py-16 text-center",
|
|
1288
1840
|
children: "No products available."
|
|
1289
1841
|
}),
|
|
1290
1842
|
/* @__PURE__ */ jsx("div", {
|
|
1291
|
-
|
|
1292
|
-
children:
|
|
1843
|
+
...partProps("catalog-products", className),
|
|
1844
|
+
children: products?.isPending ? [
|
|
1845
|
+
0,
|
|
1846
|
+
1,
|
|
1847
|
+
2
|
|
1848
|
+
].map((placeholder) => /* @__PURE__ */ jsxs("div", {
|
|
1849
|
+
"aria-hidden": "true",
|
|
1850
|
+
className: "overflow-hidden rounded-lg border border-neutral-200 motion-safe:animate-pulse",
|
|
1851
|
+
children: [/* @__PURE__ */ jsx("div", { className: "aspect-square bg-neutral-100" }), /* @__PURE__ */ jsxs("div", {
|
|
1852
|
+
className: "space-y-4 p-6",
|
|
1853
|
+
children: [
|
|
1854
|
+
/* @__PURE__ */ jsx("div", { className: "h-6 w-2/3 rounded bg-neutral-100" }),
|
|
1855
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 rounded bg-neutral-100" }),
|
|
1856
|
+
/* @__PURE__ */ jsx("div", { className: "h-11 rounded bg-neutral-100" })
|
|
1857
|
+
]
|
|
1858
|
+
})]
|
|
1859
|
+
}, placeholder)) : items.map(({ key, item }) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1293
1860
|
value: item,
|
|
1294
1861
|
children: card
|
|
1295
1862
|
}, key))
|
|
1296
1863
|
})
|
|
1297
1864
|
] });
|
|
1298
1865
|
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Renders only when a connected store's catalog has no products: "No products available.", or its children. While
|
|
1868
|
+
* one is mounted, `CatalogProducts` leaves out its own empty message. Samples are never empty.
|
|
1869
|
+
*/
|
|
1870
|
+
function CatalogEmpty({ className, children }) {
|
|
1871
|
+
const catalog = useCatalogInternal("CatalogEmpty");
|
|
1872
|
+
const register = useContext(CatalogEmptyContext)?.register;
|
|
1873
|
+
useEffect(() => register?.("empty"), [register]);
|
|
1874
|
+
if (catalog.source === "sample" || !catalog.products.isSuccess || catalog.nodes.length) return null;
|
|
1875
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1876
|
+
...partProps("catalog-empty", className),
|
|
1877
|
+
children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "No products available." })
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1299
1880
|
/** First-page and next-page buttons. Renders nothing for samples, which have one page. */
|
|
1300
1881
|
function CatalogPagination({ className }) {
|
|
1301
1882
|
const catalog = useCatalogInternal("CatalogPagination");
|
|
@@ -1304,7 +1885,7 @@ function CatalogPagination({ className }) {
|
|
|
1304
1885
|
const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
|
|
1305
1886
|
return /* @__PURE__ */ jsx("nav", {
|
|
1306
1887
|
"aria-label": "Product pages",
|
|
1307
|
-
|
|
1888
|
+
...partProps("catalog-pagination", className),
|
|
1308
1889
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1309
1890
|
className: "flex justify-between",
|
|
1310
1891
|
children: [after ? /* @__PURE__ */ jsx("button", {
|
|
@@ -1322,9 +1903,10 @@ function CatalogPagination({ className }) {
|
|
|
1322
1903
|
});
|
|
1323
1904
|
}
|
|
1324
1905
|
/** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
|
|
1325
|
-
function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1 }) {
|
|
1906
|
+
function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1, pageSize }) {
|
|
1326
1907
|
return /* @__PURE__ */ jsxs(CatalogRoot, {
|
|
1327
1908
|
sampleProducts: sampleProducts$1,
|
|
1909
|
+
pageSize,
|
|
1328
1910
|
children: [
|
|
1329
1911
|
showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
|
|
1330
1912
|
title,
|
|
@@ -1400,26 +1982,32 @@ function useSkuOptionRegistration() {
|
|
|
1400
1982
|
const { register } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1401
1983
|
useEffect(() => register("sku"), [register]);
|
|
1402
1984
|
}
|
|
1403
|
-
/** Loads
|
|
1404
|
-
function ProductDetailsRoot({
|
|
1405
|
-
const { productId = "" } = useParams();
|
|
1985
|
+
/** Loads one product, by default the route's `:productId`, and provides state to product parts. */
|
|
1986
|
+
function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
|
|
1987
|
+
const { productId: routeProductId = "" } = useParams();
|
|
1988
|
+
const productId = productIdProp ?? routeProductId;
|
|
1406
1989
|
const { connection } = useCommerce();
|
|
1990
|
+
useEffect(() => {
|
|
1991
|
+
if (process.env.NODE_ENV === "production" || productId || connection !== "ready") return;
|
|
1992
|
+
console.warn("[gd-commerce-storefront] ProductDetailsRoot has no product to show. Pass `productId`, or render it on a route with a `:productId` parameter.");
|
|
1993
|
+
}, [productId, connection]);
|
|
1407
1994
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(ProductDetailsLoader, {
|
|
1408
1995
|
productId,
|
|
1409
1996
|
className,
|
|
1410
|
-
|
|
1997
|
+
...props
|
|
1411
1998
|
}, productId) : /* @__PURE__ */ jsx("div", {
|
|
1412
|
-
|
|
1999
|
+
...partProps("product-details-root", className),
|
|
1413
2000
|
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1414
2001
|
}) });
|
|
1415
2002
|
}
|
|
1416
|
-
function ProductDetailsLoader({ productId, className, children }) {
|
|
2003
|
+
function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
|
|
1417
2004
|
const { config } = useCommerce();
|
|
1418
|
-
const [params, setParams] =
|
|
2005
|
+
const [params, setParams] = useRootParams(urlState);
|
|
1419
2006
|
const [quantity, setQuantity] = useState(1);
|
|
1420
2007
|
const [imageIndex, setImageIndex] = useState(0);
|
|
1421
2008
|
const product = useQuery({
|
|
1422
2009
|
retry: false,
|
|
2010
|
+
enabled: !!productId,
|
|
1423
2011
|
queryKey: [
|
|
1424
2012
|
"commerce",
|
|
1425
2013
|
config.cartScope,
|
|
@@ -1478,43 +2066,49 @@ function ProductDetailsLoader({ productId, className, children }) {
|
|
|
1478
2066
|
if (missing.length === 0) return;
|
|
1479
2067
|
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.`);
|
|
1480
2068
|
}, [requiredControls, has]);
|
|
1481
|
-
if (product.isPending) return /* @__PURE__ */ jsx("div", {
|
|
1482
|
-
|
|
1483
|
-
children: /* @__PURE__ */ jsx("p", {
|
|
2069
|
+
if (product.isPending && productId) return /* @__PURE__ */ jsx("div", {
|
|
2070
|
+
...partProps("product-details-root", className),
|
|
2071
|
+
children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
|
|
1484
2072
|
role: "status",
|
|
1485
2073
|
children: "Loading product…"
|
|
1486
2074
|
})
|
|
1487
2075
|
});
|
|
1488
|
-
if (product.isError && !productNotFound)
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
/* @__PURE__ */
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2076
|
+
if (product.isError && !productNotFound) {
|
|
2077
|
+
const retry = () => void product.refetch();
|
|
2078
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2079
|
+
role: "alert",
|
|
2080
|
+
...partProps("product-details-root", className),
|
|
2081
|
+
children: renderLabel(error, {
|
|
2082
|
+
message: message(product.error),
|
|
2083
|
+
retry
|
|
2084
|
+
}, /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2085
|
+
/* @__PURE__ */ jsx("h1", {
|
|
2086
|
+
className: "text-2xl font-semibold",
|
|
2087
|
+
children: "Product unavailable"
|
|
2088
|
+
}),
|
|
2089
|
+
/* @__PURE__ */ jsx("p", {
|
|
2090
|
+
className: "my-4 text-red-700",
|
|
2091
|
+
children: message(product.error)
|
|
2092
|
+
}),
|
|
2093
|
+
/* @__PURE__ */ jsx("button", {
|
|
2094
|
+
type: "button",
|
|
2095
|
+
className: buttonClass,
|
|
2096
|
+
onClick: retry,
|
|
2097
|
+
children: "Retry product"
|
|
2098
|
+
})
|
|
2099
|
+
] }))
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
if (productNotFound || !group) return /* @__PURE__ */ jsx("div", {
|
|
2103
|
+
...partProps("product-details-root", className),
|
|
2104
|
+
children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
|
|
1511
2105
|
className: "text-2xl font-semibold",
|
|
1512
2106
|
children: "Product not found"
|
|
1513
2107
|
}), /* @__PURE__ */ jsx(Link, {
|
|
1514
2108
|
className: `${buttonClass} mt-4`,
|
|
1515
2109
|
to: config.catalogPath,
|
|
1516
2110
|
children: "Back to shop"
|
|
1517
|
-
})]
|
|
2111
|
+
})] })
|
|
1518
2112
|
});
|
|
1519
2113
|
const value = {
|
|
1520
2114
|
product: group,
|
|
@@ -1545,7 +2139,7 @@ function ProductDetailsLoader({ productId, className, children }) {
|
|
|
1545
2139
|
return /* @__PURE__ */ jsx(ProductDetailsContext.Provider, {
|
|
1546
2140
|
value,
|
|
1547
2141
|
children: /* @__PURE__ */ jsx("article", {
|
|
1548
|
-
|
|
2142
|
+
...partProps("product-details-root", className),
|
|
1549
2143
|
children
|
|
1550
2144
|
})
|
|
1551
2145
|
});
|
|
@@ -1565,7 +2159,7 @@ function ProductDetailsPurchase({ className }) {
|
|
|
1565
2159
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
|
|
1566
2160
|
const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
|
|
1567
2161
|
return /* @__PURE__ */ jsxs("div", {
|
|
1568
|
-
|
|
2162
|
+
...partProps("product-details-purchase", "space-y-6", className),
|
|
1569
2163
|
children: [
|
|
1570
2164
|
/* @__PURE__ */ jsx(OptionControls, {}),
|
|
1571
2165
|
/* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
|
|
@@ -1580,13 +2174,13 @@ function ProductDetailsOptions({ className }) {
|
|
|
1580
2174
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1581
2175
|
if (attributes.length === 0 && skuOptions.length === 0) return null;
|
|
1582
2176
|
return /* @__PURE__ */ jsx("div", {
|
|
1583
|
-
|
|
2177
|
+
...partProps("product-details-options", "space-y-6", className),
|
|
1584
2178
|
children: /* @__PURE__ */ jsx(OptionControls, {})
|
|
1585
2179
|
});
|
|
1586
2180
|
}
|
|
1587
2181
|
function OptionControls() {
|
|
1588
2182
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1589
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
|
|
2183
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
|
|
1590
2184
|
}
|
|
1591
2185
|
function Select({ children,...props }) {
|
|
1592
2186
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -1615,7 +2209,7 @@ function SkuOption() {
|
|
|
1615
2209
|
const id = useId();
|
|
1616
2210
|
useSkuOptionRegistration();
|
|
1617
2211
|
return /* @__PURE__ */ jsxs("div", {
|
|
1618
|
-
|
|
2212
|
+
...partProps("product-details-sku-option"),
|
|
1619
2213
|
children: [/* @__PURE__ */ jsx("label", {
|
|
1620
2214
|
htmlFor: id,
|
|
1621
2215
|
className: "mb-2 block font-medium",
|
|
@@ -1639,7 +2233,8 @@ function ProductDetailsOption({ attribute, className }) {
|
|
|
1639
2233
|
const option = useProductDetailsOption(attribute);
|
|
1640
2234
|
if (!option) return null;
|
|
1641
2235
|
return /* @__PURE__ */ jsx("div", {
|
|
1642
|
-
|
|
2236
|
+
...partProps("product-details-option", className),
|
|
2237
|
+
"data-commerce-attribute": option.name,
|
|
1643
2238
|
children: /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", {
|
|
1644
2239
|
className: "mb-2 font-medium",
|
|
1645
2240
|
children: option.label
|
|
@@ -1698,7 +2293,7 @@ function ProductDetailsPurchaseStatus({ className }) {
|
|
|
1698
2293
|
break;
|
|
1699
2294
|
}
|
|
1700
2295
|
return /* @__PURE__ */ jsx("div", {
|
|
1701
|
-
|
|
2296
|
+
...partProps("product-details-purchase-status", className),
|
|
1702
2297
|
children: content
|
|
1703
2298
|
});
|
|
1704
2299
|
}
|
|
@@ -1707,53 +2302,70 @@ function ProductDetailsAvailability({ className }) {
|
|
|
1707
2302
|
const { selectedSku, availableQuantity } = useProductDetailsInternal("ProductDetailsAvailability");
|
|
1708
2303
|
if (!selectedSku) return null;
|
|
1709
2304
|
return /* @__PURE__ */ jsx("div", {
|
|
1710
|
-
|
|
2305
|
+
...partProps("product-details-availability", className),
|
|
1711
2306
|
children: /* @__PURE__ */ jsx("p", {
|
|
1712
2307
|
className: "text-sm",
|
|
1713
2308
|
"aria-live": "polite",
|
|
1714
|
-
children: availableQuantity === null ? "Available to order" : availableQuantity
|
|
2309
|
+
children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
|
|
1715
2310
|
})
|
|
1716
2311
|
});
|
|
1717
2312
|
}
|
|
1718
|
-
/** Quantity for the verified SKU. Renders nothing until a SKU is verified. */
|
|
2313
|
+
/** Quantity for the verified SKU, with a message when it cannot be added. Renders nothing until a SKU is verified. */
|
|
1719
2314
|
function ProductDetailsQuantity({ className }) {
|
|
1720
2315
|
const { selectedSku, availableQuantity, quantity, setQuantity } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
1721
2316
|
const id = useId();
|
|
2317
|
+
const problemId = useId();
|
|
1722
2318
|
if (!selectedSku) return null;
|
|
2319
|
+
const problem = isSoldOut(availableQuantity) ? null : quantityProblem(quantity, availableQuantity);
|
|
1723
2320
|
return /* @__PURE__ */ jsxs("div", {
|
|
1724
|
-
|
|
1725
|
-
children: [
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2321
|
+
...partProps("product-details-quantity", className),
|
|
2322
|
+
children: [
|
|
2323
|
+
/* @__PURE__ */ jsx("label", {
|
|
2324
|
+
htmlFor: id,
|
|
2325
|
+
className: "mb-2 block font-medium",
|
|
2326
|
+
children: "Quantity"
|
|
2327
|
+
}),
|
|
2328
|
+
/* @__PURE__ */ jsx("input", {
|
|
2329
|
+
id,
|
|
2330
|
+
type: "number",
|
|
2331
|
+
min: 1,
|
|
2332
|
+
max: availableQuantity ?? void 0,
|
|
2333
|
+
step: 1,
|
|
2334
|
+
className: `${inputClass} w-28`,
|
|
2335
|
+
value: Number.isNaN(quantity) ? "" : quantity,
|
|
2336
|
+
"aria-invalid": problem ? true : void 0,
|
|
2337
|
+
"aria-describedby": problem ? problemId : void 0,
|
|
2338
|
+
onChange: (event) => setQuantity(event.target.valueAsNumber)
|
|
2339
|
+
}),
|
|
2340
|
+
problem && /* @__PURE__ */ jsx("p", {
|
|
2341
|
+
id: problemId,
|
|
2342
|
+
className: "mt-2 text-sm text-red-700",
|
|
2343
|
+
children: problem
|
|
2344
|
+
})
|
|
2345
|
+
]
|
|
1739
2346
|
});
|
|
1740
2347
|
}
|
|
1741
2348
|
/** Adds the verified SKU and quantity. While disabled, its accessible description says why. */
|
|
1742
|
-
function ProductDetailsAddToCart({ className }) {
|
|
2349
|
+
function ProductDetailsAddToCart({ className, children }) {
|
|
1743
2350
|
const { selectedSku, name, quantity, status } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
1744
2351
|
const reasonId = useId();
|
|
1745
2352
|
return /* @__PURE__ */ jsx("div", {
|
|
1746
|
-
|
|
2353
|
+
...partProps("product-details-add-to-cart", className),
|
|
1747
2354
|
children: selectedSku ? /* @__PURE__ */ jsx(AddToCartButton, {
|
|
1748
2355
|
sku: selectedSku,
|
|
1749
2356
|
name,
|
|
1750
|
-
quantity
|
|
1751
|
-
|
|
2357
|
+
quantity,
|
|
2358
|
+
children
|
|
2359
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
1752
2360
|
type: "button",
|
|
1753
2361
|
className: `${buttonClass} w-full`,
|
|
1754
2362
|
disabled: true,
|
|
1755
2363
|
"aria-describedby": reasonId,
|
|
1756
|
-
|
|
2364
|
+
"data-commerce-part": "add-to-cart-button",
|
|
2365
|
+
children: renderLabel(children, {
|
|
2366
|
+
busy: false,
|
|
2367
|
+
soldOut: false
|
|
2368
|
+
}, "Add to cart")
|
|
1757
2369
|
}), /* @__PURE__ */ jsx("span", {
|
|
1758
2370
|
id: reasonId,
|
|
1759
2371
|
hidden: true,
|
|
@@ -1779,15 +2391,16 @@ function ProductDetails() {
|
|
|
1779
2391
|
})]
|
|
1780
2392
|
})] });
|
|
1781
2393
|
}
|
|
1782
|
-
|
|
2394
|
+
/** A link to the catalog. */
|
|
2395
|
+
function ProductDetailsBackLink({ className, children }) {
|
|
1783
2396
|
useProductDetailsInternal("ProductDetailsBackLink");
|
|
1784
2397
|
const { config } = useCommerce();
|
|
1785
2398
|
return /* @__PURE__ */ jsx("div", {
|
|
1786
|
-
|
|
2399
|
+
...partProps("product-details-back-link", className),
|
|
1787
2400
|
children: /* @__PURE__ */ jsx(Link, {
|
|
1788
2401
|
className: "inline-flex min-h-11 items-center text-sm text-inherit underline underline-offset-4 focus-visible:outline focus-visible:outline-2",
|
|
1789
2402
|
to: config.catalogPath,
|
|
1790
|
-
children: "← Back to shop"
|
|
2403
|
+
children: renderLabel(children, void 0, "← Back to shop")
|
|
1791
2404
|
})
|
|
1792
2405
|
});
|
|
1793
2406
|
}
|
|
@@ -1795,7 +2408,7 @@ function ProductDetailsBackLink({ className }) {
|
|
|
1795
2408
|
function ProductDetailsGallery({ className }) {
|
|
1796
2409
|
const { images, imageIndex, setImageIndex, name } = useProductDetailsInternal("ProductDetailsGallery");
|
|
1797
2410
|
return /* @__PURE__ */ jsxs("div", {
|
|
1798
|
-
|
|
2411
|
+
...partProps("product-details-gallery", className),
|
|
1799
2412
|
children: [/* @__PURE__ */ jsx("div", {
|
|
1800
2413
|
className: "aspect-square overflow-hidden rounded-lg",
|
|
1801
2414
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
@@ -1809,6 +2422,7 @@ function ProductDetailsGallery({ className }) {
|
|
|
1809
2422
|
className: `h-20 w-20 shrink-0 overflow-hidden rounded-lg border-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ${imageIndex === index ? "border-neutral-900" : "border-transparent"}`,
|
|
1810
2423
|
"aria-label": `View image ${index + 1}`,
|
|
1811
2424
|
"aria-pressed": imageIndex === index,
|
|
2425
|
+
"data-commerce-control": "thumbnail",
|
|
1812
2426
|
onClick: () => setImageIndex(index),
|
|
1813
2427
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1814
2428
|
url,
|
|
@@ -1822,7 +2436,7 @@ function ProductDetailsGallery({ className }) {
|
|
|
1822
2436
|
function ProductDetailsTitle({ className }) {
|
|
1823
2437
|
const { name } = useProductDetailsInternal("ProductDetailsTitle");
|
|
1824
2438
|
return /* @__PURE__ */ jsx("div", {
|
|
1825
|
-
|
|
2439
|
+
...partProps("product-details-title", className),
|
|
1826
2440
|
children: /* @__PURE__ */ jsx("h1", {
|
|
1827
2441
|
className: "break-words text-4xl font-semibold leading-tight sm:text-5xl",
|
|
1828
2442
|
children: name
|
|
@@ -1833,15 +2447,16 @@ function ProductDetailsTitle({ className }) {
|
|
|
1833
2447
|
function ProductDetailsPrice({ className }) {
|
|
1834
2448
|
const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
|
|
1835
2449
|
const { config } = useCommerce();
|
|
2450
|
+
const money$1 = useMoney();
|
|
1836
2451
|
return /* @__PURE__ */ jsx("div", {
|
|
1837
|
-
|
|
2452
|
+
...partProps("product-details-price", className),
|
|
1838
2453
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1839
2454
|
"aria-live": "polite",
|
|
1840
2455
|
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium",
|
|
1841
2456
|
"data-testid": "product-price",
|
|
1842
|
-
children: [price?.value?.value != null ? money(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money(product.priceRange.min
|
|
2457
|
+
children: [price?.value?.value != null ? money$1(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money$1(product.priceRange.min)}` : "Price unavailable", price?.compareAtValue?.value != null && price.value?.value != null && price.compareAtValue.value > price.value.value && /* @__PURE__ */ jsx("del", {
|
|
1843
2458
|
className: "text-lg font-normal",
|
|
1844
|
-
children: money(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
|
|
2459
|
+
children: money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
|
|
1845
2460
|
})]
|
|
1846
2461
|
})
|
|
1847
2462
|
});
|
|
@@ -1850,7 +2465,7 @@ function ProductDetailsPrice({ className }) {
|
|
|
1850
2465
|
function ProductDetailsDescription({ className }) {
|
|
1851
2466
|
const { selectedSku, product } = useProductDetailsInternal("ProductDetailsDescription");
|
|
1852
2467
|
return /* @__PURE__ */ jsx("div", {
|
|
1853
|
-
|
|
2468
|
+
...partProps("product-details-description", className),
|
|
1854
2469
|
children: /* @__PURE__ */ jsx("p", {
|
|
1855
2470
|
className: "whitespace-pre-line text-base leading-7",
|
|
1856
2471
|
children: selectedSku?.description ?? product.description
|
|
@@ -1859,4 +2474,4 @@ function ProductDetailsDescription({ className }) {
|
|
|
1859
2474
|
}
|
|
1860
2475
|
|
|
1861
2476
|
//#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 };
|
|
2477
|
+
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, CatalogEmpty, 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, ProductImage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useMoney, useProductCard, useProductDetails, useProductDetailsOption };
|