@godaddy/gd-commerce-storefront 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1184 @@
1
+ import * as Dialog from "@radix-ui/react-dialog";
2
+ import { createContext, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
3
+ import { Link, useParams, useSearchParams } from "react-router";
4
+ import { useQuery } from "@tanstack/react-query";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ //#region src/api.ts
8
+ var CartIdStorage = class {
9
+ currentId = null;
10
+ pendingWrite = false;
11
+ constructor(key) {
12
+ this.key = key;
13
+ }
14
+ read() {
15
+ if (this.pendingWrite) return this.currentId;
16
+ return localStorage.getItem(this.key);
17
+ }
18
+ write(id) {
19
+ this.currentId = id;
20
+ this.pendingWrite = true;
21
+ if (id) localStorage.setItem(this.key, id);
22
+ else localStorage.removeItem(this.key);
23
+ this.pendingWrite = false;
24
+ }
25
+ };
26
+ var ApiError = class extends Error {
27
+ constructor(message$1, status, options) {
28
+ super(message$1, options);
29
+ this.status = status;
30
+ }
31
+ };
32
+ const checkedFetch = async (input, init) => {
33
+ const response = await fetch(input, init);
34
+ if (!response.ok) {
35
+ const data = await response.text();
36
+ const parsed = (() => {
37
+ try {
38
+ return JSON.parse(data);
39
+ } catch (cause) {
40
+ throw new ApiError(`Commerce request failed (${response.status}). Please retry.`, response.status, { cause });
41
+ }
42
+ })();
43
+ throw new ApiError(typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "string" ? parsed.error : "Commerce request failed. Please retry.", response.status);
44
+ }
45
+ return response;
46
+ };
47
+ async function request(path, init) {
48
+ const headers = new Headers(init?.headers);
49
+ if (init?.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
50
+ const timeout = !init?.method || init.method.toUpperCase() === "GET" ? AbortSignal.timeout(15e3) : void 0;
51
+ const signal = timeout ? AbortSignal.any([timeout, ...init?.signal ? [init.signal] : []]) : init?.signal;
52
+ try {
53
+ return await (await checkedFetch(`/api/commerce${path}`, {
54
+ ...init,
55
+ signal,
56
+ headers,
57
+ cache: "no-store"
58
+ })).json();
59
+ } catch (cause) {
60
+ if (timeout?.aborted && !init?.signal?.aborted) throw new ApiError("Commerce information took too long to load. Please retry.", 408, { cause });
61
+ throw cause;
62
+ }
63
+ }
64
+ function money(value, currency) {
65
+ const formatter = new Intl.NumberFormat("en-US", {
66
+ style: "currency",
67
+ currency
68
+ });
69
+ const digits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
70
+ return formatter.format(value / 10 ** digits);
71
+ }
72
+ function message(error) {
73
+ return error instanceof Error ? error.message : "Something went wrong. Please retry.";
74
+ }
75
+
76
+ //#endregion
77
+ //#region src/cart-model.ts
78
+ async function addToCart(cartId, item, fetchFn = globalThis.fetch) {
79
+ const url = cartId ? `/api/commerce/cart/${encodeURIComponent(cartId)}/items` : "/api/commerce/cart";
80
+ const body = cartId ? item : { lineItems: [item] };
81
+ return (await fetchFn(url, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify(body)
85
+ })).json();
86
+ }
87
+ function getCartSummaryTotals(order) {
88
+ const lineItems = order?.lineItems ?? [];
89
+ const currencyCode = order?.totals?.total?.currencyCode || "USD";
90
+ return {
91
+ itemCount: lineItems.reduce((sum, item) => sum + (item.quantity || 0), 0),
92
+ currencyCode,
93
+ subtotal: order?.totals?.subTotal?.value || 0,
94
+ shipping: order?.totals?.shippingTotal?.value || 0,
95
+ taxes: order?.totals?.taxTotal?.value || 0,
96
+ discount: order?.totals?.discountTotal?.value || 0,
97
+ total: order?.totals?.total?.value || 0
98
+ };
99
+ }
100
+
101
+ //#endregion
102
+ //#region src/catalog-model.ts
103
+ function getSingleMatchedSku(skuGroup) {
104
+ const connection = skuGroup?.skus;
105
+ if (connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? 0) > 1) return null;
106
+ const edges = connection?.edges ?? [];
107
+ const sku = edges.length === 1 ? edges[0]?.node : null;
108
+ return sku?.id ? sku : null;
109
+ }
110
+ function getLabeledSkuOptions(skuGroup) {
111
+ if (getProductAttributes(skuGroup).length > 0) return [];
112
+ const connection = skuGroup?.skus;
113
+ const edges = connection?.edges ?? [];
114
+ if (edges.length < 2 || connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? edges.length) !== edges.length) return [];
115
+ const skus = edges.flatMap((edge) => edge?.node?.id && (edge.node.label?.trim() || edge.node.name?.trim()) ? [edge.node] : []);
116
+ const labels = new Set(skus.map((sku) => (sku.label?.trim() || sku.name?.trim() || "").toLowerCase()));
117
+ const ids = new Set(skus.map((sku) => sku.id));
118
+ return skus.length === edges.length && labels.size === skus.length && ids.size === skus.length ? skus : [];
119
+ }
120
+ function getAvailableInventoryQuantity(item) {
121
+ const edges = item?.inventoryCounts?.edges;
122
+ if (!edges || edges.length === 0) return null;
123
+ return edges.find((edge) => edge?.node?.type === "AVAILABLE")?.node?.quantity ?? 0;
124
+ }
125
+ function getImageUrls(item) {
126
+ return item?.mediaObjects?.edges?.filter((edge) => edge?.node?.type === "IMAGE" && edge.node.url).map((edge) => edge?.node?.url).filter((url) => Boolean(url)) ?? [];
127
+ }
128
+ function getPrimaryImageUrl(item) {
129
+ return getImageUrls(item)[0] ?? null;
130
+ }
131
+ function getProductAttributes(skuGroup) {
132
+ return skuGroup?.attributes?.edges?.map((edge) => {
133
+ const attributeNode = edge?.node;
134
+ const values = attributeNode?.values?.edges?.map((valueEdge) => {
135
+ const valueNode = valueEdge?.node;
136
+ return {
137
+ id: valueNode?.id || "",
138
+ name: valueNode?.name || "",
139
+ label: valueNode?.label || valueNode?.name || ""
140
+ };
141
+ }) ?? [];
142
+ return {
143
+ id: attributeNode?.id || "",
144
+ name: attributeNode?.name || "",
145
+ label: attributeNode?.label || attributeNode?.name || "",
146
+ values
147
+ };
148
+ }) ?? [];
149
+ }
150
+
151
+ //#endregion
152
+ //#region src/commerce-provider.tsx
153
+ const CommerceContext = createContext(null);
154
+ function useCommerce() {
155
+ const context = useContext(CommerceContext);
156
+ if (!context) throw new Error("Mount CommerceProvider once around the header and page outlet, inside the router.");
157
+ return context;
158
+ }
159
+ async function loadConfig(signal) {
160
+ const config = await request("/config", { signal });
161
+ if (!config || typeof config.cartScope !== "string" || !config.cartScope.trim() || typeof config.currencyCode !== "string" || !/^[A-Z]{3}$/.test(config.currencyCode)) throw new Error("The store returned invalid configuration.");
162
+ return config;
163
+ }
164
+ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {} }) {
165
+ const config = useQuery({
166
+ retry: false,
167
+ queryKey: ["commerce", "configuration"],
168
+ queryFn: ({ signal }) => loadConfig(signal),
169
+ staleTime: 3e4,
170
+ refetchOnWindowFocus: true
171
+ });
172
+ return /* @__PURE__ */ jsx(BoundCommerceProvider, {
173
+ config: {
174
+ cartScope: "",
175
+ currencyCode: "USD",
176
+ ...config.data,
177
+ catalogPath,
178
+ productPath,
179
+ checkoutSuccessPath
180
+ },
181
+ connection: config.isError ? "error" : config.data ? "ready" : "loading",
182
+ connectionError: config.isError ? message(config.error) : null,
183
+ retryConnection: () => {
184
+ config.refetch();
185
+ },
186
+ theme,
187
+ children
188
+ });
189
+ }
190
+ function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme }) {
191
+ const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
192
+ const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
193
+ const ready = connection === "ready";
194
+ const session = useMemo(() => ({
195
+ cartIdStorage,
196
+ ready
197
+ }), [cartIdStorage, ready]);
198
+ const currentSession = useRef(session);
199
+ currentSession.current = session;
200
+ const readyRef = useRef(ready);
201
+ readyRef.current = ready;
202
+ const [cart, setCart] = useState(null);
203
+ const [open, setOpen] = useState(false);
204
+ const [pending, setPending] = useState(false);
205
+ const [hydrating, setHydrating] = useState(true);
206
+ const [error, setError] = useState(null);
207
+ const [storageWarning, setStorageWarning] = useState(null);
208
+ const [announcement, setAnnouncement] = useState("");
209
+ const id = useRef(null);
210
+ const currentCart = useRef(null);
211
+ const queue = useRef(Promise.resolve(true));
212
+ const alive = useRef(true);
213
+ const opener = useRef(null);
214
+ function rememberOpener() {
215
+ opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
216
+ }
217
+ function isActive() {
218
+ return alive.current && currentSession.current === session && readyRef.current;
219
+ }
220
+ function boundRequest(path, init) {
221
+ const headers = new Headers(init?.headers);
222
+ headers.set("X-Commerce-Scope", config.cartScope);
223
+ return request(path, {
224
+ ...init,
225
+ headers
226
+ });
227
+ }
228
+ function readId() {
229
+ try {
230
+ return cartIdStorage.read();
231
+ } catch {
232
+ setStorageWarning("Your browser cannot save the cart. Keep this page open while shopping.");
233
+ return id.current;
234
+ }
235
+ }
236
+ function commit(next) {
237
+ if (!isActive()) return;
238
+ id.current = next?.id ?? null;
239
+ currentCart.current = next;
240
+ setCart(next);
241
+ try {
242
+ cartIdStorage.write(id.current);
243
+ setStorageWarning(null);
244
+ } catch {
245
+ setStorageWarning("Your browser cannot save the cart. Keep this page open while shopping.");
246
+ }
247
+ }
248
+ async function hydrate() {
249
+ const storedId = readId();
250
+ id.current = storedId;
251
+ if (!storedId) {
252
+ commit(null);
253
+ return;
254
+ }
255
+ try {
256
+ commit((await boundRequest(`/cart/${encodeURIComponent(storedId)}`)).cart);
257
+ } catch (cause) {
258
+ if (cause instanceof ApiError && (cause.status === 404 || cause.status === 410)) {
259
+ commit(null);
260
+ return;
261
+ }
262
+ throw cause;
263
+ }
264
+ }
265
+ function run(operation, loading = false) {
266
+ const task = queue.current.then(async () => {
267
+ if (!isActive()) return false;
268
+ setPending(true);
269
+ if (loading) setHydrating(true);
270
+ setError(null);
271
+ try {
272
+ if (navigator.locks) await navigator.locks.request(storageKey, async () => {
273
+ if (isActive()) await operation();
274
+ });
275
+ else await operation();
276
+ return isActive();
277
+ } catch (cause) {
278
+ if (isActive()) setError(message(cause));
279
+ return false;
280
+ } finally {
281
+ if (isActive()) {
282
+ setPending(false);
283
+ setHydrating(false);
284
+ }
285
+ }
286
+ });
287
+ queue.current = task;
288
+ return task;
289
+ }
290
+ function refresh() {
291
+ return run(hydrate, true);
292
+ }
293
+ useEffect(() => {
294
+ alive.current = true;
295
+ id.current = null;
296
+ currentCart.current = null;
297
+ queue.current = Promise.resolve(true);
298
+ setCart(null);
299
+ setOpen(false);
300
+ setError(null);
301
+ setStorageWarning(null);
302
+ setPending(false);
303
+ setHydrating(ready);
304
+ if (ready) refresh();
305
+ const onStorage = (event) => {
306
+ if (event.key === storageKey || event.key === null) refresh();
307
+ };
308
+ const onFocus = () => {
309
+ refresh();
310
+ };
311
+ window.addEventListener("storage", onStorage);
312
+ window.addEventListener("focus", onFocus);
313
+ return () => {
314
+ alive.current = false;
315
+ window.removeEventListener("storage", onStorage);
316
+ window.removeEventListener("focus", onFocus);
317
+ };
318
+ }, [cartIdStorage, ready]);
319
+ function addItem(item) {
320
+ rememberOpener();
321
+ return run(async () => {
322
+ if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new Error("Select a product variant and a positive quantity.");
323
+ await hydrate();
324
+ if (!isActive()) return;
325
+ const fetchCart = (input, init) => {
326
+ const headers = new Headers(init?.headers);
327
+ headers.set("X-Commerce-Scope", config.cartScope);
328
+ return checkedFetch(input, {
329
+ ...init,
330
+ headers
331
+ });
332
+ };
333
+ const result = await addToCart(id.current, {
334
+ skuId: item.skuId,
335
+ name: item.name,
336
+ quantity: item.quantity
337
+ }, fetchCart);
338
+ if (!result.cart?.id || !result.cart.lineItems?.length) throw new Error("Commerce did not return a cart with items. Refresh before retrying.");
339
+ if (!isActive()) return;
340
+ commit(result.cart);
341
+ setAnnouncement(`${item.name} added to your cart.`);
342
+ setOpen(true);
343
+ });
344
+ }
345
+ function mutateItem(itemId, method, quantity) {
346
+ return run(async () => {
347
+ await hydrate();
348
+ if (!isActive()) return;
349
+ if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new Error("This item is no longer in the cart.");
350
+ if (quantity !== void 0 && (!Number.isInteger(quantity) || quantity < 1)) throw new Error("Quantity must be a positive whole number.");
351
+ const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/items/${encodeURIComponent(itemId)}`, {
352
+ method,
353
+ ...quantity !== void 0 ? { body: JSON.stringify({ quantity }) } : {}
354
+ });
355
+ if (!result.cart?.id) throw new Error("Commerce did not return the updated cart. Refresh before retrying.");
356
+ if (!isActive()) return;
357
+ commit(result.cart);
358
+ setAnnouncement(method === "DELETE" ? "Item removed from cart." : "Cart quantity updated.");
359
+ });
360
+ }
361
+ function applyDiscount(code) {
362
+ return run(async () => {
363
+ await hydrate();
364
+ if (!isActive()) return;
365
+ if (!id.current || !code.trim()) throw new Error("Add an item and enter a promotion code.");
366
+ const codes = [...new Set([...currentCart.current?.discounts?.flatMap((discount) => discount.code ? [discount.code] : []) ?? [], code.trim()])];
367
+ const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/discounts`, {
368
+ method: "POST",
369
+ body: JSON.stringify({ discountCodes: codes })
370
+ });
371
+ if (!result.cart?.id) throw new Error("Commerce did not return the updated cart.");
372
+ if (!isActive()) return;
373
+ commit(result.cart);
374
+ setAnnouncement("Promotion codes updated.");
375
+ });
376
+ }
377
+ function checkout() {
378
+ return run(async () => {
379
+ if (!config.checkoutSuccessPath) throw new Error("Checkout has not been enabled for this store.");
380
+ await hydrate();
381
+ if (!isActive()) return;
382
+ if (!id.current || !currentCart.current?.lineItems?.length) throw new Error("Add an item before checking out.");
383
+ const origin = window.location.origin;
384
+ const successUrl = new URL(config.checkoutSuccessPath, origin);
385
+ successUrl.searchParams.set("orderId", id.current);
386
+ const session$1 = await boundRequest("/checkout", {
387
+ method: "POST",
388
+ body: JSON.stringify({
389
+ draftOrderId: id.current,
390
+ returnUrl: new URL(config.catalogPath, origin).href,
391
+ successUrl: successUrl.href
392
+ })
393
+ });
394
+ if (!isActive()) return;
395
+ if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new Error("Commerce did not return a checkout URL.");
396
+ const url = new URL(session$1.url, origin);
397
+ if (url.protocol !== "https:") throw new Error("Commerce returned an invalid checkout URL.");
398
+ window.location.assign(url.href);
399
+ });
400
+ }
401
+ return /* @__PURE__ */ jsxs(CommerceContext.Provider, {
402
+ value: {
403
+ config,
404
+ connection,
405
+ connectionError,
406
+ retryConnection,
407
+ theme,
408
+ cart,
409
+ open,
410
+ setOpen: (next) => {
411
+ if (next) rememberOpener();
412
+ setOpen(next);
413
+ },
414
+ restoreFocus: () => {
415
+ opener.current?.focus();
416
+ },
417
+ pending,
418
+ hydrating,
419
+ error,
420
+ storageWarning,
421
+ announcement,
422
+ addItem,
423
+ changeQuantity: (itemId, quantity) => mutateItem(itemId, "PATCH", quantity),
424
+ removeItem: (itemId) => mutateItem(itemId, "DELETE"),
425
+ applyDiscount,
426
+ refresh,
427
+ checkout
428
+ },
429
+ children: [children, /* @__PURE__ */ jsx("span", {
430
+ className: "commerce-sr-only",
431
+ role: "status",
432
+ "aria-live": "polite",
433
+ children: announcement
434
+ })]
435
+ });
436
+ }
437
+
438
+ //#endregion
439
+ //#region src/storefront-surface.tsx
440
+ function StorefrontSurface({ children, className = "" }) {
441
+ const { theme } = useCommerce();
442
+ return /* @__PURE__ */ jsx("div", {
443
+ className: `commerce-storefront ${className}`,
444
+ style: theme,
445
+ children
446
+ });
447
+ }
448
+ function CommerceStatus() {
449
+ const { connection, connectionError, retryConnection } = useCommerce();
450
+ if (connection === "ready") return null;
451
+ return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "loading" ? /* @__PURE__ */ jsx("p", {
452
+ role: "status",
453
+ children: "Connecting to the store…"
454
+ }) : /* @__PURE__ */ jsxs("div", {
455
+ role: "alert",
456
+ children: [/* @__PURE__ */ jsx("p", { children: connectionError }), /* @__PURE__ */ jsx("button", {
457
+ type: "button",
458
+ className: "min-h-11 underline",
459
+ onClick: retryConnection,
460
+ children: "Retry connection"
461
+ })]
462
+ }) });
463
+ }
464
+
465
+ //#endregion
466
+ //#region src/cart.tsx
467
+ 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";
468
+ 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";
469
+ function CartButton() {
470
+ const { cart, setOpen, open, connection } = useCommerce();
471
+ const count = cart?.lineItems?.reduce((total, item) => total + (item.quantity ?? 0), 0) ?? 0;
472
+ return /* @__PURE__ */ jsx(StorefrontSurface, {
473
+ className: "commerce-inline",
474
+ children: /* @__PURE__ */ jsxs("button", {
475
+ type: "button",
476
+ className: buttonClass,
477
+ disabled: connection !== "ready",
478
+ "aria-haspopup": "dialog",
479
+ "aria-expanded": open,
480
+ onClick: () => setOpen(true),
481
+ children: ["Cart ", /* @__PURE__ */ jsx("span", {
482
+ className: "rounded-full bg-white px-2 py-0.5 text-xs text-neutral-900",
483
+ children: count
484
+ })]
485
+ })
486
+ });
487
+ }
488
+ function AddToCartButton({ sku, name, quantity = 1 }) {
489
+ const { addItem, pending, hydrating, error, open, connection } = useCommerce();
490
+ const [adding, setAdding] = useState(false);
491
+ const available = getAvailableInventoryQuantity(sku);
492
+ const disabled = connection !== "ready" || !sku.id || adding || hydrating || available === 0 || !Number.isInteger(quantity) || quantity < 1 || available !== null && quantity > available;
493
+ async function handleAdd() {
494
+ if (disabled || pending || !sku.id) return;
495
+ setAdding(true);
496
+ try {
497
+ await addItem({
498
+ skuId: sku.id,
499
+ name,
500
+ quantity
501
+ });
502
+ } finally {
503
+ setAdding(false);
504
+ }
505
+ }
506
+ return /* @__PURE__ */ jsxs(StorefrontSurface, {
507
+ className: "commerce-inline",
508
+ children: [/* @__PURE__ */ jsx("button", {
509
+ type: "button",
510
+ className: `${buttonClass} w-full`,
511
+ disabled,
512
+ "aria-disabled": disabled || pending,
513
+ "aria-busy": adding,
514
+ onClick: () => void handleAdd(),
515
+ children: available === 0 ? "Out of stock" : adding ? "Adding…" : "Add to cart"
516
+ }), error && !open && /* @__PURE__ */ jsx("p", {
517
+ role: "alert",
518
+ className: "mt-3 bg-white text-sm text-red-700",
519
+ children: error
520
+ })]
521
+ });
522
+ }
523
+ function CartDrawer() {
524
+ const { config, cart, open, setOpen, restoreFocus, pending, hydrating, error, storageWarning, changeQuantity, removeItem, refresh, checkout } = useCommerce();
525
+ const [drawerAction, setDrawerAction] = useState(null);
526
+ const checkingOut = drawerAction === "checkout";
527
+ const locked = pending || hydrating || drawerAction !== null;
528
+ const items = cart?.lineItems ?? [];
529
+ const summary = getCartSummaryTotals(cart);
530
+ const currency = cart?.totals?.total?.currencyCode ?? config.currencyCode;
531
+ async function handleAction(action, operation) {
532
+ if (locked) return;
533
+ setDrawerAction(action);
534
+ try {
535
+ await operation();
536
+ } finally {
537
+ setDrawerAction(null);
538
+ }
539
+ }
540
+ return /* @__PURE__ */ jsx(Dialog.Root, {
541
+ open,
542
+ onOpenChange: setOpen,
543
+ children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40" }), /* @__PURE__ */ jsxs(Dialog.Content, {
544
+ className: "fixed inset-y-0 right-0 z-50 flex w-full max-w-lg flex-col bg-white text-neutral-900 shadow-xl",
545
+ onCloseAutoFocus: (event) => {
546
+ event.preventDefault();
547
+ restoreFocus();
548
+ },
549
+ children: [
550
+ /* @__PURE__ */ jsxs("div", {
551
+ className: "flex items-start justify-between border-b border-neutral-200 p-6",
552
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Dialog.Title, {
553
+ className: "text-2xl font-semibold",
554
+ children: "Your cart"
555
+ }), /* @__PURE__ */ jsx(Dialog.Description, {
556
+ className: "mt-1 text-sm text-neutral-600",
557
+ children: "Review your items before checkout."
558
+ })] }), /* @__PURE__ */ jsx(Dialog.Close, {
559
+ 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",
560
+ "aria-label": "Close cart",
561
+ children: "✕"
562
+ })]
563
+ }),
564
+ /* @__PURE__ */ jsxs("div", {
565
+ className: "flex-1 overflow-y-auto p-6",
566
+ children: [
567
+ storageWarning && /* @__PURE__ */ jsx("p", {
568
+ role: "status",
569
+ className: "mb-4 text-sm text-amber-800",
570
+ children: storageWarning
571
+ }),
572
+ error && /* @__PURE__ */ jsxs("div", {
573
+ role: "alert",
574
+ className: "mb-4 rounded-lg bg-red-50 p-4 text-red-800",
575
+ children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
576
+ type: "button",
577
+ className: "mt-2 min-h-11 bg-red-50 text-red-800 underline disabled:cursor-not-allowed",
578
+ disabled: drawerAction === "refresh",
579
+ "aria-disabled": locked,
580
+ "aria-busy": drawerAction === "refresh",
581
+ onClick: () => void handleAction("refresh", refresh),
582
+ children: "Refresh cart"
583
+ })]
584
+ }),
585
+ hydrating && /* @__PURE__ */ jsx("p", {
586
+ role: "status",
587
+ children: "Loading your cart…"
588
+ }),
589
+ !hydrating && !items.length && /* @__PURE__ */ jsxs("div", {
590
+ className: "py-16 text-center",
591
+ children: [
592
+ /* @__PURE__ */ jsx("p", {
593
+ className: "text-xl font-medium",
594
+ children: "Your cart is empty"
595
+ }),
596
+ /* @__PURE__ */ jsx("p", {
597
+ className: "mt-2 text-neutral-600",
598
+ children: "Find something you love in the shop."
599
+ }),
600
+ /* @__PURE__ */ jsx(Link, {
601
+ className: `${buttonClass} mt-6`,
602
+ to: config.catalogPath,
603
+ onClick: () => setOpen(false),
604
+ children: "Browse products"
605
+ })
606
+ ]
607
+ }),
608
+ /* @__PURE__ */ jsx("ul", {
609
+ className: "divide-y divide-neutral-200",
610
+ children: items.map((item) => /* @__PURE__ */ jsxs("li", {
611
+ className: "py-5 first:pt-0",
612
+ "data-testid": "cart-item",
613
+ children: [/* @__PURE__ */ jsxs("div", {
614
+ className: "flex gap-4",
615
+ children: [item.details?.productAssetUrl && /* @__PURE__ */ jsx("img", {
616
+ src: item.details.productAssetUrl,
617
+ alt: "",
618
+ className: "h-20 w-20 rounded-lg bg-neutral-100 object-cover"
619
+ }), /* @__PURE__ */ jsxs("div", {
620
+ className: "min-w-0 flex-1",
621
+ children: [
622
+ /* @__PURE__ */ jsx("h3", {
623
+ className: "font-semibold",
624
+ children: item.name
625
+ }),
626
+ item.details?.selectedOptions?.map((option) => /* @__PURE__ */ jsxs("p", {
627
+ className: "text-sm text-neutral-600",
628
+ children: [
629
+ option.attribute,
630
+ ": ",
631
+ option.values?.join(", ")
632
+ ]
633
+ }, `${option.attribute}:${option.values?.join(",")}`)),
634
+ /* @__PURE__ */ jsx("p", {
635
+ className: "mt-1 text-sm",
636
+ children: typeof item.totals?.subTotal?.value === "number" ? money(item.totals.subTotal.value, item.totals.subTotal.currencyCode ?? currency) : "Price unavailable"
637
+ })
638
+ ]
639
+ })]
640
+ }), /* @__PURE__ */ jsxs("div", {
641
+ className: "mt-4 flex items-center justify-between gap-2",
642
+ children: [/* @__PURE__ */ jsxs("div", {
643
+ className: "flex items-center rounded-lg border border-neutral-300",
644
+ children: [
645
+ /* @__PURE__ */ jsx("button", {
646
+ type: "button",
647
+ 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",
648
+ "aria-label": `Decrease quantity of ${item.name}`,
649
+ disabled: drawerAction === `decrease:${item.id}` || !item.id || (item.quantity ?? 0) <= 1,
650
+ "aria-disabled": locked || !item.id || (item.quantity ?? 0) <= 1,
651
+ "aria-busy": drawerAction === `decrease:${item.id}`,
652
+ onClick: () => void handleAction(`decrease:${item.id}`, () => changeQuantity(item.id ?? "", (item.quantity ?? 1) - 1)),
653
+ children: "−"
654
+ }),
655
+ /* @__PURE__ */ jsxs("span", {
656
+ className: "min-w-8 text-center",
657
+ children: [/* @__PURE__ */ jsx("span", {
658
+ className: "sr-only",
659
+ children: "Quantity: "
660
+ }), item.quantity]
661
+ }),
662
+ /* @__PURE__ */ jsx("button", {
663
+ type: "button",
664
+ 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",
665
+ "aria-label": `Increase quantity of ${item.name}`,
666
+ disabled: drawerAction === `increase:${item.id}` || !item.id,
667
+ "aria-disabled": locked || !item.id,
668
+ "aria-busy": drawerAction === `increase:${item.id}`,
669
+ onClick: () => void handleAction(`increase:${item.id}`, () => changeQuantity(item.id ?? "", (item.quantity ?? 1) + 1)),
670
+ children: "+"
671
+ })
672
+ ]
673
+ }), /* @__PURE__ */ jsx("button", {
674
+ type: "button",
675
+ 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",
676
+ "aria-label": `Remove ${item.name}`,
677
+ disabled: drawerAction === `remove:${item.id}` || !item.id,
678
+ "aria-disabled": locked || !item.id,
679
+ "aria-busy": drawerAction === `remove:${item.id}`,
680
+ onClick: () => void handleAction(`remove:${item.id}`, () => removeItem(item.id ?? "")),
681
+ children: "Remove"
682
+ })]
683
+ })]
684
+ }, item.id))
685
+ })
686
+ ]
687
+ }),
688
+ items.length > 0 && /* @__PURE__ */ jsxs("div", {
689
+ className: "border-t border-neutral-200 p-6",
690
+ children: [
691
+ /* @__PURE__ */ jsx("dl", {
692
+ className: "space-y-2 text-sm",
693
+ children: typeof cart?.totals?.subTotal?.value === "number" && /* @__PURE__ */ jsxs("div", {
694
+ className: "flex justify-between",
695
+ children: [/* @__PURE__ */ jsx("dt", { children: "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money(summary.subtotal, cart.totals.subTotal.currencyCode ?? currency) })]
696
+ })
697
+ }),
698
+ /* @__PURE__ */ jsx("p", {
699
+ className: "commerce-cart-checkout-adjustments-note mt-3 text-sm text-neutral-600",
700
+ children: "Shipping, taxes, and discounts are calculated at checkout."
701
+ }),
702
+ config.checkoutSuccessPath ? /* @__PURE__ */ jsx("button", {
703
+ type: "button",
704
+ className: `${buttonClass} w-full`,
705
+ disabled: checkingOut || hydrating || cart?.totals?.total?.value == null,
706
+ "aria-disabled": locked || cart?.totals?.total?.value == null,
707
+ "aria-busy": checkingOut,
708
+ onClick: () => void handleAction("checkout", checkout),
709
+ children: checkingOut ? "Opening checkout…" : "Proceed to Checkout"
710
+ }) : /* @__PURE__ */ jsx("p", {
711
+ className: "text-sm text-neutral-600",
712
+ children: "Checkout is not available yet."
713
+ })
714
+ ]
715
+ })
716
+ ]
717
+ })] }) })
718
+ });
719
+ }
720
+
721
+ //#endregion
722
+ //#region src/catalog.tsx
723
+ function ProductImage({ url, name, className = "" }) {
724
+ const [failed, setFailed] = useState(false);
725
+ return url && !failed ? /* @__PURE__ */ jsx("img", {
726
+ src: url,
727
+ alt: name,
728
+ loading: "lazy",
729
+ className: `h-full w-full object-cover ${className}`,
730
+ onError: () => setFailed(true)
731
+ }) : /* @__PURE__ */ jsx("div", {
732
+ className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
733
+ role: "img",
734
+ "aria-label": `${name}: image unavailable`,
735
+ children: "No image available"
736
+ });
737
+ }
738
+ function ProductCard({ product }) {
739
+ return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsx(ProductCardContent, { product }) });
740
+ }
741
+ function ProductCardContent({ product }) {
742
+ const { config } = useCommerce();
743
+ const name = product.label ?? product.name ?? "Product";
744
+ const href = `${config.productPath}/${encodeURIComponent(product.id ?? "")}`;
745
+ const selectedSku = getSingleMatchedSku(product);
746
+ selectedSku?.id;
747
+ const hasVariants = getProductAttributes(product).length > 0;
748
+ const min = product.priceRange?.min;
749
+ const max = product.priceRange?.max;
750
+ return /* @__PURE__ */ jsxs("article", {
751
+ className: "group flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200",
752
+ children: [/* @__PURE__ */ jsx(Link, {
753
+ to: href,
754
+ tabIndex: -1,
755
+ "aria-hidden": "true",
756
+ className: "block aspect-square overflow-hidden",
757
+ children: /* @__PURE__ */ jsx(ProductImage, {
758
+ url: getPrimaryImageUrl(product),
759
+ name,
760
+ className: "motion-safe:transition-transform motion-safe:duration-200 motion-safe:group-hover:scale-105"
761
+ })
762
+ }), /* @__PURE__ */ jsxs("div", {
763
+ className: "flex flex-1 flex-col p-6",
764
+ children: [
765
+ /* @__PURE__ */ jsx("h2", {
766
+ className: "break-words text-2xl font-semibold leading-tight",
767
+ children: /* @__PURE__ */ jsx(Link, {
768
+ className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
769
+ to: href,
770
+ children: name
771
+ })
772
+ }),
773
+ product.description && /* @__PURE__ */ jsx("p", {
774
+ className: "mt-3 line-clamp-3 text-sm leading-6",
775
+ children: product.description
776
+ }),
777
+ /* @__PURE__ */ jsx("div", {
778
+ className: "mt-auto pt-6",
779
+ children: /* @__PURE__ */ jsxs("div", {
780
+ className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
781
+ children: [/* @__PURE__ */ jsx("p", {
782
+ className: "min-w-0 flex-1 text-lg font-semibold leading-7",
783
+ children: typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable"
784
+ }), /* @__PURE__ */ jsx("div", {
785
+ className: "max-w-full",
786
+ children: !hasVariants && selectedSku ? /* @__PURE__ */ jsx(AddToCartButton, {
787
+ sku: selectedSku,
788
+ name
789
+ }) : /* @__PURE__ */ jsx(Link, {
790
+ className: buttonClass,
791
+ to: href,
792
+ children: hasVariants ? "Choose options" : "View product"
793
+ })
794
+ })]
795
+ })
796
+ })
797
+ ]
798
+ })]
799
+ });
800
+ }
801
+ function Catalog(props) {
802
+ const { connection } = useCommerce();
803
+ return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(CatalogContent, { ...props }) : /* @__PURE__ */ jsx(CommerceStatus, {}) });
804
+ }
805
+ function CatalogContent({ title = "Shop all products", description = "Explore the collection and find your favorites.", showHeader = true }) {
806
+ const { config } = useCommerce();
807
+ const [params, setParams] = useSearchParams();
808
+ const after = params.get("after") ?? "";
809
+ const urlParams = new URLSearchParams({ first: "6" });
810
+ if (after) urlParams.set("after", after);
811
+ const products = useQuery({
812
+ retry: false,
813
+ queryKey: [
814
+ "commerce",
815
+ config.cartScope,
816
+ "products",
817
+ after
818
+ ],
819
+ queryFn: ({ signal }) => request(`/products?${urlParams}`, {
820
+ signal,
821
+ headers: { "X-Commerce-Scope": config.cartScope }
822
+ })
823
+ });
824
+ function goToPage(cursor) {
825
+ const next = new URLSearchParams(params);
826
+ if (cursor) next.set("after", cursor);
827
+ else next.delete("after");
828
+ setParams(next);
829
+ }
830
+ const nodes = products.data?.skuGroups?.edges?.flatMap((edge) => edge?.node?.id ? [edge.node] : []) ?? [];
831
+ const pageInfo = products.data?.skuGroups?.pageInfo;
832
+ return /* @__PURE__ */ jsxs("section", { children: [
833
+ showHeader && /* @__PURE__ */ jsxs("header", {
834
+ className: "border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8",
835
+ children: [/* @__PURE__ */ jsx("h1", {
836
+ className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
837
+ children: title
838
+ }), /* @__PURE__ */ jsx("p", {
839
+ className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
840
+ children: description
841
+ })]
842
+ }),
843
+ /* @__PURE__ */ jsx("div", {
844
+ className: "border-b border-neutral-200 py-6 sm:py-8",
845
+ children: /* @__PURE__ */ jsx("p", {
846
+ className: "text-sm",
847
+ role: "status",
848
+ children: products.isPending ? "Loading products…" : products.isError ? "Products unavailable" : `Showing ${nodes.length} product${nodes.length === 1 ? "" : "s"}`
849
+ })
850
+ }),
851
+ products.isPending && /* @__PURE__ */ jsx("div", {
852
+ "aria-hidden": "true",
853
+ className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3",
854
+ children: [
855
+ 0,
856
+ 1,
857
+ 2
858
+ ].map((item) => /* @__PURE__ */ jsxs("div", {
859
+ className: "overflow-hidden rounded-lg border border-neutral-200 motion-safe:animate-pulse",
860
+ children: [/* @__PURE__ */ jsx("div", { className: "aspect-square bg-neutral-100" }), /* @__PURE__ */ jsxs("div", {
861
+ className: "space-y-4 p-6",
862
+ children: [
863
+ /* @__PURE__ */ jsx("div", { className: "h-6 w-2/3 rounded bg-neutral-100" }),
864
+ /* @__PURE__ */ jsx("div", { className: "h-4 rounded bg-neutral-100" }),
865
+ /* @__PURE__ */ jsx("div", { className: "h-11 rounded bg-neutral-100" })
866
+ ]
867
+ })]
868
+ }, item))
869
+ }),
870
+ products.isError && /* @__PURE__ */ jsxs("div", {
871
+ role: "alert",
872
+ className: "rounded-lg bg-red-50 p-5 text-red-800",
873
+ children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
874
+ type: "button",
875
+ className: "min-h-11 bg-red-50 text-red-800 underline",
876
+ onClick: () => void products.refetch(),
877
+ children: "Retry products"
878
+ })]
879
+ }),
880
+ products.isSuccess && !nodes.length && /* @__PURE__ */ jsx("p", {
881
+ className: "py-16 text-center",
882
+ children: "No products available."
883
+ }),
884
+ /* @__PURE__ */ jsx("div", {
885
+ className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10",
886
+ children: nodes.map((product) => /* @__PURE__ */ jsx(ProductCard, { product }, product.id))
887
+ }),
888
+ /* @__PURE__ */ jsxs("nav", {
889
+ "aria-label": "Product pages",
890
+ className: "flex justify-between border-t border-neutral-200 pt-6",
891
+ children: [after ? /* @__PURE__ */ jsx("button", {
892
+ type: "button",
893
+ className: buttonClass,
894
+ onClick: () => goToPage(),
895
+ children: "First page"
896
+ }) : /* @__PURE__ */ jsx("span", {}), pageInfo?.hasNextPage && pageInfo.endCursor && /* @__PURE__ */ jsx("button", {
897
+ type: "button",
898
+ className: buttonClass,
899
+ onClick: () => goToPage(pageInfo.endCursor ?? void 0),
900
+ children: "Next page"
901
+ })]
902
+ })
903
+ ] });
904
+ }
905
+
906
+ //#endregion
907
+ //#region src/commerce-storefront.tsx
908
+ /** Mount once inside the application's Router and QueryClientProvider. */
909
+ function CommerceStorefront({ children,...props }) {
910
+ return /* @__PURE__ */ jsxs(CommerceProvider, {
911
+ ...props,
912
+ children: [children, /* @__PURE__ */ jsx(CartDrawer, {})]
913
+ });
914
+ }
915
+
916
+ //#endregion
917
+ //#region src/product-details.tsx
918
+ function ProductDetails() {
919
+ const { productId = "" } = useParams();
920
+ const { connection } = useCommerce();
921
+ return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(ProductDetailsContent, { productId }, productId) : /* @__PURE__ */ jsx(CommerceStatus, {}) });
922
+ }
923
+ function ProductDetailsContent({ productId }) {
924
+ const { config } = useCommerce();
925
+ const fieldId = useId();
926
+ const [params, setParams] = useSearchParams();
927
+ const [quantity, setQuantity] = useState(1);
928
+ const [imageIndex, setImageIndex] = useState(0);
929
+ const product = useQuery({
930
+ retry: false,
931
+ queryKey: [
932
+ "commerce",
933
+ config.cartScope,
934
+ "product",
935
+ productId
936
+ ],
937
+ queryFn: ({ signal }) => request(`/products/${encodeURIComponent(productId)}`, {
938
+ signal,
939
+ headers: { "X-Commerce-Scope": config.cartScope }
940
+ })
941
+ });
942
+ const group = product.data?.skuGroup;
943
+ const productUnavailable = group?.status != null && group.status !== "ACTIVE";
944
+ const attributes = getProductAttributes(group);
945
+ const skuOptions = getLabeledSkuOptions(group);
946
+ const explicitSku = skuOptions.find((sku) => sku.id === params.get("sku"));
947
+ const selections = attributes.map((attribute) => params.get(`option.${attribute.name}`) ?? "");
948
+ const complete = skuOptions.length > 0 ? !!explicitSku : attributes.every((attribute, index) => attribute.values.some((value) => value.name === selections[index]));
949
+ const selectionParams = new URLSearchParams();
950
+ selections.filter(Boolean).forEach((value) => {
951
+ selectionParams.append("attributeValues", value);
952
+ });
953
+ const matched = useQuery({
954
+ retry: false,
955
+ queryKey: [
956
+ "commerce",
957
+ config.cartScope,
958
+ "product-variants",
959
+ productId,
960
+ selections
961
+ ],
962
+ queryFn: ({ signal }) => request(`/products/${encodeURIComponent(productId)}?${selectionParams}`, {
963
+ signal,
964
+ headers: { "X-Commerce-Scope": config.cartScope }
965
+ }),
966
+ enabled: !!group && attributes.length > 0 && complete
967
+ });
968
+ const selectionVerified = complete && product.isSuccess && !product.isFetching && (attributes.length === 0 || matched.isSuccess && !matched.isFetching);
969
+ const selectedSku = selectionVerified ? explicitSku ?? getSingleMatchedSku(attributes.length ? matched.data?.skuGroup : group) : null;
970
+ const skuId = selectedSku?.id ?? null;
971
+ const skuPrice = selectedSku?.prices?.edges?.find((edge) => edge?.node?.value)?.node;
972
+ const selectedImages = getImageUrls(selectedSku);
973
+ const images = [...new Set(selectedImages.length ? selectedImages : getImageUrls(group))];
974
+ const available = getAvailableInventoryQuantity(selectedSku);
975
+ const name = group?.label ?? group?.name ?? "Product";
976
+ const selectOption = (attribute, value) => {
977
+ const next = new URLSearchParams(params);
978
+ if (value) next.set(`option.${attribute}`, value);
979
+ else next.delete(`option.${attribute}`);
980
+ setParams(next);
981
+ setQuantity(1);
982
+ setImageIndex(0);
983
+ };
984
+ if (product.isPending) return /* @__PURE__ */ jsx("p", {
985
+ role: "status",
986
+ children: "Loading product…"
987
+ });
988
+ if (product.isError) return /* @__PURE__ */ jsxs("div", {
989
+ role: "alert",
990
+ children: [
991
+ /* @__PURE__ */ jsx("h1", {
992
+ className: "text-2xl font-semibold",
993
+ children: "Product unavailable"
994
+ }),
995
+ /* @__PURE__ */ jsx("p", {
996
+ className: "my-4 text-red-700",
997
+ children: message(product.error)
998
+ }),
999
+ /* @__PURE__ */ jsx("button", {
1000
+ type: "button",
1001
+ className: buttonClass,
1002
+ onClick: () => void product.refetch(),
1003
+ children: "Retry product"
1004
+ })
1005
+ ]
1006
+ });
1007
+ if (!group) return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
1008
+ className: "text-2xl font-semibold",
1009
+ children: "Product not found"
1010
+ }), /* @__PURE__ */ jsx(Link, {
1011
+ className: `${buttonClass} mt-4`,
1012
+ to: config.catalogPath,
1013
+ children: "Back to shop"
1014
+ })] });
1015
+ return /* @__PURE__ */ jsxs("article", { children: [/* @__PURE__ */ jsx(Link, {
1016
+ className: "mb-8 inline-flex min-h-11 items-center text-sm text-inherit underline underline-offset-4 focus-visible:outline focus-visible:outline-2 sm:mb-10",
1017
+ to: config.catalogPath,
1018
+ children: "← Back to shop"
1019
+ }), /* @__PURE__ */ jsxs("div", {
1020
+ className: "grid items-start gap-8 lg:grid-cols-12 lg:gap-12",
1021
+ children: [/* @__PURE__ */ jsxs("div", {
1022
+ className: "min-w-0 lg:col-span-7",
1023
+ children: [/* @__PURE__ */ jsx("div", {
1024
+ className: "aspect-square overflow-hidden rounded-lg",
1025
+ children: /* @__PURE__ */ jsx(ProductImage, {
1026
+ url: images[imageIndex] ?? images[0],
1027
+ name
1028
+ }, images[imageIndex] ?? images[0])
1029
+ }), images.length > 1 && /* @__PURE__ */ jsx("div", {
1030
+ className: "mt-4 flex gap-3 overflow-x-auto",
1031
+ children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
1032
+ type: "button",
1033
+ 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"}`,
1034
+ "aria-label": `View image ${index + 1}`,
1035
+ "aria-pressed": imageIndex === index,
1036
+ onClick: () => setImageIndex(index),
1037
+ children: /* @__PURE__ */ jsx(ProductImage, {
1038
+ url,
1039
+ name: ""
1040
+ })
1041
+ }, url))
1042
+ })]
1043
+ }), /* @__PURE__ */ jsxs("div", {
1044
+ className: "min-w-0 lg:col-span-5 lg:py-4",
1045
+ children: [
1046
+ /* @__PURE__ */ jsx("h1", {
1047
+ className: "break-words text-4xl font-semibold leading-tight sm:text-5xl",
1048
+ children: name
1049
+ }),
1050
+ /* @__PURE__ */ jsxs("div", {
1051
+ "aria-live": "polite",
1052
+ className: "mt-6 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium",
1053
+ "data-testid": "product-price",
1054
+ children: [skuPrice?.value?.value != null ? money(skuPrice.value.value, skuPrice.value.currencyCode ?? config.currencyCode) : group.priceRange?.min != null ? `From ${money(group.priceRange.min, config.currencyCode)}` : "Price unavailable", skuPrice?.compareAtValue?.value != null && skuPrice.value?.value != null && skuPrice.compareAtValue.value > skuPrice.value.value && /* @__PURE__ */ jsx("del", {
1055
+ className: "text-lg font-normal",
1056
+ children: money(skuPrice.compareAtValue.value, skuPrice.compareAtValue.currencyCode ?? config.currencyCode)
1057
+ })]
1058
+ }),
1059
+ /* @__PURE__ */ jsx("p", {
1060
+ className: "my-8 whitespace-pre-line text-base leading-7",
1061
+ children: selectedSku?.description ?? group.description
1062
+ }),
1063
+ /* @__PURE__ */ jsxs("div", {
1064
+ className: "space-y-6 border-t border-neutral-200 pt-8",
1065
+ children: [
1066
+ attributes.map((attribute, index) => /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", {
1067
+ className: "mb-2 font-medium",
1068
+ children: attribute.label
1069
+ }), /* @__PURE__ */ jsxs("select", {
1070
+ className: `${inputClass} w-full`,
1071
+ "aria-label": attribute.label,
1072
+ value: selections[index],
1073
+ onChange: (event) => selectOption(attribute.name, event.target.value),
1074
+ children: [/* @__PURE__ */ jsxs("option", {
1075
+ value: "",
1076
+ children: ["Select ", attribute.label.toLowerCase()]
1077
+ }), attribute.values.map((value) => /* @__PURE__ */ jsx("option", {
1078
+ value: value.name,
1079
+ children: value.label
1080
+ }, value.id || value.name))]
1081
+ })] }, attribute.id || attribute.name)),
1082
+ skuOptions.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
1083
+ htmlFor: `${fieldId}-sku-option`,
1084
+ className: "mb-2 block font-medium",
1085
+ children: "Option"
1086
+ }), /* @__PURE__ */ jsxs("select", {
1087
+ id: `${fieldId}-sku-option`,
1088
+ className: `${inputClass} w-full`,
1089
+ value: explicitSku?.id ?? "",
1090
+ onChange: (event) => {
1091
+ const next = new URLSearchParams(params);
1092
+ if (event.target.value) next.set("sku", event.target.value);
1093
+ else next.delete("sku");
1094
+ setParams(next);
1095
+ setQuantity(1);
1096
+ setImageIndex(0);
1097
+ },
1098
+ children: [/* @__PURE__ */ jsx("option", {
1099
+ value: "",
1100
+ children: "Select an option"
1101
+ }), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
1102
+ value: sku.id ?? "",
1103
+ children: sku.label?.trim() || sku.name?.trim()
1104
+ }, sku.id))]
1105
+ })] }),
1106
+ matched.isError ? /* @__PURE__ */ jsxs("div", {
1107
+ role: "alert",
1108
+ className: "text-red-700",
1109
+ children: [/* @__PURE__ */ jsx("p", { children: message(matched.error) }), /* @__PURE__ */ jsx("button", {
1110
+ type: "button",
1111
+ className: "min-h-11 bg-white text-red-700 underline",
1112
+ onClick: () => {
1113
+ if (matched.isError) matched.refetch();
1114
+ },
1115
+ children: "Retry availability"
1116
+ })]
1117
+ }) : null,
1118
+ !complete && /* @__PURE__ */ jsx("p", {
1119
+ id: `${fieldId}-variant-guidance`,
1120
+ role: "status",
1121
+ className: "text-sm",
1122
+ children: "Select all options to add this item to your cart."
1123
+ }),
1124
+ selectionVerified && attributes.length > 0 && !skuId && /* @__PURE__ */ jsx("p", {
1125
+ role: "status",
1126
+ className: "text-sm text-red-700",
1127
+ children: "This combination is unavailable. Choose different options."
1128
+ }),
1129
+ selectionVerified && attributes.length === 0 && !skuId && /* @__PURE__ */ jsx("p", {
1130
+ className: "text-sm",
1131
+ children: "This product is not currently available to purchase."
1132
+ }),
1133
+ complete && (product.isFetching || matched.isFetching) && /* @__PURE__ */ jsx("p", {
1134
+ role: "status",
1135
+ children: "Checking this variant…"
1136
+ }),
1137
+ selectedSku && /* @__PURE__ */ jsxs(Fragment, { children: [
1138
+ /* @__PURE__ */ jsx("p", {
1139
+ className: "text-sm",
1140
+ "aria-live": "polite",
1141
+ children: available === null ? "Available to order" : available === 0 ? "This variant is out of stock" : `${available} available`
1142
+ }),
1143
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
1144
+ htmlFor: `${fieldId}-quantity`,
1145
+ className: "mb-2 block font-medium",
1146
+ children: "Quantity"
1147
+ }), /* @__PURE__ */ jsx("input", {
1148
+ id: `${fieldId}-quantity`,
1149
+ type: "number",
1150
+ min: 1,
1151
+ max: available ?? void 0,
1152
+ step: 1,
1153
+ className: `${inputClass} w-28`,
1154
+ value: Number.isNaN(quantity) ? "" : quantity,
1155
+ onChange: (event) => setQuantity(event.target.valueAsNumber)
1156
+ })] }),
1157
+ productUnavailable ? /* @__PURE__ */ jsx("p", {
1158
+ className: "text-sm",
1159
+ children: "This product is no longer available."
1160
+ }) : /* @__PURE__ */ jsx(AddToCartButton, {
1161
+ sku: selectedSku,
1162
+ name,
1163
+ quantity
1164
+ })
1165
+ ] }),
1166
+ !selectedSku && (productUnavailable ? /* @__PURE__ */ jsx("p", {
1167
+ className: "text-sm",
1168
+ children: "This product is no longer available."
1169
+ }) : /* @__PURE__ */ jsx("button", {
1170
+ type: "button",
1171
+ className: `${buttonClass} w-full`,
1172
+ disabled: true,
1173
+ "aria-describedby": !complete ? `${fieldId}-variant-guidance` : void 0,
1174
+ children: "Add to cart"
1175
+ }))
1176
+ ]
1177
+ })
1178
+ ]
1179
+ })]
1180
+ })] });
1181
+ }
1182
+
1183
+ //#endregion
1184
+ export { AddToCartButton, ApiError, CartButton, CartDrawer, Catalog, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductDetails, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCommerce };