@godaddy/gd-commerce-storefront 0.9.0 → 0.11.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 +100 -9
- package/dist/index.d.ts +256 -163
- package/dist/index.js +756 -249
- package/dist/messages/fr-FR.d.ts +7 -0
- package/dist/messages/fr-FR.js +143 -0
- package/dist/messages-CHl-bWx9.d.ts +152 -0
- package/dist/styles.css +6 -0
- package/package.json +13 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,190 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
import * as Dialog from "@radix-ui/react-dialog";
|
|
5
5
|
import { Link, useParams, useSearchParams } from "react-router";
|
|
6
6
|
|
|
7
|
+
//#region src/messages.ts
|
|
8
|
+
const englishText = {
|
|
9
|
+
connectPrompt: "Connect a store to show products.",
|
|
10
|
+
connecting: "Connecting to the store…",
|
|
11
|
+
retryConnection: "Retry connection",
|
|
12
|
+
connectionSlow: "The store is taking too long to connect.",
|
|
13
|
+
invalidConfiguration: "The store returned invalid configuration.",
|
|
14
|
+
requestFailed: "Commerce request failed. Please retry.",
|
|
15
|
+
requestFailedStatus: "Commerce request failed ({status}). Please retry.",
|
|
16
|
+
requestTimeout: "Commerce information took too long to load. Please retry.",
|
|
17
|
+
unknownError: "Something went wrong. Please retry.",
|
|
18
|
+
storageUnavailable: "Your browser cannot save the cart. Keep this page open while shopping.",
|
|
19
|
+
invalidAddItem: "Select a product variant and a positive quantity.",
|
|
20
|
+
addReturnedNoCart: "Commerce did not return a cart with items. Refresh before retrying.",
|
|
21
|
+
itemNotInCart: "This item is no longer in the cart.",
|
|
22
|
+
invalidQuantity: "Quantity must be a positive whole number.",
|
|
23
|
+
updateReturnedNoCart: "Commerce did not return the updated cart. Refresh before retrying.",
|
|
24
|
+
discountMissing: "Add an item and enter a promotion code.",
|
|
25
|
+
discountReturnedNoCart: "Commerce did not return the updated cart.",
|
|
26
|
+
checkoutDisabled: "Checkout has not been enabled for this store.",
|
|
27
|
+
checkoutEmpty: "Add an item before checking out.",
|
|
28
|
+
checkoutNoUrl: "Commerce did not return a checkout URL.",
|
|
29
|
+
checkoutInvalidUrl: "Commerce returned an invalid checkout URL.",
|
|
30
|
+
itemAdded: "{name} added to your cart.",
|
|
31
|
+
sampleItemAdded: "{name} added to your sample cart.",
|
|
32
|
+
itemRemoved: "Item removed from cart.",
|
|
33
|
+
quantityUpdated: "Cart quantity updated.",
|
|
34
|
+
discountsUpdated: "Promotion codes updated.",
|
|
35
|
+
sampleQuantityUpdated: "Sample cart quantity updated.",
|
|
36
|
+
sampleItemRemoved: "Sample item removed from cart.",
|
|
37
|
+
cartButtonLabel: {
|
|
38
|
+
one: "Cart, {count} item",
|
|
39
|
+
other: "Cart, {count} items"
|
|
40
|
+
},
|
|
41
|
+
addToCart: "Add to cart",
|
|
42
|
+
addToCartWithPrice: "Add to cart · {price}",
|
|
43
|
+
addToCartWithPriceName: "Add to cart, {price}",
|
|
44
|
+
adding: "Adding…",
|
|
45
|
+
outOfStock: "Out of stock",
|
|
46
|
+
addReasonUnbound: "Connect a store to add items to the cart.",
|
|
47
|
+
addReasonError: "The store could not be reached.",
|
|
48
|
+
addReasonNoSku: "This item cannot be added to the cart.",
|
|
49
|
+
loadingCart: "Loading your cart…",
|
|
50
|
+
quantityNotWhole: "Enter a whole number of 1 or more.",
|
|
51
|
+
quantityOverStock: "Only {count} available.",
|
|
52
|
+
cartTitle: "Your cart",
|
|
53
|
+
sampleCartTitle: "Sample cart",
|
|
54
|
+
cartDescription: "Review your items before checkout.",
|
|
55
|
+
sampleCartDescription: "Example products and prices in {currency}. Nothing can be purchased.",
|
|
56
|
+
closeCart: "Close cart",
|
|
57
|
+
refreshCart: "Refresh cart",
|
|
58
|
+
priceUnavailable: "Price unavailable",
|
|
59
|
+
decreaseQuantity: "Decrease quantity of {name}",
|
|
60
|
+
increaseQuantity: "Increase quantity of {name}",
|
|
61
|
+
quantityPrefix: "Quantity: ",
|
|
62
|
+
remove: "Remove",
|
|
63
|
+
itemOption: "{attribute}: {values}",
|
|
64
|
+
cartEmpty: "Your cart is empty",
|
|
65
|
+
cartEmptyHint: "Add something you love and it will show up here.",
|
|
66
|
+
sampleCartEmptyHint: "Add a sample product to try the cart.",
|
|
67
|
+
browseProducts: "Browse products",
|
|
68
|
+
subtotal: "Subtotal",
|
|
69
|
+
sampleSubtotal: "Sample subtotal",
|
|
70
|
+
checkoutAdjustments: "Shipping, taxes & discounts",
|
|
71
|
+
checkoutAdjustmentsValue: "Calculated at checkout",
|
|
72
|
+
sampleCheckoutNote: "Example total in {currency}. Checkout is not set up yet.",
|
|
73
|
+
checkoutUnavailable: "Checkout is not available yet.",
|
|
74
|
+
checkout: "Proceed to checkout",
|
|
75
|
+
openingCheckout: "Opening checkout…",
|
|
76
|
+
continueShopping: "Continue shopping",
|
|
77
|
+
catalogTitle: "Shop all products",
|
|
78
|
+
catalogDescription: "Explore the collection and find your favorites.",
|
|
79
|
+
sampleCatalogSummary: "Sample products · Example prices in {currency}",
|
|
80
|
+
loadingProducts: "Loading products…",
|
|
81
|
+
productsUnavailable: "Products unavailable",
|
|
82
|
+
productCount: {
|
|
83
|
+
one: "Showing {count} product",
|
|
84
|
+
other: "Showing {count} products"
|
|
85
|
+
},
|
|
86
|
+
retryProducts: "Retry products",
|
|
87
|
+
noProducts: "No products available.",
|
|
88
|
+
productPages: "Product pages",
|
|
89
|
+
firstPage: "First page",
|
|
90
|
+
nextPage: "Next page",
|
|
91
|
+
sampleProductName: "Product {number}",
|
|
92
|
+
relatedTitle: "You may also like",
|
|
93
|
+
imageUnavailable: "{name}: image unavailable",
|
|
94
|
+
noImage: "No image available",
|
|
95
|
+
sale: "Sale",
|
|
96
|
+
wasPrice: "Was",
|
|
97
|
+
swatchSummary: {
|
|
98
|
+
one: "{count} {attribute} option: {values}",
|
|
99
|
+
other: "{count} {attribute} options: {values}"
|
|
100
|
+
},
|
|
101
|
+
chooseOptions: "Choose options",
|
|
102
|
+
viewProduct: "View product",
|
|
103
|
+
addSampleToCart: "Add {name} to sample cart",
|
|
104
|
+
sampleItemName: "({name}, sample)",
|
|
105
|
+
loadingProduct: "Loading product…",
|
|
106
|
+
productUnavailable: "Product unavailable",
|
|
107
|
+
retryProduct: "Retry product",
|
|
108
|
+
productNotFound: "Product not found",
|
|
109
|
+
backToShop: "Back to shop",
|
|
110
|
+
viewImage: "View image {number}",
|
|
111
|
+
priceFrom: "From {price}",
|
|
112
|
+
selectAllOptions: "Select all options to add this item to your cart.",
|
|
113
|
+
checkingVariant: "Checking this variant…",
|
|
114
|
+
variantCheckFailed: "Availability for this selection could not be checked.",
|
|
115
|
+
combinationUnavailable: "This combination is unavailable. Choose different options.",
|
|
116
|
+
notPurchasable: "This product is not currently available to purchase.",
|
|
117
|
+
selectAttribute: "Select {attribute}",
|
|
118
|
+
selectAttributeError: "Select {attribute}.",
|
|
119
|
+
selectOption: "Select an option",
|
|
120
|
+
skuOptionLabel: "Option",
|
|
121
|
+
soldOutOption: "{label} (sold out)",
|
|
122
|
+
soldOutChoice: "{label}, sold out",
|
|
123
|
+
retryAvailability: "Retry availability",
|
|
124
|
+
availableToOrder: "Available to order",
|
|
125
|
+
variantOutOfStock: "This variant is out of stock",
|
|
126
|
+
availableCount: "{count} available",
|
|
127
|
+
quantity: "Quantity"
|
|
128
|
+
};
|
|
129
|
+
/** The package's English text, the default for every key a site does not set. */
|
|
130
|
+
const enUS = {
|
|
131
|
+
locale: "en-US",
|
|
132
|
+
errorCodes: {},
|
|
133
|
+
...englishText
|
|
134
|
+
};
|
|
135
|
+
/** `key` from `messages`, with its plural form chosen by `values.count` and its placeholders filled in. */
|
|
136
|
+
function formatMessage(messages, key, values = {}) {
|
|
137
|
+
return (pickText(messages[key], values.count, messages.locale) ?? pickText(enUS[key], values.count, enUS.locale) ?? key).replace(/\{(\w+)\}/g, (placeholder, name) => {
|
|
138
|
+
const value = values[name];
|
|
139
|
+
if (value === void 0) return placeholder;
|
|
140
|
+
return typeof value === "number" ? numberFormat(messages.locale).format(value) : value;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function pickText(message, count, locale) {
|
|
144
|
+
if (typeof message === "string") return message;
|
|
145
|
+
if (typeof message !== "object" || message === null) return void 0;
|
|
146
|
+
const forms = message;
|
|
147
|
+
const form = typeof count === "number" ? pluralRules(locale).select(count) : void 0;
|
|
148
|
+
const chosen = form && forms[form];
|
|
149
|
+
if (typeof chosen === "string" && chosen) return chosen;
|
|
150
|
+
return typeof forms.other === "string" ? forms.other : void 0;
|
|
151
|
+
}
|
|
152
|
+
/** Whether the text is English, the language of the server's own error messages. */
|
|
153
|
+
const isEnglish = (messages) => /^en(?:-|$)/i.test(messages.locale);
|
|
154
|
+
/** An error whose text is a package message, so it shows in the provider's language. */
|
|
155
|
+
var StorefrontError = class extends Error {
|
|
156
|
+
constructor(key, values = {}, options) {
|
|
157
|
+
super(formatMessage(enUS, key, values), options);
|
|
158
|
+
this.key = key;
|
|
159
|
+
this.values = values;
|
|
160
|
+
this.name = "StorefrontError";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const pluralRulesCache = /* @__PURE__ */ new Map();
|
|
164
|
+
const numberFormatCache = /* @__PURE__ */ new Map();
|
|
165
|
+
function pluralRules(locale) {
|
|
166
|
+
let rules = pluralRulesCache.get(locale);
|
|
167
|
+
if (!rules) {
|
|
168
|
+
rules = new Intl.PluralRules(safeLocale(locale));
|
|
169
|
+
pluralRulesCache.set(locale, rules);
|
|
170
|
+
}
|
|
171
|
+
return rules;
|
|
172
|
+
}
|
|
173
|
+
function numberFormat(locale) {
|
|
174
|
+
let format = numberFormatCache.get(locale);
|
|
175
|
+
if (!format) {
|
|
176
|
+
format = new Intl.NumberFormat(safeLocale(locale));
|
|
177
|
+
numberFormatCache.set(locale, format);
|
|
178
|
+
}
|
|
179
|
+
return format;
|
|
180
|
+
}
|
|
181
|
+
/** `locale`, or `en-US` when it is not a valid tag, so a typo in a message file cannot break rendering. */
|
|
182
|
+
function safeLocale(locale) {
|
|
183
|
+
try {
|
|
184
|
+
return Intl.getCanonicalLocales(locale)[0] ?? "en-US";
|
|
185
|
+
} catch {
|
|
186
|
+
return "en-US";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
7
191
|
//#region src/api.ts
|
|
8
192
|
var CartIdStorage = class {
|
|
9
193
|
currentId = null;
|
|
@@ -24,9 +208,15 @@ var CartIdStorage = class {
|
|
|
24
208
|
}
|
|
25
209
|
};
|
|
26
210
|
var ApiError = class extends Error {
|
|
27
|
-
|
|
28
|
-
|
|
211
|
+
/** The failure response's `code`, such as `not_found`, when the server sent one. */
|
|
212
|
+
code;
|
|
213
|
+
/** Package text to show in place of `message`, for failures the package describes itself. */
|
|
214
|
+
text;
|
|
215
|
+
constructor(message, status, options) {
|
|
216
|
+
super(message, options);
|
|
29
217
|
this.status = status;
|
|
218
|
+
this.code = options?.code;
|
|
219
|
+
this.text = options?.text;
|
|
30
220
|
}
|
|
31
221
|
};
|
|
32
222
|
const checkedFetch = async (input, init) => {
|
|
@@ -37,10 +227,23 @@ const checkedFetch = async (input, init) => {
|
|
|
37
227
|
try {
|
|
38
228
|
return JSON.parse(data);
|
|
39
229
|
} catch (cause) {
|
|
40
|
-
|
|
230
|
+
const text = {
|
|
231
|
+
key: "requestFailedStatus",
|
|
232
|
+
values: { status: response.status }
|
|
233
|
+
};
|
|
234
|
+
throw new ApiError(formatMessage(enUS, text.key, text.values), response.status, {
|
|
235
|
+
cause,
|
|
236
|
+
text
|
|
237
|
+
});
|
|
41
238
|
}
|
|
42
239
|
})();
|
|
43
|
-
|
|
240
|
+
const body = typeof parsed === "object" && parsed !== null ? { ...parsed } : {};
|
|
241
|
+
const code = typeof body.code === "string" ? body.code : void 0;
|
|
242
|
+
if (typeof body.error === "string") throw new ApiError(body.error, response.status, { code });
|
|
243
|
+
throw new ApiError(formatMessage(enUS, "requestFailed"), response.status, {
|
|
244
|
+
code,
|
|
245
|
+
text: { key: "requestFailed" }
|
|
246
|
+
});
|
|
44
247
|
}
|
|
45
248
|
return response;
|
|
46
249
|
};
|
|
@@ -57,22 +260,65 @@ async function request(path, init) {
|
|
|
57
260
|
cache: "no-store"
|
|
58
261
|
})).json();
|
|
59
262
|
} catch (cause) {
|
|
60
|
-
if (timeout?.aborted && !init?.signal?.aborted) throw new ApiError(
|
|
263
|
+
if (timeout?.aborted && !init?.signal?.aborted) throw new ApiError(formatMessage(enUS, "requestTimeout"), 408, {
|
|
264
|
+
cause,
|
|
265
|
+
text: { key: "requestTimeout" }
|
|
266
|
+
});
|
|
61
267
|
throw cause;
|
|
62
268
|
}
|
|
63
269
|
}
|
|
270
|
+
const currencyFormats = /* @__PURE__ */ new Map();
|
|
271
|
+
/** A page uses a few pairs; the limit bounds a long-running server rendering many locales and currencies. */
|
|
272
|
+
const maxCurrencyFormats = 64;
|
|
273
|
+
function currencyFormat(currency, locale) {
|
|
274
|
+
const key = `${locale}|${currency}`;
|
|
275
|
+
let cached = currencyFormats.get(key);
|
|
276
|
+
if (!cached) {
|
|
277
|
+
const format = new Intl.NumberFormat(locale, {
|
|
278
|
+
style: "currency",
|
|
279
|
+
currency
|
|
280
|
+
});
|
|
281
|
+
cached = {
|
|
282
|
+
format,
|
|
283
|
+
scale: 10 ** (format.resolvedOptions().maximumFractionDigits ?? 2)
|
|
284
|
+
};
|
|
285
|
+
if (currencyFormats.size >= maxCurrencyFormats) currencyFormats.delete(currencyFormats.keys().next().value ?? "");
|
|
286
|
+
currencyFormats.set(key, cached);
|
|
287
|
+
}
|
|
288
|
+
return cached;
|
|
289
|
+
}
|
|
64
290
|
/** Formats minor units, such as cents, as currency. Inside the provider, prefer `useMoney`, which follows its `locale`. */
|
|
65
291
|
function money(value, currency, locale = "en-US") {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
currency
|
|
69
|
-
});
|
|
70
|
-
const digits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
|
|
71
|
-
return formatter.format(value / 10 ** digits);
|
|
292
|
+
const { format, scale } = currencyFormat(currency, locale);
|
|
293
|
+
return format.format(value / scale);
|
|
72
294
|
}
|
|
73
|
-
|
|
74
|
-
|
|
295
|
+
/** Formats a range of minor units the way `locale` writes ranges, such as `$20.00 – $25.00` or `20,00–25,00 €`. */
|
|
296
|
+
function moneyRange(min, max, currency, locale = "en-US") {
|
|
297
|
+
const { format, scale } = currencyFormat(currency, locale);
|
|
298
|
+
if (min === max) return format.format(min / scale);
|
|
299
|
+
try {
|
|
300
|
+
if (typeof format.formatRange === "function") return format.formatRange(min / scale, max / scale);
|
|
301
|
+
} catch {}
|
|
302
|
+
return `${format.format(min / scale)} – ${format.format(max / scale)}`;
|
|
75
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* The text to show for a failure, in the language of `messages`. Server messages are English, so other languages
|
|
306
|
+
* show the text for the response's `code`, or a generic message.
|
|
307
|
+
*/
|
|
308
|
+
function describeError(error, messages) {
|
|
309
|
+
const codes = messages.errorCodes;
|
|
310
|
+
const coded = error instanceof ApiError && error.code && codes && Object.hasOwn(codes, error.code) ? codes[error.code] : void 0;
|
|
311
|
+
if (typeof coded === "string" && coded) return coded;
|
|
312
|
+
const text = error instanceof StorefrontError || error instanceof ApiError ? textOf(error) : void 0;
|
|
313
|
+
if (text) return formatMessage(messages, text.key, text.values);
|
|
314
|
+
if (error instanceof ApiError) return isEnglish(messages) ? error.message : formatMessage(messages, "requestFailed");
|
|
315
|
+
if (error instanceof Error && isEnglish(messages)) return error.message;
|
|
316
|
+
return formatMessage(messages, "unknownError");
|
|
317
|
+
}
|
|
318
|
+
const textOf = (error) => error instanceof StorefrontError ? {
|
|
319
|
+
key: error.key,
|
|
320
|
+
values: error.values
|
|
321
|
+
} : error.text;
|
|
76
322
|
|
|
77
323
|
//#endregion
|
|
78
324
|
//#region src/cart-model.ts
|
|
@@ -85,9 +331,10 @@ async function addToCart(cartId, item, fetchFn = globalThis.fetch) {
|
|
|
85
331
|
body: JSON.stringify(body)
|
|
86
332
|
})).json();
|
|
87
333
|
}
|
|
88
|
-
|
|
334
|
+
/** The cart's totals in minor units. `currencyCode` is the cart's, or `fallbackCurrency`, such as the store's. */
|
|
335
|
+
function getCartSummaryTotals(order, fallbackCurrency = "USD") {
|
|
89
336
|
const lineItems = order?.lineItems ?? [];
|
|
90
|
-
const currencyCode = order?.totals?.total?.currencyCode ||
|
|
337
|
+
const currencyCode = order?.totals?.total?.currencyCode || fallbackCurrency;
|
|
91
338
|
return {
|
|
92
339
|
itemCount: lineItems.reduce((sum, item) => sum + (item.quantity || 0), 0),
|
|
93
340
|
currencyCode,
|
|
@@ -101,54 +348,130 @@ function getCartSummaryTotals(order) {
|
|
|
101
348
|
|
|
102
349
|
//#endregion
|
|
103
350
|
//#region src/sample-cart.tsx
|
|
104
|
-
|
|
351
|
+
/** The six packaged samples, "Product 1" to "Product 6", without images. */
|
|
352
|
+
const builtInSamples = Array.from({ length: 6 }, (_, index) => ({
|
|
105
353
|
key: `sample-${index + 1}`,
|
|
106
354
|
name: `Product ${index + 1}`,
|
|
107
355
|
price: (index + 1) * 1e3
|
|
108
356
|
}));
|
|
109
|
-
const
|
|
357
|
+
const builtInNumbers = new Map(builtInSamples.map((sample, index) => [sample.key, index + 1]));
|
|
358
|
+
const builtInNumber = (sample) => builtInNumbers.get(sample.key);
|
|
359
|
+
const isBuiltInSample = (sample) => builtInNumber(sample) !== void 0;
|
|
360
|
+
/** Sample prices are examples, in this currency whatever the store's. */
|
|
361
|
+
const sampleCurrency = "USD";
|
|
362
|
+
/** A sample's name. Packaged samples are named in the provider's language, such as "Product 1". */
|
|
363
|
+
const sampleName = (text, sample) => {
|
|
364
|
+
const number = builtInNumber(sample);
|
|
365
|
+
return number === void 0 ? sample.name : text("sampleProductName", { number });
|
|
366
|
+
};
|
|
367
|
+
const validPrice = (price) => Number.isSafeInteger(price) && price >= 0;
|
|
368
|
+
/** The first reason host samples could not be shown or added consistently, or `null` when they can. */
|
|
369
|
+
function sampleProblem(samples) {
|
|
370
|
+
if (!samples.length) return "CatalogRoot sampleProducts needs at least one product, or `true`.";
|
|
371
|
+
const keys = /* @__PURE__ */ new Set();
|
|
372
|
+
for (const [index, sample] of samples.entries()) {
|
|
373
|
+
if (typeof sample !== "object" || sample === null) return `CatalogRoot sampleProducts: entry ${index} is ${String(sample)}, not a sample.`;
|
|
374
|
+
if (typeof sample.key !== "string" || !sample.key || keys.has(sample.key)) return `CatalogRoot sampleProducts need unique, non-empty string keys, not "${String(sample.key)}".`;
|
|
375
|
+
if (builtInNumbers.has(sample.key)) return `CatalogRoot sampleProducts: "${sample.key}" is a packaged sample's key. Use another key.`;
|
|
376
|
+
keys.add(sample.key);
|
|
377
|
+
if (typeof sample.name !== "string" || !sample.name.trim()) return `CatalogRoot sampleProducts: "${sample.key}" needs a non-empty name.`;
|
|
378
|
+
for (const field of ["description", "imageUrl"]) if (sample[field] !== void 0 && typeof sample[field] !== "string") return `CatalogRoot sampleProducts: "${sample.key}" has a ${field} that is not a string.`;
|
|
379
|
+
for (const field of ["price", "compareAtPrice"]) {
|
|
380
|
+
const value = sample[field];
|
|
381
|
+
if ((field === "price" || value !== void 0) && !validPrice(value)) return `CatalogRoot sampleProducts: "${sample.key}" has an invalid ${field}, ${String(value)}. A price must be a whole number of minor units, 0 or more.`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const sameSample = (a, b) => {
|
|
387
|
+
if (a === b) return true;
|
|
388
|
+
if (typeof a !== "object" || !a || typeof b !== "object" || !b) return false;
|
|
389
|
+
const fields = Object.keys(a);
|
|
390
|
+
return fields.length === Object.keys(b).length && fields.every((field) => Object.is(a[field], b[field]));
|
|
391
|
+
};
|
|
392
|
+
/** Whether two sample lists have the same samples with the same field values, so an inline list can be reused. */
|
|
393
|
+
const sameSamples = (a, b) => a === b || a.length === b.length && a.every((sample, index) => sameSample(sample, b[index]));
|
|
394
|
+
const noLines = [];
|
|
110
395
|
const SampleCartContext = createContext(null);
|
|
111
396
|
function useSampleCart() {
|
|
112
397
|
const value = useContext(SampleCartContext);
|
|
113
398
|
if (!value) throw new Error("Mount CommerceProvider around sample products.");
|
|
114
399
|
return value;
|
|
115
400
|
}
|
|
116
|
-
function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
|
|
401
|
+
function SampleCartProvider({ enabled, messages, onAdd, announce, emit, children }) {
|
|
402
|
+
const text = useCallback((key, values) => formatMessage(messages, key, values), [messages]);
|
|
117
403
|
const session = useMemo(() => ({ enabled }), [enabled]);
|
|
118
404
|
const active = useRef(session);
|
|
119
405
|
active.current = session;
|
|
120
406
|
const [state, setState] = useState({
|
|
121
407
|
session,
|
|
122
|
-
|
|
408
|
+
lines: noLines
|
|
123
409
|
});
|
|
124
410
|
useEffect(() => {
|
|
125
411
|
active.current = session;
|
|
126
412
|
setState({
|
|
127
413
|
session,
|
|
128
|
-
|
|
414
|
+
lines: noLines
|
|
129
415
|
});
|
|
130
416
|
return () => {
|
|
131
417
|
active.current = null;
|
|
132
418
|
};
|
|
133
419
|
}, [session]);
|
|
134
|
-
const
|
|
135
|
-
const items = useMemo(() =>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
420
|
+
const lines = state.session === session && enabled ? state.lines : noLines;
|
|
421
|
+
const items = useMemo(() => [...lines].sort((a, b) => (builtInNumber(a.sample) ?? Number.POSITIVE_INFINITY) - (builtInNumber(b.sample) ?? Number.POSITIVE_INFINITY)).map(({ sample, quantity }) => ({
|
|
422
|
+
key: sample.key,
|
|
423
|
+
name: sampleName(text, sample),
|
|
424
|
+
price: sample.price,
|
|
425
|
+
quantity,
|
|
426
|
+
sample
|
|
427
|
+
})), [lines, text]);
|
|
428
|
+
const refresh = useCallback((samples) => {
|
|
429
|
+
const byKey = new Map(samples.map((sample) => [sample.key, sample]));
|
|
430
|
+
setState((previous) => {
|
|
431
|
+
if (previous.session !== session) return previous;
|
|
432
|
+
let changed = false;
|
|
433
|
+
const next = previous.lines.map((line) => {
|
|
434
|
+
const current = byKey.get(line.sample.key);
|
|
435
|
+
if (!current || current === line.sample) return line;
|
|
436
|
+
changed = true;
|
|
437
|
+
return {
|
|
438
|
+
...line,
|
|
439
|
+
sample: current
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
return changed ? {
|
|
443
|
+
session,
|
|
444
|
+
lines: next
|
|
445
|
+
} : previous;
|
|
446
|
+
});
|
|
447
|
+
}, [session]);
|
|
448
|
+
function update(key, quantity, added) {
|
|
449
|
+
if (!enabled || active.current !== session) return false;
|
|
450
|
+
if (!added && !lines.some((line) => line.sample.key === key)) return false;
|
|
141
451
|
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
|
|
142
452
|
setState((previous) => {
|
|
143
|
-
const current = previous.session === session ? previous.
|
|
144
|
-
const
|
|
453
|
+
const current = previous.session === session ? previous.lines : noLines;
|
|
454
|
+
const line = current.find((candidate) => candidate.sample.key === key);
|
|
455
|
+
const next = quantity ?? (line?.quantity ?? 0) + 1;
|
|
145
456
|
if (next > 999) return previous;
|
|
457
|
+
const kept = current.filter((candidate) => candidate !== line);
|
|
458
|
+
if (next === 0) return {
|
|
459
|
+
session,
|
|
460
|
+
lines: kept
|
|
461
|
+
};
|
|
462
|
+
if (!line && added) return {
|
|
463
|
+
session,
|
|
464
|
+
lines: [...current, {
|
|
465
|
+
sample: added,
|
|
466
|
+
quantity: next
|
|
467
|
+
}]
|
|
468
|
+
};
|
|
146
469
|
return {
|
|
147
470
|
session,
|
|
148
|
-
|
|
149
|
-
...
|
|
150
|
-
|
|
151
|
-
}
|
|
471
|
+
lines: current.map((candidate) => candidate === line ? {
|
|
472
|
+
...candidate,
|
|
473
|
+
quantity: next
|
|
474
|
+
} : candidate)
|
|
152
475
|
};
|
|
153
476
|
});
|
|
154
477
|
return true;
|
|
@@ -156,16 +479,16 @@ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
|
|
|
156
479
|
return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
|
|
157
480
|
value: {
|
|
158
481
|
items,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!
|
|
162
|
-
onAdd(
|
|
482
|
+
refresh,
|
|
483
|
+
add: (sample) => {
|
|
484
|
+
if (!update(sample.key, void 0, sample)) return false;
|
|
485
|
+
onAdd(sampleName(text, sample));
|
|
163
486
|
return true;
|
|
164
487
|
},
|
|
165
488
|
changeQuantity: async (key, quantity) => {
|
|
166
489
|
const updated = quantity >= 1 && update(key, quantity);
|
|
167
490
|
if (updated) {
|
|
168
|
-
announce("
|
|
491
|
+
announce("sampleQuantityUpdated");
|
|
169
492
|
emit({
|
|
170
493
|
type: "quantity-changed",
|
|
171
494
|
itemId: key,
|
|
@@ -178,7 +501,7 @@ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
|
|
|
178
501
|
remove: async (key) => {
|
|
179
502
|
const updated = update(key, 0);
|
|
180
503
|
if (updated) {
|
|
181
|
-
announce("
|
|
504
|
+
announce("sampleItemRemoved");
|
|
182
505
|
emit({
|
|
183
506
|
type: "item-removed",
|
|
184
507
|
itemId: key,
|
|
@@ -211,12 +534,50 @@ function useMoney() {
|
|
|
211
534
|
const { currencyCode } = config;
|
|
212
535
|
return useCallback((value, currency = currencyCode) => money(value, currency, locale), [locale, currencyCode]);
|
|
213
536
|
}
|
|
537
|
+
/** `useMoney` for a range, such as `$20.00 – $25.00`, written the way the provider's `locale` writes ranges. */
|
|
538
|
+
function useMoneyRange() {
|
|
539
|
+
const { locale, config } = useCommerce();
|
|
540
|
+
const { currencyCode } = config;
|
|
541
|
+
return useCallback((min, max, currency = currencyCode) => moneyRange(min, max, currency, locale), [locale, currencyCode]);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* The provider's text for `key`, with `{placeholders}` filled from `values`, the way the package's own parts read
|
|
545
|
+
* it. For custom parts that should match the package's language. English outside a provider.
|
|
546
|
+
*/
|
|
547
|
+
function useMessage() {
|
|
548
|
+
const messages = useContext(CommerceContext)?.messages ?? enUS;
|
|
549
|
+
return useCallback((key, values) => formatMessage(messages, key, values), [messages]);
|
|
550
|
+
}
|
|
214
551
|
const CONNECTING_FAILURE_GRACE_MS = 6e4;
|
|
215
552
|
/** Polls every five seconds for five minutes after the page loads, then every minute, and stops at thirty. */
|
|
216
553
|
const FAST_POLL_FOR_MS = 5 * 6e4;
|
|
217
554
|
const STOP_POLLING_AT_MS = 30 * 6e4;
|
|
218
555
|
/** A failed retry keeps showing connecting this long so the click visibly does something. */
|
|
219
556
|
const RETRY_FEEDBACK_MS = 1e3;
|
|
557
|
+
/**
|
|
558
|
+
* `messages` while its contents are unchanged, so an inline object, including inline `errorCodes` or plural forms,
|
|
559
|
+
* does not change every render. The kept value is state, so a render React discards cannot leave it behind.
|
|
560
|
+
*/
|
|
561
|
+
function useStableMessages(value) {
|
|
562
|
+
const [stable, setStable] = useState(value);
|
|
563
|
+
if (stable === value || sameMessages(stable, value)) return stable;
|
|
564
|
+
setStable(() => value);
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
/** Equal keys and values, comparing plain-object values, such as `errorCodes` and plural forms, one level deeper. */
|
|
568
|
+
function sameMessages(a, b, deeper = true) {
|
|
569
|
+
if (a === b) return true;
|
|
570
|
+
if (!a || !b) return false;
|
|
571
|
+
const left = a;
|
|
572
|
+
const right = b;
|
|
573
|
+
const keys = Object.keys(left);
|
|
574
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && (Object.is(left[key], right[key]) || deeper && isPlainObject(left[key]) && isPlainObject(right[key]) && sameMessages(left[key], right[key], false)));
|
|
575
|
+
}
|
|
576
|
+
function isPlainObject(value) {
|
|
577
|
+
if (typeof value !== "object" || value === null) return false;
|
|
578
|
+
const prototype = Object.getPrototypeOf(value);
|
|
579
|
+
return prototype === Object.prototype || prototype === null;
|
|
580
|
+
}
|
|
220
581
|
/** `locale` as a canonical tag, or `en-US` when it is not a valid one, so one bad prop cannot break every price. */
|
|
221
582
|
function resolveLocale(locale) {
|
|
222
583
|
if (!locale) return "en-US";
|
|
@@ -243,18 +604,28 @@ function callHost(callback) {
|
|
|
243
604
|
async function loadConfig(signal) {
|
|
244
605
|
const config = await request("/config", { signal });
|
|
245
606
|
if (config?.state === "unbound" || config?.state === "connecting") {
|
|
246
|
-
if ("cartScope" in config || "currencyCode" in config) throw new
|
|
607
|
+
if ("cartScope" in config || "currencyCode" in config) throw new StorefrontError("invalidConfiguration");
|
|
247
608
|
return { state: config.state };
|
|
248
609
|
}
|
|
249
|
-
if (!config || config.state !== void 0 && config.state !== "ready" || typeof config.cartScope !== "string" || !config.cartScope.trim() || typeof config.currencyCode !== "string" || !/^[A-Z]{3}$/.test(config.currencyCode)) throw new
|
|
610
|
+
if (!config || config.state !== void 0 && config.state !== "ready" || typeof config.cartScope !== "string" || !config.cartScope.trim() || typeof config.currencyCode !== "string" || !/^[A-Z]{3}$/.test(config.currencyCode)) throw new StorefrontError("invalidConfiguration");
|
|
250
611
|
return {
|
|
251
612
|
state: "ready",
|
|
252
613
|
cartScope: config.cartScope,
|
|
253
614
|
currencyCode: config.currencyCode
|
|
254
615
|
};
|
|
255
616
|
}
|
|
256
|
-
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, onItemAdded, onEvent }) {
|
|
617
|
+
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, messages, onItemAdded, onEvent }) {
|
|
257
618
|
const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
|
|
619
|
+
const stableMessages = useStableMessages(messages);
|
|
620
|
+
const resolvedMessages = useMemo(() => ({
|
|
621
|
+
...enUS,
|
|
622
|
+
...stableMessages,
|
|
623
|
+
locale: stableMessages?.locale || enUS.locale,
|
|
624
|
+
errorCodes: {
|
|
625
|
+
...enUS.errorCodes,
|
|
626
|
+
...stableMessages?.errorCodes
|
|
627
|
+
}
|
|
628
|
+
}), [stableMessages]);
|
|
258
629
|
const [pollingSince, setPollingSince] = useState(Date.now);
|
|
259
630
|
const [pollingStopped, setPollingStopped] = useState(false);
|
|
260
631
|
const [retrying, setRetrying] = useState(false);
|
|
@@ -300,7 +671,7 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
300
671
|
checkoutSuccessPath
|
|
301
672
|
},
|
|
302
673
|
connection,
|
|
303
|
-
connectionError: connection !== "error" ? null : stalled ?
|
|
674
|
+
connectionError: connection !== "error" ? null : stalled ? formatMessage(resolvedMessages, "connectionSlow") : describeError(config.error, resolvedMessages),
|
|
304
675
|
retryConnection: () => {
|
|
305
676
|
const started = Date.now();
|
|
306
677
|
setPollingSince(started);
|
|
@@ -317,12 +688,14 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
317
688
|
theme,
|
|
318
689
|
getSwatch,
|
|
319
690
|
locale: resolvedLocale,
|
|
691
|
+
messages: resolvedMessages,
|
|
320
692
|
onItemAdded,
|
|
321
693
|
onEvent,
|
|
322
694
|
children
|
|
323
695
|
});
|
|
324
696
|
}
|
|
325
|
-
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, onItemAdded, onEvent }) {
|
|
697
|
+
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, messages, onItemAdded, onEvent }) {
|
|
698
|
+
const text = (key, values) => formatMessage(messages, key, values);
|
|
326
699
|
const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
|
|
327
700
|
const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
|
|
328
701
|
const ready = connection === "ready";
|
|
@@ -338,8 +711,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
338
711
|
const [open, setOpen] = useState(false);
|
|
339
712
|
const [pending, setPending] = useState(false);
|
|
340
713
|
const [hydrating, setHydrating] = useState(true);
|
|
341
|
-
const [
|
|
342
|
-
const [
|
|
714
|
+
const [failure, setError] = useState(null);
|
|
715
|
+
const [storageUnavailable, setStorageUnavailable] = useState(false);
|
|
716
|
+
const error = failure && describeError(failure.cause, messages);
|
|
717
|
+
const storageWarning = storageUnavailable ? text("storageUnavailable") : null;
|
|
343
718
|
const [announcement, setAnnouncement] = useState("");
|
|
344
719
|
const id = useRef(null);
|
|
345
720
|
const currentCart = useRef(null);
|
|
@@ -399,7 +774,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
399
774
|
try {
|
|
400
775
|
return cartIdStorage.read();
|
|
401
776
|
} catch {
|
|
402
|
-
|
|
777
|
+
setStorageUnavailable(true);
|
|
403
778
|
return id.current;
|
|
404
779
|
}
|
|
405
780
|
}
|
|
@@ -410,9 +785,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
410
785
|
setCart(next);
|
|
411
786
|
try {
|
|
412
787
|
cartIdStorage.write(id.current);
|
|
413
|
-
|
|
788
|
+
setStorageUnavailable(false);
|
|
414
789
|
} catch {
|
|
415
|
-
|
|
790
|
+
setStorageUnavailable(true);
|
|
416
791
|
}
|
|
417
792
|
}
|
|
418
793
|
async function hydrate() {
|
|
@@ -446,12 +821,14 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
446
821
|
return isActive();
|
|
447
822
|
} catch (cause) {
|
|
448
823
|
if (isActive()) {
|
|
449
|
-
const
|
|
450
|
-
setError(
|
|
824
|
+
const shown = describeError(cause, messages);
|
|
825
|
+
setError({ cause });
|
|
826
|
+
const code = cause instanceof ApiError ? cause.code : void 0;
|
|
451
827
|
emit({
|
|
452
828
|
type: "error",
|
|
453
829
|
operation: kind,
|
|
454
|
-
message:
|
|
830
|
+
message: shown,
|
|
831
|
+
...code ? { code } : {}
|
|
455
832
|
});
|
|
456
833
|
}
|
|
457
834
|
return false;
|
|
@@ -476,7 +853,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
476
853
|
setCart(null);
|
|
477
854
|
setOpen(false);
|
|
478
855
|
setError(null);
|
|
479
|
-
|
|
856
|
+
setStorageUnavailable(false);
|
|
480
857
|
setAnnouncement("");
|
|
481
858
|
setPending(false);
|
|
482
859
|
setHydrating(ready);
|
|
@@ -498,7 +875,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
498
875
|
function addItem(item) {
|
|
499
876
|
rememberOpener();
|
|
500
877
|
return run("add", async () => {
|
|
501
|
-
if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new
|
|
878
|
+
if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new StorefrontError("invalidAddItem");
|
|
502
879
|
await hydrate();
|
|
503
880
|
if (!isActive()) return;
|
|
504
881
|
const fetchCart = (input, init) => {
|
|
@@ -514,10 +891,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
514
891
|
name: item.name,
|
|
515
892
|
quantity: item.quantity
|
|
516
893
|
}, fetchCart);
|
|
517
|
-
if (!result.cart?.id || !result.cart.lineItems?.length) throw new
|
|
894
|
+
if (!result.cart?.id || !result.cart.lineItems?.length) throw new StorefrontError("addReturnedNoCart");
|
|
518
895
|
if (!isActive()) return;
|
|
519
896
|
commit(result.cart);
|
|
520
|
-
setAnnouncement(
|
|
897
|
+
setAnnouncement(text("itemAdded", { name: item.name }));
|
|
521
898
|
openDrawer();
|
|
522
899
|
emit({
|
|
523
900
|
type: "item-added",
|
|
@@ -532,16 +909,16 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
532
909
|
return run(method === "DELETE" ? "remove" : "change-quantity", async () => {
|
|
533
910
|
await hydrate();
|
|
534
911
|
if (!isActive()) return;
|
|
535
|
-
if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new
|
|
536
|
-
if (quantity !== void 0 && (!Number.isInteger(quantity) || quantity < 1)) throw new
|
|
912
|
+
if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new StorefrontError("itemNotInCart");
|
|
913
|
+
if (quantity !== void 0 && (!Number.isInteger(quantity) || quantity < 1)) throw new StorefrontError("invalidQuantity");
|
|
537
914
|
const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/items/${encodeURIComponent(itemId)}`, {
|
|
538
915
|
method,
|
|
539
916
|
...quantity !== void 0 ? { body: JSON.stringify({ quantity }) } : {}
|
|
540
917
|
});
|
|
541
|
-
if (!result.cart?.id) throw new
|
|
918
|
+
if (!result.cart?.id) throw new StorefrontError("updateReturnedNoCart");
|
|
542
919
|
if (!isActive()) return;
|
|
543
920
|
commit(result.cart);
|
|
544
|
-
setAnnouncement(method === "DELETE" ? "
|
|
921
|
+
setAnnouncement(text(method === "DELETE" ? "itemRemoved" : "quantityUpdated"));
|
|
545
922
|
emit(method === "DELETE" ? {
|
|
546
923
|
type: "item-removed",
|
|
547
924
|
itemId,
|
|
@@ -558,16 +935,16 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
558
935
|
return run("discount", async () => {
|
|
559
936
|
await hydrate();
|
|
560
937
|
if (!isActive()) return;
|
|
561
|
-
if (!id.current || !code.trim()) throw new
|
|
938
|
+
if (!id.current || !code.trim()) throw new StorefrontError("discountMissing");
|
|
562
939
|
const codes = [...new Set([...currentCart.current?.discounts?.flatMap((discount) => discount.code ? [discount.code] : []) ?? [], code.trim()])];
|
|
563
940
|
const result = await boundRequest(`/cart/${encodeURIComponent(id.current)}/discounts`, {
|
|
564
941
|
method: "POST",
|
|
565
942
|
body: JSON.stringify({ discountCodes: codes })
|
|
566
943
|
});
|
|
567
|
-
if (!result.cart?.id) throw new
|
|
944
|
+
if (!result.cart?.id) throw new StorefrontError("discountReturnedNoCart");
|
|
568
945
|
if (!isActive()) return;
|
|
569
946
|
commit(result.cart);
|
|
570
|
-
setAnnouncement("
|
|
947
|
+
setAnnouncement(text("discountsUpdated"));
|
|
571
948
|
emit({
|
|
572
949
|
type: "discount-applied",
|
|
573
950
|
code: code.trim()
|
|
@@ -576,10 +953,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
576
953
|
}
|
|
577
954
|
function checkout() {
|
|
578
955
|
return run("checkout", async () => {
|
|
579
|
-
if (!config.checkoutSuccessPath) throw new
|
|
956
|
+
if (!config.checkoutSuccessPath) throw new StorefrontError("checkoutDisabled");
|
|
580
957
|
await hydrate();
|
|
581
958
|
if (!isActive()) return;
|
|
582
|
-
if (!id.current || !currentCart.current?.lineItems?.length) throw new
|
|
959
|
+
if (!id.current || !currentCart.current?.lineItems?.length) throw new StorefrontError("checkoutEmpty");
|
|
583
960
|
const origin = window.location.origin;
|
|
584
961
|
const successUrl = new URL(config.checkoutSuccessPath, origin);
|
|
585
962
|
successUrl.searchParams.set("orderId", id.current);
|
|
@@ -592,9 +969,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
592
969
|
})
|
|
593
970
|
});
|
|
594
971
|
if (!isActive()) return;
|
|
595
|
-
if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new
|
|
972
|
+
if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new StorefrontError("checkoutNoUrl");
|
|
596
973
|
const url = new URL(session$1.url, origin);
|
|
597
|
-
if (url.protocol !== "https:") throw new
|
|
974
|
+
if (url.protocol !== "https:") throw new StorefrontError("checkoutInvalidUrl");
|
|
598
975
|
emit({
|
|
599
976
|
type: "checkout-started",
|
|
600
977
|
cartId: id.current
|
|
@@ -611,6 +988,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
611
988
|
theme,
|
|
612
989
|
getSwatch,
|
|
613
990
|
locale,
|
|
991
|
+
messages,
|
|
614
992
|
cart,
|
|
615
993
|
open,
|
|
616
994
|
setOpen: (next) => {
|
|
@@ -637,10 +1015,11 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
637
1015
|
value: registerDrawer,
|
|
638
1016
|
children: /* @__PURE__ */ jsx(SampleCartProvider, {
|
|
639
1017
|
enabled: connection === "unbound",
|
|
640
|
-
|
|
1018
|
+
messages,
|
|
1019
|
+
announce: (key) => setAnnouncement(text(key)),
|
|
641
1020
|
emit,
|
|
642
1021
|
onAdd: (name) => {
|
|
643
|
-
setAnnouncement(
|
|
1022
|
+
setAnnouncement(text("sampleItemAdded", { name }));
|
|
644
1023
|
openDrawer();
|
|
645
1024
|
emit({
|
|
646
1025
|
type: "item-added",
|
|
@@ -668,21 +1047,22 @@ function useCart() {
|
|
|
668
1047
|
const { cart, config, connection, hydrating } = useCommerce();
|
|
669
1048
|
const sample = useSampleCart();
|
|
670
1049
|
const isSample = connection === "unbound";
|
|
671
|
-
const sampleItems
|
|
1050
|
+
const sampleItems = sample.items;
|
|
672
1051
|
return useMemo(() => {
|
|
673
1052
|
if (isSample) return {
|
|
674
|
-
items: sampleItems
|
|
1053
|
+
items: sampleItems.map((item) => ({
|
|
675
1054
|
id: item.key,
|
|
676
1055
|
name: item.name,
|
|
677
1056
|
quantity: item.quantity,
|
|
678
|
-
imageUrl: null,
|
|
1057
|
+
imageUrl: item.sample.imageUrl || null,
|
|
679
1058
|
options: [],
|
|
680
1059
|
subtotal: item.price * item.quantity,
|
|
681
|
-
currency:
|
|
1060
|
+
currency: sampleCurrency,
|
|
1061
|
+
sample: item.sample
|
|
682
1062
|
})),
|
|
683
|
-
itemCount: sampleItems
|
|
684
|
-
subtotal: sampleItems
|
|
685
|
-
currency:
|
|
1063
|
+
itemCount: sampleItems.reduce((total, item) => total + item.quantity, 0),
|
|
1064
|
+
subtotal: sampleItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
1065
|
+
currency: sampleCurrency,
|
|
686
1066
|
isSample,
|
|
687
1067
|
loading: false
|
|
688
1068
|
};
|
|
@@ -700,7 +1080,8 @@ function useCart() {
|
|
|
700
1080
|
values: option.values ?? []
|
|
701
1081
|
})),
|
|
702
1082
|
subtotal: typeof item.totals?.subTotal?.value === "number" ? item.totals.subTotal.value : null,
|
|
703
|
-
currency: item.totals?.subTotal?.currencyCode ?? currency
|
|
1083
|
+
currency: item.totals?.subTotal?.currencyCode ?? currency,
|
|
1084
|
+
sample: null
|
|
704
1085
|
})),
|
|
705
1086
|
itemCount: lines.reduce((total, item) => total + (item.quantity ?? 0), 0),
|
|
706
1087
|
subtotal: typeof subtotal === "number" ? subtotal : null,
|
|
@@ -713,7 +1094,7 @@ function useCart() {
|
|
|
713
1094
|
config.currencyCode,
|
|
714
1095
|
hydrating,
|
|
715
1096
|
isSample,
|
|
716
|
-
sampleItems
|
|
1097
|
+
sampleItems
|
|
717
1098
|
]);
|
|
718
1099
|
}
|
|
719
1100
|
|
|
@@ -907,15 +1288,16 @@ function StorefrontSurface({ children, className = "" }) {
|
|
|
907
1288
|
}
|
|
908
1289
|
function CommerceStatus() {
|
|
909
1290
|
const { connection, connectionError, retryConnection } = useCommerce();
|
|
1291
|
+
const text = useMessage();
|
|
910
1292
|
if (connection === "ready") return null;
|
|
911
1293
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "unbound" ? /* @__PURE__ */ jsx("p", {
|
|
912
1294
|
role: "status",
|
|
913
1295
|
className: "text-sm text-commerce-muted",
|
|
914
|
-
children: "
|
|
1296
|
+
children: text("connectPrompt")
|
|
915
1297
|
}) : connection === "loading" || connection === "connecting" ? /* @__PURE__ */ jsx("p", {
|
|
916
1298
|
role: "status",
|
|
917
1299
|
className: "text-sm text-commerce-muted",
|
|
918
|
-
children: "
|
|
1300
|
+
children: text("connecting")
|
|
919
1301
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
920
1302
|
role: "alert",
|
|
921
1303
|
className: errorNoticeClass,
|
|
@@ -923,7 +1305,7 @@ function CommerceStatus() {
|
|
|
923
1305
|
type: "button",
|
|
924
1306
|
className: errorActionClass,
|
|
925
1307
|
onClick: retryConnection,
|
|
926
|
-
children: "
|
|
1308
|
+
children: text("retryConnection")
|
|
927
1309
|
})]
|
|
928
1310
|
}) });
|
|
929
1311
|
}
|
|
@@ -937,6 +1319,7 @@ function CommerceStatus() {
|
|
|
937
1319
|
function CartButton({ children } = {}) {
|
|
938
1320
|
const { setOpen, open, connection } = useCommerce();
|
|
939
1321
|
const { itemCount } = useCart();
|
|
1322
|
+
const text = useMessage();
|
|
940
1323
|
const [bump, setBump] = useState(0);
|
|
941
1324
|
const previous = useRef(itemCount);
|
|
942
1325
|
useEffect(() => {
|
|
@@ -952,7 +1335,7 @@ function CartButton({ children } = {}) {
|
|
|
952
1335
|
disabled: connection !== "ready" && connection !== "unbound",
|
|
953
1336
|
"aria-haspopup": "dialog",
|
|
954
1337
|
"aria-expanded": open,
|
|
955
|
-
"aria-label": custom === null ?
|
|
1338
|
+
"aria-label": custom === null ? text("cartButtonLabel", { count: itemCount }) : void 0,
|
|
956
1339
|
"data-commerce-part": "cart-button",
|
|
957
1340
|
onClick: () => setOpen(true),
|
|
958
1341
|
children: custom ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BagIcon, {}), itemCount > 0 && /* @__PURE__ */ jsx("span", {
|
|
@@ -982,8 +1365,11 @@ function BagIcon() {
|
|
|
982
1365
|
const isSoldOut = (available) => available !== null && available <= 0;
|
|
983
1366
|
/** Why `quantity` cannot be added, or `null` when it can. `available` is `null` when stock is not tracked. */
|
|
984
1367
|
function quantityProblem(quantity, available) {
|
|
985
|
-
if (!Number.isInteger(quantity) || quantity < 1) return
|
|
986
|
-
if (available !== null && available > 0 && quantity > available) return
|
|
1368
|
+
if (!Number.isInteger(quantity) || quantity < 1) return { key: "quantityNotWhole" };
|
|
1369
|
+
if (available !== null && available > 0 && quantity > available) return {
|
|
1370
|
+
key: "quantityOverStock",
|
|
1371
|
+
values: { count: available }
|
|
1372
|
+
};
|
|
987
1373
|
return null;
|
|
988
1374
|
}
|
|
989
1375
|
function AddToCartButton(props) {
|
|
@@ -998,16 +1384,21 @@ function AddToCartButton(props) {
|
|
|
998
1384
|
*/
|
|
999
1385
|
function AddToCart({ sku, name, quantity = 1, children, className, block = false, label, labelName }) {
|
|
1000
1386
|
const { addItem, pending, hydrating, error, open, connection } = useCommerce();
|
|
1387
|
+
const text = useMessage();
|
|
1001
1388
|
const [adding, setAdding] = useState(false);
|
|
1002
1389
|
const reasonId = useId();
|
|
1003
1390
|
const available = getAvailableInventoryQuantity(sku);
|
|
1004
1391
|
const soldOut = isSoldOut(available);
|
|
1005
1392
|
let reason = null;
|
|
1006
|
-
if (connection === "unbound") reason = "
|
|
1007
|
-
else if (connection === "error") reason = "
|
|
1008
|
-
else if (connection !== "ready") reason = "
|
|
1009
|
-
else if (!sku.id) reason = "
|
|
1010
|
-
else if (!soldOut) reason =
|
|
1393
|
+
if (connection === "unbound") reason = text("addReasonUnbound");
|
|
1394
|
+
else if (connection === "error") reason = text("addReasonError");
|
|
1395
|
+
else if (connection !== "ready") reason = text("connecting");
|
|
1396
|
+
else if (!sku.id) reason = text("addReasonNoSku");
|
|
1397
|
+
else if (!soldOut && hydrating) reason = text("loadingCart");
|
|
1398
|
+
else if (!soldOut) {
|
|
1399
|
+
const problem = quantityProblem(quantity, available);
|
|
1400
|
+
reason = problem && text(problem.key, problem.values);
|
|
1401
|
+
}
|
|
1011
1402
|
const disabled = adding || soldOut || reason !== null;
|
|
1012
1403
|
const ownLabel = !!labelName && !soldOut && !adding && renderLabel(children, {
|
|
1013
1404
|
busy: false,
|
|
@@ -1042,7 +1433,7 @@ function AddToCart({ sku, name, quantity = 1, children, className, block = false
|
|
|
1042
1433
|
children: renderLabel(children, {
|
|
1043
1434
|
busy: adding,
|
|
1044
1435
|
soldOut
|
|
1045
|
-
}, soldOut ? "
|
|
1436
|
+
}, soldOut ? text("outOfStock") : adding ? text("adding") : label ?? text("addToCart"))
|
|
1046
1437
|
}),
|
|
1047
1438
|
reason && /* @__PURE__ */ jsx("span", {
|
|
1048
1439
|
id: reasonId,
|
|
@@ -1158,11 +1549,12 @@ function CartDrawerRoot({ children, className }) {
|
|
|
1158
1549
|
function CartTitle({ className, children }) {
|
|
1159
1550
|
const { isSample, itemCount } = useCartInternal("CartTitle");
|
|
1160
1551
|
const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
|
|
1552
|
+
const text = useMessage();
|
|
1161
1553
|
return /* @__PURE__ */ jsx("div", {
|
|
1162
1554
|
...partProps("cart-title", className),
|
|
1163
1555
|
children: /* @__PURE__ */ jsx(Heading, {
|
|
1164
1556
|
className: "text-lg font-semibold tracking-tight",
|
|
1165
|
-
children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [isSample ? "
|
|
1557
|
+
children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [text(isSample ? "sampleCartTitle" : "cartTitle"), itemCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1166
1558
|
className: "font-normal text-commerce-muted",
|
|
1167
1559
|
children: [
|
|
1168
1560
|
" (",
|
|
@@ -1179,7 +1571,8 @@ function CartDescription({ className, children }) {
|
|
|
1179
1571
|
const drawer = useContext(CartDrawerContext);
|
|
1180
1572
|
const register = drawer?.register;
|
|
1181
1573
|
useEffect(() => register?.("description"), [register]);
|
|
1182
|
-
const
|
|
1574
|
+
const message = useMessage();
|
|
1575
|
+
const text = renderLabel(children, { isSample }, isSample ? message("sampleCartDescription", { currency: sampleCurrency }) : message("cartDescription"));
|
|
1183
1576
|
return /* @__PURE__ */ jsx("div", {
|
|
1184
1577
|
...partProps("cart-description", className),
|
|
1185
1578
|
children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
|
|
@@ -1194,12 +1587,13 @@ function CartDescription({ className, children }) {
|
|
|
1194
1587
|
/** The drawer's close button. Renders nothing outside the drawer. */
|
|
1195
1588
|
function CartClose({ className }) {
|
|
1196
1589
|
useCartInternal("CartClose");
|
|
1590
|
+
const text = useMessage();
|
|
1197
1591
|
if (!useContext(CartDrawerContext)) return null;
|
|
1198
1592
|
return /* @__PURE__ */ jsx("div", {
|
|
1199
1593
|
...partProps("cart-close", className),
|
|
1200
1594
|
children: /* @__PURE__ */ jsx(Dialog.Close, {
|
|
1201
1595
|
className: "inline-flex min-h-11 min-w-11 items-center justify-center rounded-full text-commerce-muted transition-colors hover:bg-commerce-tint hover:text-inherit focus-visible:outline focus-visible:outline-2 focus-visible:outline-current",
|
|
1202
|
-
"aria-label": "
|
|
1596
|
+
"aria-label": text("closeCart"),
|
|
1203
1597
|
children: /* @__PURE__ */ jsx("svg", {
|
|
1204
1598
|
"aria-hidden": "true",
|
|
1205
1599
|
viewBox: "0 0 20 20",
|
|
@@ -1218,6 +1612,7 @@ function CartClose({ className }) {
|
|
|
1218
1612
|
/** A storage warning, and a cart error with a refresh button. Renders nothing when there are neither. */
|
|
1219
1613
|
function CartNotices({ className }) {
|
|
1220
1614
|
const { storageWarning, error, action, locked, run, refresh } = useCartInternal("CartNotices");
|
|
1615
|
+
const text = useMessage();
|
|
1221
1616
|
if (!storageWarning && !error) return null;
|
|
1222
1617
|
return /* @__PURE__ */ jsx("div", {
|
|
1223
1618
|
...partProps("cart-notices", className),
|
|
@@ -1237,7 +1632,7 @@ function CartNotices({ className }) {
|
|
|
1237
1632
|
"aria-disabled": locked,
|
|
1238
1633
|
"aria-busy": action === "refresh",
|
|
1239
1634
|
onClick: () => run("refresh", refresh),
|
|
1240
|
-
children: "
|
|
1635
|
+
children: text("refreshCart")
|
|
1241
1636
|
})]
|
|
1242
1637
|
})]
|
|
1243
1638
|
})
|
|
@@ -1251,7 +1646,11 @@ function useCartItemInternal(part) {
|
|
|
1251
1646
|
if (!item) throw new Error(`${part} must be rendered inside <CartItemRoot>.`);
|
|
1252
1647
|
return item;
|
|
1253
1648
|
}
|
|
1254
|
-
/**
|
|
1649
|
+
/**
|
|
1650
|
+
* Read the line from inside `CartItemRoot`, for custom item parts. Pass the type of the site's samples, such as
|
|
1651
|
+
* `useCartItem<MySample>()`, to read their own fields from `sample`. The type is not checked: when the cart can hold
|
|
1652
|
+
* samples from more than one catalog, read the site's own fields as possibly missing.
|
|
1653
|
+
*/
|
|
1255
1654
|
function useCartItem() {
|
|
1256
1655
|
return useCartItemInternal("useCartItem");
|
|
1257
1656
|
}
|
|
@@ -1261,6 +1660,7 @@ function useCartItem() {
|
|
|
1261
1660
|
*/
|
|
1262
1661
|
function CartItems({ className, children }) {
|
|
1263
1662
|
const { items, hydrating } = useCartInternal("CartItems");
|
|
1663
|
+
const text = useMessage();
|
|
1264
1664
|
const template = hasChildren(children) ? children : packagedItem;
|
|
1265
1665
|
const registry = usePartRegistry();
|
|
1266
1666
|
const { has } = registry;
|
|
@@ -1273,7 +1673,7 @@ function CartItems({ className, children }) {
|
|
|
1273
1673
|
children: [hydrating && /* @__PURE__ */ jsx("p", {
|
|
1274
1674
|
role: "status",
|
|
1275
1675
|
className: "py-4 text-sm text-commerce-muted",
|
|
1276
|
-
children: "
|
|
1676
|
+
children: text("loadingCart")
|
|
1277
1677
|
}), /* @__PURE__ */ jsx("div", {
|
|
1278
1678
|
role: "list",
|
|
1279
1679
|
...partProps("cart-items", className),
|
|
@@ -1306,13 +1706,15 @@ function CartItemRoot({ children, className }) {
|
|
|
1306
1706
|
/** The line's product image. Renders nothing without one. */
|
|
1307
1707
|
function CartItemImage({ className }) {
|
|
1308
1708
|
const { imageUrl } = useCartItemInternal("CartItemImage");
|
|
1309
|
-
|
|
1709
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
1710
|
+
if (!imageUrl || imageUrl === failedUrl) return null;
|
|
1310
1711
|
return /* @__PURE__ */ jsx("div", {
|
|
1311
1712
|
...partProps("cart-item-image", className),
|
|
1312
1713
|
children: /* @__PURE__ */ jsx("img", {
|
|
1313
1714
|
src: imageUrl,
|
|
1314
1715
|
alt: "",
|
|
1315
|
-
className: "h-20 w-20 bg-commerce-tint object-cover"
|
|
1716
|
+
className: "h-20 w-20 bg-commerce-tint object-cover",
|
|
1717
|
+
onError: () => setFailedUrl(imageUrl)
|
|
1316
1718
|
})
|
|
1317
1719
|
});
|
|
1318
1720
|
}
|
|
@@ -1330,17 +1732,16 @@ function CartItemName({ className }) {
|
|
|
1330
1732
|
/** The selected options on one line, such as "Color: Blue · Size: M". Renders nothing without options. */
|
|
1331
1733
|
function CartItemOptions({ className }) {
|
|
1332
1734
|
const { options } = useCartItemInternal("CartItemOptions");
|
|
1735
|
+
const text = useMessage();
|
|
1333
1736
|
if (!options.length) return null;
|
|
1334
1737
|
return /* @__PURE__ */ jsx("div", {
|
|
1335
1738
|
...partProps("cart-item-options", className),
|
|
1336
1739
|
children: /* @__PURE__ */ jsx("p", {
|
|
1337
1740
|
className: "text-sm text-commerce-muted",
|
|
1338
|
-
children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [
|
|
1339
|
-
|
|
1340
|
-
option.
|
|
1341
|
-
|
|
1342
|
-
option.values.join(", ")
|
|
1343
|
-
] }, `${option.attribute}:${option.values.join(",")}`))
|
|
1741
|
+
children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [index > 0 && " · ", text("itemOption", {
|
|
1742
|
+
attribute: option.attribute,
|
|
1743
|
+
values: option.values.join(", ")
|
|
1744
|
+
})] }, `${option.attribute}:${option.values.join(",")}`))
|
|
1344
1745
|
})
|
|
1345
1746
|
});
|
|
1346
1747
|
}
|
|
@@ -1348,11 +1749,12 @@ function CartItemOptions({ className }) {
|
|
|
1348
1749
|
function CartItemPrice({ className }) {
|
|
1349
1750
|
const { subtotal, currency } = useCartItemInternal("CartItemPrice");
|
|
1350
1751
|
const money$1 = useMoney();
|
|
1752
|
+
const text = useMessage();
|
|
1351
1753
|
return /* @__PURE__ */ jsx("div", {
|
|
1352
1754
|
...partProps("cart-item-price", className),
|
|
1353
1755
|
children: /* @__PURE__ */ jsx("p", {
|
|
1354
1756
|
className: "text-sm leading-6 tabular-nums",
|
|
1355
|
-
children: subtotal === null ? "
|
|
1757
|
+
children: subtotal === null ? text("priceUnavailable") : money$1(subtotal, currency)
|
|
1356
1758
|
})
|
|
1357
1759
|
});
|
|
1358
1760
|
}
|
|
@@ -1375,6 +1777,7 @@ function StepperIcon$1({ plus }) {
|
|
|
1375
1777
|
function CartItemQuantity({ className }) {
|
|
1376
1778
|
const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
|
|
1377
1779
|
const { action, locked, run, changeQuantity } = useCartInternal("CartItemQuantity");
|
|
1780
|
+
const text = useMessage();
|
|
1378
1781
|
return /* @__PURE__ */ jsx("div", {
|
|
1379
1782
|
...partProps("cart-item-quantity", className),
|
|
1380
1783
|
children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -1383,7 +1786,7 @@ function CartItemQuantity({ className }) {
|
|
|
1383
1786
|
/* @__PURE__ */ jsx("button", {
|
|
1384
1787
|
type: "button",
|
|
1385
1788
|
className: stepperButtonClass$1,
|
|
1386
|
-
"aria-label":
|
|
1789
|
+
"aria-label": text("decreaseQuantity", { name }),
|
|
1387
1790
|
"data-commerce-control": "decrease",
|
|
1388
1791
|
disabled: action === `decrease:${id}` || !id || quantity <= 1,
|
|
1389
1792
|
"aria-disabled": locked || !id || quantity <= 1,
|
|
@@ -1395,13 +1798,13 @@ function CartItemQuantity({ className }) {
|
|
|
1395
1798
|
className: "min-w-8 text-center text-sm tabular-nums",
|
|
1396
1799
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1397
1800
|
className: "sr-only",
|
|
1398
|
-
children: "
|
|
1801
|
+
children: text("quantityPrefix")
|
|
1399
1802
|
}), quantity]
|
|
1400
1803
|
}),
|
|
1401
1804
|
/* @__PURE__ */ jsx("button", {
|
|
1402
1805
|
type: "button",
|
|
1403
1806
|
className: stepperButtonClass$1,
|
|
1404
|
-
"aria-label":
|
|
1807
|
+
"aria-label": text("increaseQuantity", { name }),
|
|
1405
1808
|
"data-commerce-control": "increase",
|
|
1406
1809
|
disabled: action === `increase:${id}` || !id,
|
|
1407
1810
|
"aria-disabled": locked || !id,
|
|
@@ -1417,6 +1820,7 @@ function CartItemQuantity({ className }) {
|
|
|
1417
1820
|
function CartItemRemove({ className, children }) {
|
|
1418
1821
|
const { id, name } = useCartItemInternal("CartItemRemove");
|
|
1419
1822
|
const { action, locked, run, removeItem } = useCartInternal("CartItemRemove");
|
|
1823
|
+
const text = useMessage();
|
|
1420
1824
|
return /* @__PURE__ */ jsx("div", {
|
|
1421
1825
|
...partProps("cart-item-remove", className),
|
|
1422
1826
|
children: /* @__PURE__ */ jsxs("button", {
|
|
@@ -1426,7 +1830,7 @@ function CartItemRemove({ className, children }) {
|
|
|
1426
1830
|
"aria-disabled": locked || !id,
|
|
1427
1831
|
"aria-busy": action === `remove:${id}`,
|
|
1428
1832
|
onClick: () => run(`remove:${id}`, () => removeItem(id)),
|
|
1429
|
-
children: [renderLabel(children, { busy: action === `remove:${id}` }, "
|
|
1833
|
+
children: [renderLabel(children, { busy: action === `remove:${id}` }, text("remove")), /* @__PURE__ */ jsxs("span", {
|
|
1430
1834
|
className: "sr-only",
|
|
1431
1835
|
children: [" ", name]
|
|
1432
1836
|
})]
|
|
@@ -1452,6 +1856,7 @@ const packagedItem = /* @__PURE__ */ jsxs(CartItemRoot, {
|
|
|
1452
1856
|
/** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
|
|
1453
1857
|
function CartEmpty({ className, children }) {
|
|
1454
1858
|
const { items, hydrating, isSample, catalogPath, close } = useCartInternal("CartEmpty");
|
|
1859
|
+
const text = useMessage();
|
|
1455
1860
|
if (hydrating || items.length) return null;
|
|
1456
1861
|
return /* @__PURE__ */ jsx("div", {
|
|
1457
1862
|
...partProps("cart-empty", className),
|
|
@@ -1474,17 +1879,17 @@ function CartEmpty({ className, children }) {
|
|
|
1474
1879
|
}),
|
|
1475
1880
|
/* @__PURE__ */ jsx("p", {
|
|
1476
1881
|
className: "text-base font-semibold",
|
|
1477
|
-
children: "
|
|
1882
|
+
children: text("cartEmpty")
|
|
1478
1883
|
}),
|
|
1479
1884
|
/* @__PURE__ */ jsx("p", {
|
|
1480
1885
|
className: "mt-1 text-sm text-commerce-muted",
|
|
1481
|
-
children: isSample ? "
|
|
1886
|
+
children: text(isSample ? "sampleCartEmptyHint" : "cartEmptyHint")
|
|
1482
1887
|
}),
|
|
1483
1888
|
/* @__PURE__ */ jsx(Link, {
|
|
1484
1889
|
className: `${secondaryButtonClass} mt-6`,
|
|
1485
1890
|
to: catalogPath,
|
|
1486
1891
|
onClick: close,
|
|
1487
|
-
children: "
|
|
1892
|
+
children: text("browseProducts")
|
|
1488
1893
|
})
|
|
1489
1894
|
] })
|
|
1490
1895
|
});
|
|
@@ -1502,12 +1907,13 @@ function CartSummary({ className, children }) {
|
|
|
1502
1907
|
function CartSubtotal({ className }) {
|
|
1503
1908
|
const { items, subtotal, currency, isSample } = useCartInternal("CartSubtotal");
|
|
1504
1909
|
const money$1 = useMoney();
|
|
1910
|
+
const text = useMessage();
|
|
1505
1911
|
if (!items.length || subtotal === null) return null;
|
|
1506
1912
|
return /* @__PURE__ */ jsx("div", {
|
|
1507
1913
|
...partProps("cart-subtotal", className),
|
|
1508
1914
|
children: /* @__PURE__ */ jsx("dl", { children: /* @__PURE__ */ jsxs("div", {
|
|
1509
1915
|
className: "flex justify-between gap-4 font-semibold",
|
|
1510
|
-
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "
|
|
1916
|
+
children: [/* @__PURE__ */ jsx("dt", { children: text(isSample ? "sampleSubtotal" : "subtotal") }), /* @__PURE__ */ jsx("dd", {
|
|
1511
1917
|
className: "tabular-nums",
|
|
1512
1918
|
children: money$1(subtotal, currency)
|
|
1513
1919
|
})]
|
|
@@ -1520,12 +1926,13 @@ function CartSubtotal({ className }) {
|
|
|
1520
1926
|
*/
|
|
1521
1927
|
function CartCheckoutNote({ className }) {
|
|
1522
1928
|
const { items, isSample } = useCartInternal("CartCheckoutNote");
|
|
1929
|
+
const text = useMessage();
|
|
1523
1930
|
if (isSample || !items.length) return null;
|
|
1524
1931
|
return /* @__PURE__ */ jsx("div", {
|
|
1525
1932
|
...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
|
|
1526
1933
|
children: /* @__PURE__ */ jsxs("dl", {
|
|
1527
1934
|
className: "flex justify-between gap-4 text-sm text-commerce-muted",
|
|
1528
|
-
children: [/* @__PURE__ */ jsx("dt", { children: "
|
|
1935
|
+
children: [/* @__PURE__ */ jsx("dt", { children: text("checkoutAdjustments") }), /* @__PURE__ */ jsx("dd", { children: text("checkoutAdjustmentsValue") })]
|
|
1529
1936
|
})
|
|
1530
1937
|
});
|
|
1531
1938
|
}
|
|
@@ -1535,15 +1942,16 @@ function CartCheckoutNote({ className }) {
|
|
|
1535
1942
|
*/
|
|
1536
1943
|
function CartCheckout({ className, children }) {
|
|
1537
1944
|
const { items, isSample, checkoutEnabled, checkoutReady, hydrating, action, locked, run, checkout } = useCartInternal("CartCheckout");
|
|
1945
|
+
const text = useMessage();
|
|
1538
1946
|
if (!items.length) return null;
|
|
1539
1947
|
let content;
|
|
1540
1948
|
if (isSample) content = /* @__PURE__ */ jsx("p", {
|
|
1541
1949
|
className: "text-sm text-commerce-muted",
|
|
1542
|
-
children: "
|
|
1950
|
+
children: text("sampleCheckoutNote", { currency: sampleCurrency })
|
|
1543
1951
|
});
|
|
1544
1952
|
else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
|
|
1545
1953
|
className: "text-sm text-commerce-muted",
|
|
1546
|
-
children: "
|
|
1954
|
+
children: text("checkoutUnavailable")
|
|
1547
1955
|
});
|
|
1548
1956
|
else content = /* @__PURE__ */ jsx("button", {
|
|
1549
1957
|
type: "button",
|
|
@@ -1552,7 +1960,7 @@ function CartCheckout({ className, children }) {
|
|
|
1552
1960
|
"aria-disabled": locked || !checkoutReady,
|
|
1553
1961
|
"aria-busy": action === "checkout",
|
|
1554
1962
|
onClick: () => run("checkout", checkout),
|
|
1555
|
-
children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "
|
|
1963
|
+
children: renderLabel(children, { busy: action === "checkout" }, text(action === "checkout" ? "openingCheckout" : "checkout"))
|
|
1556
1964
|
});
|
|
1557
1965
|
return /* @__PURE__ */ jsx("div", {
|
|
1558
1966
|
...partProps("cart-checkout", className),
|
|
@@ -1566,8 +1974,9 @@ function CartCheckout({ className, children }) {
|
|
|
1566
1974
|
function CartContinueShopping({ className, children }) {
|
|
1567
1975
|
const { items, isSample, catalogPath } = useCartInternal("CartContinueShopping");
|
|
1568
1976
|
const inDrawer = !!useContext(CartDrawerContext);
|
|
1977
|
+
const text = useMessage();
|
|
1569
1978
|
if (!items.length) return null;
|
|
1570
|
-
const label = renderLabel(children, { isSample }, "
|
|
1979
|
+
const label = renderLabel(children, { isSample }, text("continueShopping"));
|
|
1571
1980
|
const linkClass = "inline-flex min-h-11 w-full items-center justify-center rounded-lg px-5 py-2 text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current";
|
|
1572
1981
|
return /* @__PURE__ */ jsx("div", {
|
|
1573
1982
|
...partProps("cart-continue-shopping", className),
|
|
@@ -1626,6 +2035,7 @@ function CartDrawer() {
|
|
|
1626
2035
|
*/
|
|
1627
2036
|
function ProductImage({ url, name, className = "" }) {
|
|
1628
2037
|
const [failedUrl, setFailedUrl] = useState(null);
|
|
2038
|
+
const text = useMessage();
|
|
1629
2039
|
return url && url !== failedUrl ? /* @__PURE__ */ jsx("img", {
|
|
1630
2040
|
src: url,
|
|
1631
2041
|
alt: name,
|
|
@@ -1635,20 +2045,20 @@ function ProductImage({ url, name, className = "" }) {
|
|
|
1635
2045
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1636
2046
|
className: `flex h-full min-h-44 items-center justify-center bg-commerce-tint p-6 text-center text-sm text-commerce-muted ${className}`,
|
|
1637
2047
|
role: "img",
|
|
1638
|
-
"aria-label":
|
|
1639
|
-
children: "
|
|
2048
|
+
"aria-label": text("imageUnavailable", { name }),
|
|
2049
|
+
children: text("noImage")
|
|
1640
2050
|
});
|
|
1641
2051
|
}
|
|
1642
2052
|
/** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
|
|
1643
2053
|
const CatalogItemContext = createContext(null);
|
|
1644
|
-
const titleLink = "title
|
|
1645
|
-
function cardValue(item, productPath, links) {
|
|
2054
|
+
const titleLink = "title";
|
|
2055
|
+
function cardValue(item, productPath, links, sampleNameText) {
|
|
1646
2056
|
if (item.source === "sample") return {
|
|
1647
2057
|
...links,
|
|
1648
2058
|
source: "sample",
|
|
1649
2059
|
sample: item.sample,
|
|
1650
2060
|
product: null,
|
|
1651
|
-
name:
|
|
2061
|
+
name: sampleNameText,
|
|
1652
2062
|
href: null,
|
|
1653
2063
|
isSample: true
|
|
1654
2064
|
};
|
|
@@ -1667,16 +2077,24 @@ function useProductCardInternal(part) {
|
|
|
1667
2077
|
if (!context) throw new Error(`${part} must be rendered inside <ProductCardRoot>.`);
|
|
1668
2078
|
return context;
|
|
1669
2079
|
}
|
|
1670
|
-
/**
|
|
2080
|
+
/**
|
|
2081
|
+
* Read the card's product or sample, name, and link from inside `ProductCardRoot`. Pass the type of the site's
|
|
2082
|
+
* samples, such as `useProductCard<MySample>()`, to read their own fields from `sample`. The type is not checked:
|
|
2083
|
+
* on a page that also shows the packaged samples, read the site's own fields as possibly missing.
|
|
2084
|
+
*/
|
|
1671
2085
|
function useProductCard() {
|
|
1672
|
-
const
|
|
2086
|
+
const card = useProductCardInternal("useProductCard");
|
|
2087
|
+
const { product, name, href, isSample } = card;
|
|
2088
|
+
const sample = card.source === "sample" ? card.sample : null;
|
|
1673
2089
|
return useMemo(() => ({
|
|
1674
2090
|
product,
|
|
2091
|
+
sample,
|
|
1675
2092
|
name,
|
|
1676
2093
|
href,
|
|
1677
2094
|
isSample
|
|
1678
2095
|
}), [
|
|
1679
2096
|
product,
|
|
2097
|
+
sample,
|
|
1680
2098
|
name,
|
|
1681
2099
|
href,
|
|
1682
2100
|
isSample
|
|
@@ -1686,6 +2104,7 @@ function useProductCard() {
|
|
|
1686
2104
|
function ProductCardRoot({ children, product, className }) {
|
|
1687
2105
|
const catalogItem = useContext(CatalogItemContext);
|
|
1688
2106
|
const { config } = useCommerce();
|
|
2107
|
+
const text = useMessage();
|
|
1689
2108
|
const [imageLinked, setImageLinked] = useState(false);
|
|
1690
2109
|
const { register, has } = usePartRegistry(useCallback((registered) => setImageLinked(!registered(titleLink)), []));
|
|
1691
2110
|
useEffect(() => {
|
|
@@ -1696,16 +2115,18 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
1696
2115
|
source: "live",
|
|
1697
2116
|
product
|
|
1698
2117
|
} : null);
|
|
2118
|
+
const sampleText = item?.source === "sample" ? sampleName(text, item.sample) : "";
|
|
1699
2119
|
return item && cardValue(item, config.productPath, {
|
|
1700
2120
|
imageLinked,
|
|
1701
2121
|
register
|
|
1702
|
-
});
|
|
2122
|
+
}, sampleText);
|
|
1703
2123
|
}, [
|
|
1704
2124
|
catalogItem,
|
|
1705
2125
|
product,
|
|
1706
2126
|
config.productPath,
|
|
1707
2127
|
imageLinked,
|
|
1708
|
-
register
|
|
2128
|
+
register,
|
|
2129
|
+
text
|
|
1709
2130
|
]);
|
|
1710
2131
|
if (catalogItem && product) throw new Error("Inside <CatalogProducts>, ProductCardRoot shows each catalog product. Remove its product prop.");
|
|
1711
2132
|
if (!value) throw new Error("ProductCardRoot needs a product prop outside <CatalogProducts>.");
|
|
@@ -1722,8 +2143,9 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
1722
2143
|
return catalogItem ? card : /* @__PURE__ */ jsx(StorefrontSurface, { children: card });
|
|
1723
2144
|
}
|
|
1724
2145
|
/**
|
|
1725
|
-
* The product's first image, linked to its page.
|
|
1726
|
-
* the title is the one link keyboard and screen reader users get
|
|
2146
|
+
* The product's first image, linked to its page. A sample shows its `imageUrl`, unlinked, or the packaged samples'
|
|
2147
|
+
* placeholder. With a `ProductCardTitle` in the card, the title is the one link keyboard and screen reader users get,
|
|
2148
|
+
* and a sample's image is hidden from screen readers so its name is not read twice; without one, the image is exposed.
|
|
1727
2149
|
*/
|
|
1728
2150
|
function ProductCardImage({ className }) {
|
|
1729
2151
|
const card = useProductCardInternal("ProductCardImage");
|
|
@@ -1733,23 +2155,30 @@ function ProductCardImage({ className }) {
|
|
|
1733
2155
|
to: card.href,
|
|
1734
2156
|
tabIndex: card.imageLinked ? void 0 : -1,
|
|
1735
2157
|
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
1736
|
-
className: "block aspect-
|
|
2158
|
+
className: "block aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
|
|
1737
2159
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1738
2160
|
url: getPrimaryImageUrl(card.product),
|
|
1739
2161
|
name: card.name,
|
|
1740
2162
|
className: "motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out motion-safe:group-hover:scale-[1.03]"
|
|
1741
2163
|
})
|
|
1742
|
-
}) : /* @__PURE__ */ jsx("div", {
|
|
2164
|
+
}) : isBuiltInSample(card.sample) ? /* @__PURE__ */ jsx("div", {
|
|
1743
2165
|
"aria-hidden": "true",
|
|
1744
|
-
className: "flex aspect-
|
|
2166
|
+
className: "flex aspect-(--commerce-card-image-aspect,1/1) items-center justify-center bg-commerce-tint",
|
|
1745
2167
|
children: /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-full border border-commerce-line" })
|
|
2168
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
2169
|
+
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
2170
|
+
className: "aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint",
|
|
2171
|
+
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2172
|
+
url: card.sample.imageUrl,
|
|
2173
|
+
name: card.name
|
|
2174
|
+
})
|
|
1746
2175
|
})
|
|
1747
2176
|
});
|
|
1748
2177
|
}
|
|
1749
2178
|
/** The product name as an H2, linked to its page. */
|
|
1750
2179
|
function ProductCardTitle({ className }) {
|
|
1751
2180
|
const { name, href, register } = useProductCardInternal("ProductCardTitle");
|
|
1752
|
-
useEffect(() =>
|
|
2181
|
+
useEffect(() => register(titleLink), [register]);
|
|
1753
2182
|
return /* @__PURE__ */ jsx("div", {
|
|
1754
2183
|
...partProps("product-card-title", className),
|
|
1755
2184
|
children: /* @__PURE__ */ jsx("h2", {
|
|
@@ -1762,47 +2191,55 @@ function ProductCardTitle({ className }) {
|
|
|
1762
2191
|
})
|
|
1763
2192
|
});
|
|
1764
2193
|
}
|
|
1765
|
-
/** Up to
|
|
2194
|
+
/** Up to two lines of the product or sample description. Renders nothing without one. */
|
|
1766
2195
|
function ProductCardDescription({ className }) {
|
|
1767
|
-
const
|
|
1768
|
-
|
|
2196
|
+
const card = useProductCardInternal("ProductCardDescription");
|
|
2197
|
+
const description = card.source === "sample" ? card.sample.description : card.product.description;
|
|
2198
|
+
if (!description) return null;
|
|
1769
2199
|
return /* @__PURE__ */ jsx("div", {
|
|
1770
2200
|
...partProps("product-card-description", className),
|
|
1771
2201
|
children: /* @__PURE__ */ jsx("p", {
|
|
1772
2202
|
className: "line-clamp-2 text-sm leading-6 text-commerce-muted",
|
|
1773
|
-
children:
|
|
2203
|
+
children: description
|
|
1774
2204
|
})
|
|
1775
2205
|
});
|
|
1776
2206
|
}
|
|
1777
|
-
const formatRange = (range, money$1) => {
|
|
2207
|
+
const formatRange = (range, money$1, moneyRange$1) => {
|
|
1778
2208
|
const min = range?.min;
|
|
1779
2209
|
const max = range?.max;
|
|
1780
2210
|
if (typeof min !== "number") return null;
|
|
1781
|
-
return
|
|
2211
|
+
return typeof max === "number" && max !== min ? moneyRange$1(min, max) : money$1(min);
|
|
1782
2212
|
};
|
|
1783
2213
|
/**
|
|
1784
2214
|
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
1785
|
-
* when the lowest compare-at price is above the highest price.
|
|
2215
|
+
* when the lowest compare-at price is above the highest price. For a sample, when its `compareAtPrice` is above its
|
|
2216
|
+
* price. Renders nothing otherwise.
|
|
1786
2217
|
*/
|
|
1787
2218
|
function ProductCardBadge({ className }) {
|
|
1788
|
-
const
|
|
1789
|
-
|
|
2219
|
+
const card = useProductCardInternal("ProductCardBadge");
|
|
2220
|
+
const text = useMessage();
|
|
2221
|
+
if (card.source === "sample" ? sampleCompareAt(card.sample) === null : !isProductOnSale(card.product)) return null;
|
|
1790
2222
|
return /* @__PURE__ */ jsx("div", {
|
|
1791
2223
|
...partProps("product-card-badge", className),
|
|
1792
2224
|
children: /* @__PURE__ */ jsx("span", {
|
|
1793
2225
|
className: saleBadgeClass,
|
|
1794
|
-
children: "
|
|
2226
|
+
children: text("sale")
|
|
1795
2227
|
})
|
|
1796
2228
|
});
|
|
1797
2229
|
}
|
|
2230
|
+
const sampleCompareAt = (sample) => sample.compareAtPrice !== void 0 && sample.compareAtPrice > sample.price ? sample.compareAtPrice : null;
|
|
1798
2231
|
/** The product's price, or its price range when variants differ. A single price on sale shows its compare-at price. */
|
|
1799
2232
|
function ProductCardPrice({ className }) {
|
|
1800
2233
|
const card = useProductCardInternal("ProductCardPrice");
|
|
1801
2234
|
const money$1 = useMoney();
|
|
1802
|
-
const
|
|
2235
|
+
const moneyRange$1 = useMoneyRange();
|
|
2236
|
+
const message = useMessage();
|
|
2237
|
+
const text = card.source === "sample" ? money$1(card.sample.price, sampleCurrency) : formatRange(card.product.priceRange, money$1, moneyRange$1) ?? message("priceUnavailable");
|
|
1803
2238
|
const range = card.source === "live" ? card.product.priceRange : null;
|
|
1804
|
-
|
|
1805
|
-
|
|
2239
|
+
let starting = null;
|
|
2240
|
+
if (card.source === "sample") starting = sampleCompareAt(card.sample);
|
|
2241
|
+
else if (range?.min === range?.max) starting = getStartingCompareAt(card.product);
|
|
2242
|
+
const compareAt = starting === null ? null : money$1(starting, card.source === "sample" ? sampleCurrency : void 0);
|
|
1806
2243
|
return /* @__PURE__ */ jsx("div", {
|
|
1807
2244
|
...partProps("product-card-price", className),
|
|
1808
2245
|
children: /* @__PURE__ */ jsxs("p", {
|
|
@@ -1812,7 +2249,7 @@ function ProductCardPrice({ className }) {
|
|
|
1812
2249
|
children: text
|
|
1813
2250
|
}), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1814
2251
|
className: "sr-only",
|
|
1815
|
-
children: "
|
|
2252
|
+
children: message("wasPrice")
|
|
1816
2253
|
}), /* @__PURE__ */ jsx("del", {
|
|
1817
2254
|
className: "text-commerce-muted",
|
|
1818
2255
|
children: compareAt
|
|
@@ -1827,6 +2264,7 @@ function ProductCardPrice({ className }) {
|
|
|
1827
2264
|
function ProductCardSwatches({ className }) {
|
|
1828
2265
|
const { product } = useProductCardInternal("ProductCardSwatches");
|
|
1829
2266
|
const { getSwatch } = useCommerce();
|
|
2267
|
+
const text = useMessage();
|
|
1830
2268
|
if (!getSwatch || !product) return null;
|
|
1831
2269
|
for (const attribute of getProductAttributes(product)) {
|
|
1832
2270
|
const swatches = attribute.values.map((value) => ({
|
|
@@ -1844,16 +2282,13 @@ function ProductCardSwatches({ className }) {
|
|
|
1844
2282
|
children: /* @__PURE__ */ jsxs("p", {
|
|
1845
2283
|
className: "flex items-center gap-1.5 text-xs text-commerce-muted",
|
|
1846
2284
|
children: [
|
|
1847
|
-
/* @__PURE__ */
|
|
2285
|
+
/* @__PURE__ */ jsx("span", {
|
|
1848
2286
|
className: "sr-only",
|
|
1849
|
-
children:
|
|
1850
|
-
swatches.length,
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
" ",
|
|
1855
|
-
swatches.map((swatch) => swatch.label).join(", ")
|
|
1856
|
-
]
|
|
2287
|
+
children: text("swatchSummary", {
|
|
2288
|
+
count: swatches.length,
|
|
2289
|
+
attribute: inSentence(attribute.label),
|
|
2290
|
+
values: swatches.map((swatch) => swatch.label).join(", ")
|
|
2291
|
+
})
|
|
1857
2292
|
}),
|
|
1858
2293
|
shown.map((swatch) => /* @__PURE__ */ jsx("span", {
|
|
1859
2294
|
"aria-hidden": "true",
|
|
@@ -1892,6 +2327,7 @@ function ProductCardAction({ className, children }) {
|
|
|
1892
2327
|
function LiveAction({ product, name, href, label }) {
|
|
1893
2328
|
const selectedSku = getSingleMatchedSku(product);
|
|
1894
2329
|
const hasVariants = getProductAttributes(product).length > 0;
|
|
2330
|
+
const text = useMessage();
|
|
1895
2331
|
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCart, {
|
|
1896
2332
|
sku: selectedSku,
|
|
1897
2333
|
name,
|
|
@@ -1910,11 +2346,13 @@ function LiveAction({ product, name, href, label }) {
|
|
|
1910
2346
|
action,
|
|
1911
2347
|
busy: false,
|
|
1912
2348
|
soldOut: false
|
|
1913
|
-
}, hasVariants ? "
|
|
2349
|
+
}, text(hasVariants ? "chooseOptions" : "viewProduct"))
|
|
1914
2350
|
});
|
|
1915
2351
|
}
|
|
1916
2352
|
function SampleAction({ sample, label }) {
|
|
1917
2353
|
const { add } = useSampleCart();
|
|
2354
|
+
const text = useMessage();
|
|
2355
|
+
const name = sampleName(text, sample);
|
|
1918
2356
|
const custom = renderLabel(label, {
|
|
1919
2357
|
action: "add",
|
|
1920
2358
|
busy: false,
|
|
@@ -1923,20 +2361,16 @@ function SampleAction({ sample, label }) {
|
|
|
1923
2361
|
return custom === null ? /* @__PURE__ */ jsx("button", {
|
|
1924
2362
|
type: "button",
|
|
1925
2363
|
className: `${secondaryButtonClass} w-full`,
|
|
1926
|
-
"aria-label":
|
|
1927
|
-
onClick: () => add(sample
|
|
1928
|
-
children: "
|
|
2364
|
+
"aria-label": text("addSampleToCart", { name }),
|
|
2365
|
+
onClick: () => add(sample),
|
|
2366
|
+
children: text("addToCart")
|
|
1929
2367
|
}) : /* @__PURE__ */ jsxs("button", {
|
|
1930
2368
|
type: "button",
|
|
1931
2369
|
className: `${secondaryButtonClass} w-full`,
|
|
1932
|
-
onClick: () => add(sample
|
|
2370
|
+
onClick: () => add(sample),
|
|
1933
2371
|
children: [custom, /* @__PURE__ */ jsxs("span", {
|
|
1934
2372
|
className: "sr-only",
|
|
1935
|
-
children: [
|
|
1936
|
-
" (",
|
|
1937
|
-
sample.name,
|
|
1938
|
-
", sample)"
|
|
1939
|
-
]
|
|
2373
|
+
children: [" ", text("sampleItemName", { name })]
|
|
1940
2374
|
})]
|
|
1941
2375
|
});
|
|
1942
2376
|
}
|
|
@@ -1984,14 +2418,39 @@ function useRootParams(urlState) {
|
|
|
1984
2418
|
//#region src/catalog.tsx
|
|
1985
2419
|
const CatalogContext = createContext(null);
|
|
1986
2420
|
const CatalogEmptyContext = createContext(null);
|
|
1987
|
-
const
|
|
2421
|
+
const builtInCatalog = {
|
|
2422
|
+
source: "sample",
|
|
2423
|
+
samples: builtInSamples
|
|
2424
|
+
};
|
|
1988
2425
|
function useCatalogInternal(part) {
|
|
1989
2426
|
const context = useContext(CatalogContext);
|
|
1990
2427
|
if (!context) throw new Error(`${part} must be rendered inside <CatalogRoot>.`);
|
|
1991
2428
|
return context;
|
|
1992
2429
|
}
|
|
1993
|
-
/**
|
|
1994
|
-
function
|
|
2430
|
+
/** Read how many products the catalog shows, and whether they are samples, for a custom summary. */
|
|
2431
|
+
function useCatalog() {
|
|
2432
|
+
const catalog = useCatalogInternal("useCatalog");
|
|
2433
|
+
let count;
|
|
2434
|
+
let status = "ready";
|
|
2435
|
+
if (catalog.source === "sample") count = catalog.samples.length;
|
|
2436
|
+
else {
|
|
2437
|
+
count = catalog.nodes.length;
|
|
2438
|
+
if (catalog.products.isPending) status = "loading";
|
|
2439
|
+
else if (catalog.products.isError) status = "error";
|
|
2440
|
+
}
|
|
2441
|
+
const isSample = catalog.source === "sample";
|
|
2442
|
+
return useMemo(() => ({
|
|
2443
|
+
count,
|
|
2444
|
+
isSample,
|
|
2445
|
+
status
|
|
2446
|
+
}), [
|
|
2447
|
+
count,
|
|
2448
|
+
isSample,
|
|
2449
|
+
status
|
|
2450
|
+
]);
|
|
2451
|
+
}
|
|
2452
|
+
/** Loads one cursor page of products, or the samples, and provides them to catalog parts. */
|
|
2453
|
+
function CatalogRoot({ children, className, sampleProducts = false, pageSize = 6, urlState = true }) {
|
|
1995
2454
|
const { connection } = useCommerce();
|
|
1996
2455
|
const [custom, setCustom] = useState(false);
|
|
1997
2456
|
const { register, has } = usePartRegistry(useCallback((registered) => setCustom(registered("empty")), []));
|
|
@@ -2003,6 +2462,25 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2003
2462
|
custom
|
|
2004
2463
|
}), [register, custom]);
|
|
2005
2464
|
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new Error(`CatalogRoot pageSize must be a whole number of 1 or more, not ${pageSize}.`);
|
|
2465
|
+
const hostSamples = useStableSamples(Array.isArray(sampleProducts) ? sampleProducts : null);
|
|
2466
|
+
const problem = useMemo(() => hostSamples && sampleProblem(hostSamples), [hostSamples]);
|
|
2467
|
+
if (problem && connection === "unbound") throw new Error(problem);
|
|
2468
|
+
useEffect(() => {
|
|
2469
|
+
if (problem && process.env.NODE_ENV !== "production") console.error(`[gd-commerce-storefront] ${problem}`);
|
|
2470
|
+
}, [problem]);
|
|
2471
|
+
const { refresh } = useSampleCart();
|
|
2472
|
+
useEffect(() => {
|
|
2473
|
+
if (hostSamples && !problem && connection === "unbound") refresh(hostSamples);
|
|
2474
|
+
}, [
|
|
2475
|
+
hostSamples,
|
|
2476
|
+
problem,
|
|
2477
|
+
connection,
|
|
2478
|
+
refresh
|
|
2479
|
+
]);
|
|
2480
|
+
const sampleCatalog = useMemo(() => hostSamples ? {
|
|
2481
|
+
source: "sample",
|
|
2482
|
+
samples: hostSamples
|
|
2483
|
+
} : builtInCatalog, [hostSamples]);
|
|
2006
2484
|
let content;
|
|
2007
2485
|
if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
|
|
2008
2486
|
className,
|
|
@@ -2010,7 +2488,7 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2010
2488
|
urlState,
|
|
2011
2489
|
children
|
|
2012
2490
|
});
|
|
2013
|
-
else if (connection === "unbound" && sampleProducts
|
|
2491
|
+
else if (connection === "unbound" && sampleProducts) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
2014
2492
|
value: sampleCatalog,
|
|
2015
2493
|
children: /* @__PURE__ */ jsx("section", {
|
|
2016
2494
|
...partProps("catalog-root", className),
|
|
@@ -2027,6 +2505,13 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2027
2505
|
children: content
|
|
2028
2506
|
}) });
|
|
2029
2507
|
}
|
|
2508
|
+
/** The previous list while the samples and their fields are unchanged, so an inline list does not rebuild the cards. */
|
|
2509
|
+
function useStableSamples(samples) {
|
|
2510
|
+
const [stable, setStable] = useState(samples);
|
|
2511
|
+
const same = stable === samples || !!stable && !!samples && sameSamples(stable, samples);
|
|
2512
|
+
if (!same) setStable(samples);
|
|
2513
|
+
return same ? stable : samples;
|
|
2514
|
+
}
|
|
2030
2515
|
function CatalogLoader({ children, className, pageSize, urlState }) {
|
|
2031
2516
|
const { config } = useCommerce();
|
|
2032
2517
|
const [params, setParams] = useRootParams(urlState);
|
|
@@ -2075,46 +2560,47 @@ function CatalogLoader({ children, className, pageSize, urlState }) {
|
|
|
2075
2560
|
});
|
|
2076
2561
|
}
|
|
2077
2562
|
/** The catalog title as the page's H1, with a description. */
|
|
2078
|
-
function CatalogHeader({ title
|
|
2563
|
+
function CatalogHeader({ title, description, className }) {
|
|
2079
2564
|
useCatalogInternal("CatalogHeader");
|
|
2565
|
+
const text = useMessage();
|
|
2080
2566
|
return /* @__PURE__ */ jsxs("header", {
|
|
2081
2567
|
...partProps("catalog-header", className),
|
|
2082
|
-
children: [/* @__PURE__ */ jsx("h1", {
|
|
2568
|
+
children: [title !== null && /* @__PURE__ */ jsx("h1", {
|
|
2083
2569
|
className: "max-w-3xl text-balance text-3xl font-semibold leading-tight tracking-tight",
|
|
2084
|
-
children: title
|
|
2085
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
2570
|
+
children: title === void 0 ? text("catalogTitle") : title
|
|
2571
|
+
}), description !== null && /* @__PURE__ */ jsx("p", {
|
|
2086
2572
|
className: "mt-1 max-w-xl text-base leading-7 text-commerce-muted",
|
|
2087
|
-
children: description
|
|
2573
|
+
children: description === void 0 ? text("catalogDescription") : description
|
|
2088
2574
|
})]
|
|
2089
2575
|
});
|
|
2090
2576
|
}
|
|
2091
2577
|
/** Announces loading, failure, or how many products the page shows. */
|
|
2092
2578
|
function CatalogSummary({ className }) {
|
|
2093
|
-
const
|
|
2579
|
+
const { key, values } = summary(useCatalogInternal("CatalogSummary"));
|
|
2580
|
+
const text = useMessage();
|
|
2094
2581
|
return /* @__PURE__ */ jsx("div", {
|
|
2095
2582
|
...partProps("catalog-summary", className),
|
|
2096
2583
|
children: /* @__PURE__ */ jsx("p", {
|
|
2097
2584
|
className: "text-sm text-commerce-muted",
|
|
2098
2585
|
role: "status",
|
|
2099
|
-
children:
|
|
2586
|
+
children: text(key, values)
|
|
2100
2587
|
})
|
|
2101
2588
|
});
|
|
2102
2589
|
}
|
|
2103
2590
|
function summary(catalog) {
|
|
2104
|
-
if (catalog.source === "sample") return
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2591
|
+
if (catalog.source === "sample") return {
|
|
2592
|
+
key: "sampleCatalogSummary",
|
|
2593
|
+
values: { currency: sampleCurrency }
|
|
2594
|
+
};
|
|
2595
|
+
if (catalog.products.isPending) return { key: "loadingProducts" };
|
|
2596
|
+
if (catalog.products.isError) return { key: "productsUnavailable" };
|
|
2597
|
+
return {
|
|
2598
|
+
key: "productCount",
|
|
2599
|
+
values: { count: catalog.nodes.length }
|
|
2600
|
+
};
|
|
2108
2601
|
}
|
|
2109
2602
|
/** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
|
|
2110
2603
|
const catalogGridClass = "grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] lg:gap-y-12";
|
|
2111
|
-
const sampleItems = sampleProducts.map((sample) => ({
|
|
2112
|
-
key: sample.key,
|
|
2113
|
-
item: {
|
|
2114
|
-
source: "sample",
|
|
2115
|
-
sample
|
|
2116
|
-
}
|
|
2117
|
-
}));
|
|
2118
2604
|
const packagedCard$1 = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
2119
2605
|
className: packagedCardClass,
|
|
2120
2606
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
@@ -2128,28 +2614,37 @@ function CatalogProducts({ className, children }) {
|
|
|
2128
2614
|
const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
|
|
2129
2615
|
const card = hasChildren(children) ? children : packagedCard$1;
|
|
2130
2616
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
2617
|
+
const samples = catalog.source === "sample" ? catalog.samples : null;
|
|
2131
2618
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
2132
2619
|
key: product.id ?? "",
|
|
2133
2620
|
item: {
|
|
2134
2621
|
source: "live",
|
|
2135
2622
|
product
|
|
2136
2623
|
}
|
|
2137
|
-
})) :
|
|
2624
|
+
})) : (samples ?? []).map((sample) => ({
|
|
2625
|
+
key: sample.key,
|
|
2626
|
+
item: {
|
|
2627
|
+
source: "sample",
|
|
2628
|
+
sample
|
|
2629
|
+
}
|
|
2630
|
+
})), [nodes, samples]);
|
|
2138
2631
|
const products = catalog.source === "live" ? catalog.products : null;
|
|
2632
|
+
const { messages } = useCommerce();
|
|
2633
|
+
const text = useMessage();
|
|
2139
2634
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2140
2635
|
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
2141
2636
|
role: "alert",
|
|
2142
2637
|
className: errorNoticeClass,
|
|
2143
|
-
children: [/* @__PURE__ */ jsx("p", { children:
|
|
2638
|
+
children: [/* @__PURE__ */ jsx("p", { children: describeError(products.error, messages) }), /* @__PURE__ */ jsx("button", {
|
|
2144
2639
|
type: "button",
|
|
2145
2640
|
className: errorActionClass,
|
|
2146
2641
|
onClick: () => void products.refetch(),
|
|
2147
|
-
children: "
|
|
2642
|
+
children: text("retryProducts")
|
|
2148
2643
|
})]
|
|
2149
2644
|
}),
|
|
2150
2645
|
products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
|
|
2151
2646
|
className: "py-16 text-center text-commerce-muted",
|
|
2152
|
-
children: "
|
|
2647
|
+
children: text("noProducts")
|
|
2153
2648
|
}),
|
|
2154
2649
|
/* @__PURE__ */ jsx("div", {
|
|
2155
2650
|
...partProps("catalog-products", className),
|
|
@@ -2161,7 +2656,7 @@ function CatalogProducts({ className, children }) {
|
|
|
2161
2656
|
"aria-hidden": "true",
|
|
2162
2657
|
className: "motion-safe:animate-pulse",
|
|
2163
2658
|
children: [
|
|
2164
|
-
/* @__PURE__ */ jsx("div", { className: "aspect-
|
|
2659
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-(--commerce-card-image-aspect,1/1) bg-commerce-tint-strong" }),
|
|
2165
2660
|
/* @__PURE__ */ jsxs("div", {
|
|
2166
2661
|
className: "space-y-2 pt-3",
|
|
2167
2662
|
children: [/* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 rounded bg-commerce-tint-strong" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-1/4 rounded bg-commerce-tint-strong" })]
|
|
@@ -2183,10 +2678,11 @@ function CatalogEmpty({ className, children }) {
|
|
|
2183
2678
|
const catalog = useCatalogInternal("CatalogEmpty");
|
|
2184
2679
|
const register = useContext(CatalogEmptyContext)?.register;
|
|
2185
2680
|
useEffect(() => register?.("empty"), [register]);
|
|
2681
|
+
const text = useMessage();
|
|
2186
2682
|
if (catalog.source === "sample" || !catalog.products.isSuccess || catalog.nodes.length) return null;
|
|
2187
2683
|
return /* @__PURE__ */ jsx("div", {
|
|
2188
2684
|
...partProps("catalog-empty", className),
|
|
2189
|
-
children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "
|
|
2685
|
+
children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: text("noProducts") })
|
|
2190
2686
|
});
|
|
2191
2687
|
}
|
|
2192
2688
|
/**
|
|
@@ -2194,13 +2690,14 @@ function CatalogEmpty({ className, children }) {
|
|
|
2194
2690
|
*/
|
|
2195
2691
|
function CatalogPagination({ className }) {
|
|
2196
2692
|
const catalog = useCatalogInternal("CatalogPagination");
|
|
2693
|
+
const text = useMessage();
|
|
2197
2694
|
if (catalog.source === "sample") return null;
|
|
2198
2695
|
const { after, goToPage } = catalog;
|
|
2199
2696
|
const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
|
|
2200
2697
|
const hasNext = !!(pageInfo?.hasNextPage && pageInfo.endCursor);
|
|
2201
2698
|
if (!after && !hasNext) return null;
|
|
2202
2699
|
return /* @__PURE__ */ jsx("nav", {
|
|
2203
|
-
"aria-label": "
|
|
2700
|
+
"aria-label": text("productPages"),
|
|
2204
2701
|
...partProps("catalog-pagination", className),
|
|
2205
2702
|
children: /* @__PURE__ */ jsxs("div", {
|
|
2206
2703
|
className: "flex justify-center gap-3",
|
|
@@ -2208,20 +2705,20 @@ function CatalogPagination({ className }) {
|
|
|
2208
2705
|
type: "button",
|
|
2209
2706
|
className: secondaryButtonClass,
|
|
2210
2707
|
onClick: () => goToPage(),
|
|
2211
|
-
children: "
|
|
2708
|
+
children: text("firstPage")
|
|
2212
2709
|
}), hasNext && pageInfo?.endCursor && /* @__PURE__ */ jsx("button", {
|
|
2213
2710
|
type: "button",
|
|
2214
2711
|
className: secondaryButtonClass,
|
|
2215
2712
|
onClick: () => goToPage(pageInfo.endCursor ?? void 0),
|
|
2216
|
-
children: "
|
|
2713
|
+
children: text("nextPage")
|
|
2217
2714
|
})]
|
|
2218
2715
|
})
|
|
2219
2716
|
});
|
|
2220
2717
|
}
|
|
2221
2718
|
/** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
|
|
2222
|
-
function Catalog({ title, description, showHeader = true, sampleProducts
|
|
2719
|
+
function Catalog({ title, description, showHeader = true, sampleProducts, pageSize }) {
|
|
2223
2720
|
return /* @__PURE__ */ jsxs(CatalogRoot, {
|
|
2224
|
-
sampleProducts
|
|
2721
|
+
sampleProducts,
|
|
2225
2722
|
pageSize,
|
|
2226
2723
|
children: [
|
|
2227
2724
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -2325,7 +2822,8 @@ function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
|
|
|
2325
2822
|
}) });
|
|
2326
2823
|
}
|
|
2327
2824
|
function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
|
|
2328
|
-
const { config, getSwatch } = useCommerce();
|
|
2825
|
+
const { config, getSwatch, messages } = useCommerce();
|
|
2826
|
+
const text = useMessage();
|
|
2329
2827
|
const [params, setParams] = useRootParams(urlState);
|
|
2330
2828
|
const [quantity, setQuantity] = useState(1);
|
|
2331
2829
|
const [imageIndex, setImageIndex] = useState(0);
|
|
@@ -2403,7 +2901,7 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2403
2901
|
children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
|
|
2404
2902
|
role: "status",
|
|
2405
2903
|
className: "text-sm text-commerce-muted",
|
|
2406
|
-
children: "
|
|
2904
|
+
children: text("loadingProduct")
|
|
2407
2905
|
})
|
|
2408
2906
|
});
|
|
2409
2907
|
if (product.isError && !productNotFound) {
|
|
@@ -2412,22 +2910,22 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2412
2910
|
role: "alert",
|
|
2413
2911
|
...partProps("product-details-root", className),
|
|
2414
2912
|
children: renderLabel(error, {
|
|
2415
|
-
message:
|
|
2913
|
+
message: describeError(product.error, messages),
|
|
2416
2914
|
retry
|
|
2417
2915
|
}, /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2418
2916
|
/* @__PURE__ */ jsx("h1", {
|
|
2419
2917
|
className: "text-2xl font-semibold tracking-tight",
|
|
2420
|
-
children: "
|
|
2918
|
+
children: text("productUnavailable")
|
|
2421
2919
|
}),
|
|
2422
2920
|
/* @__PURE__ */ jsx("p", {
|
|
2423
2921
|
className: `my-4 ${errorNoticeClass}`,
|
|
2424
|
-
children:
|
|
2922
|
+
children: describeError(product.error, messages)
|
|
2425
2923
|
}),
|
|
2426
2924
|
/* @__PURE__ */ jsx("button", {
|
|
2427
2925
|
type: "button",
|
|
2428
2926
|
className: buttonClass,
|
|
2429
2927
|
onClick: retry,
|
|
2430
|
-
children: "
|
|
2928
|
+
children: text("retryProduct")
|
|
2431
2929
|
})
|
|
2432
2930
|
] }))
|
|
2433
2931
|
});
|
|
@@ -2436,11 +2934,11 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2436
2934
|
...partProps("product-details-root", className),
|
|
2437
2935
|
children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
|
|
2438
2936
|
className: "text-2xl font-semibold tracking-tight",
|
|
2439
|
-
children: "
|
|
2937
|
+
children: text("productNotFound")
|
|
2440
2938
|
}), /* @__PURE__ */ jsx(Link, {
|
|
2441
2939
|
className: `${buttonClass} mt-6`,
|
|
2442
2940
|
to: config.catalogPath,
|
|
2443
|
-
children: "
|
|
2941
|
+
children: text("backToShop")
|
|
2444
2942
|
})] })
|
|
2445
2943
|
});
|
|
2446
2944
|
const value = {
|
|
@@ -2539,11 +3037,11 @@ function getSoldOutValues(group, attributes, selections) {
|
|
|
2539
3037
|
//#endregion
|
|
2540
3038
|
//#region src/product-purchase.tsx
|
|
2541
3039
|
const disabledReasons = {
|
|
2542
|
-
incomplete: "
|
|
2543
|
-
checking: "
|
|
2544
|
-
"variant-error": "
|
|
2545
|
-
unavailable: "
|
|
2546
|
-
"not-purchasable": "
|
|
3040
|
+
incomplete: "selectAllOptions",
|
|
3041
|
+
checking: "checkingVariant",
|
|
3042
|
+
"variant-error": "variantCheckFailed",
|
|
3043
|
+
unavailable: "combinationUnavailable",
|
|
3044
|
+
"not-purchasable": "notPurchasable"
|
|
2547
3045
|
};
|
|
2548
3046
|
/** Above this many values, buttons wrap into a wall, so the option is a select. */
|
|
2549
3047
|
const maxChoiceButtons = 8;
|
|
@@ -2608,14 +3106,11 @@ const swatchClass = "relative block h-9 w-9 overflow-hidden rounded-full border
|
|
|
2608
3106
|
function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
2609
3107
|
const name = useId();
|
|
2610
3108
|
const errorId = useId();
|
|
2611
|
-
const
|
|
3109
|
+
const text = useMessage();
|
|
3110
|
+
const error = missing && /* @__PURE__ */ jsx("p", {
|
|
2612
3111
|
id: errorId,
|
|
2613
3112
|
className: "mt-2 inline-block rounded bg-red-50 px-2 py-1 text-sm text-red-800",
|
|
2614
|
-
children:
|
|
2615
|
-
"Select ",
|
|
2616
|
-
inSentence(label),
|
|
2617
|
-
"."
|
|
2618
|
-
]
|
|
3113
|
+
children: text("selectAttributeError", { attribute: inSentence(label) })
|
|
2619
3114
|
});
|
|
2620
3115
|
if (choices.length > maxChoiceButtons) return /* @__PURE__ */ jsxs("fieldset", { children: [
|
|
2621
3116
|
/* @__PURE__ */ jsx("legend", {
|
|
@@ -2628,12 +3123,12 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
|
2628
3123
|
"aria-describedby": missing ? errorId : void 0,
|
|
2629
3124
|
value: selected,
|
|
2630
3125
|
onChange: (event) => onSelect(event.target.value),
|
|
2631
|
-
children: [/* @__PURE__ */
|
|
3126
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
2632
3127
|
value: "",
|
|
2633
|
-
children:
|
|
3128
|
+
children: text("selectAttribute", { attribute: inSentence(label) })
|
|
2634
3129
|
}), choices.map((choice) => /* @__PURE__ */ jsx("option", {
|
|
2635
3130
|
value: choice.value,
|
|
2636
|
-
children: choice.soldOut ?
|
|
3131
|
+
children: choice.soldOut ? text("soldOutOption", { label: choice.label }) : choice.label
|
|
2637
3132
|
}, choice.value))]
|
|
2638
3133
|
}),
|
|
2639
3134
|
error
|
|
@@ -2654,7 +3149,7 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
|
2654
3149
|
className: `flex flex-wrap ${swatches ? "gap-3" : "gap-2"}`,
|
|
2655
3150
|
children: choices.map((choice) => /* @__PURE__ */ jsxs("label", {
|
|
2656
3151
|
className: "group relative cursor-pointer",
|
|
2657
|
-
title: swatches ?
|
|
3152
|
+
title: swatches ? choice.soldOut ? text("soldOutChoice", { label: choice.label }) : choice.label : void 0,
|
|
2658
3153
|
children: [/* @__PURE__ */ jsx("input", {
|
|
2659
3154
|
type: "radio",
|
|
2660
3155
|
className: "peer absolute inset-0 z-10 m-0 cursor-pointer opacity-0",
|
|
@@ -2662,7 +3157,7 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
|
2662
3157
|
value: choice.value,
|
|
2663
3158
|
checked: choice.value === selected,
|
|
2664
3159
|
"aria-invalid": missing || void 0,
|
|
2665
|
-
"aria-label": choice.soldOut ?
|
|
3160
|
+
"aria-label": choice.soldOut ? text("soldOutChoice", { label: choice.label }) : void 0,
|
|
2666
3161
|
"data-commerce-control": "choice",
|
|
2667
3162
|
onChange: () => onSelect(choice.value)
|
|
2668
3163
|
}), swatches ? /* @__PURE__ */ jsxs("span", {
|
|
@@ -2688,12 +3183,13 @@ function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
|
2688
3183
|
function SkuOption() {
|
|
2689
3184
|
const { skuOptions, explicitSkuId, selectSku, showMissing } = useProductDetailsInternal("ProductDetailsOptions");
|
|
2690
3185
|
useSkuOptionRegistration();
|
|
3186
|
+
const text = useMessage();
|
|
2691
3187
|
const missing = showMissing && !explicitSkuId;
|
|
2692
3188
|
return /* @__PURE__ */ jsx("div", {
|
|
2693
3189
|
...partProps("product-details-sku-option"),
|
|
2694
3190
|
"data-commerce-missing": missing || void 0,
|
|
2695
3191
|
children: /* @__PURE__ */ jsx(ChoiceGroup, {
|
|
2696
|
-
label: "
|
|
3192
|
+
label: text("skuOptionLabel"),
|
|
2697
3193
|
selected: explicitSkuId,
|
|
2698
3194
|
missing,
|
|
2699
3195
|
onSelect: selectSku,
|
|
@@ -2737,6 +3233,8 @@ function ProductDetailsOption({ attribute, className }) {
|
|
|
2737
3233
|
*/
|
|
2738
3234
|
function ProductDetailsPurchaseStatus({ className }) {
|
|
2739
3235
|
const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
|
|
3236
|
+
const { messages } = useCommerce();
|
|
3237
|
+
const text = useMessage();
|
|
2740
3238
|
let content;
|
|
2741
3239
|
switch (status) {
|
|
2742
3240
|
case "ready":
|
|
@@ -2745,11 +3243,11 @@ function ProductDetailsPurchaseStatus({ className }) {
|
|
|
2745
3243
|
content = /* @__PURE__ */ jsxs("div", {
|
|
2746
3244
|
role: "alert",
|
|
2747
3245
|
className: errorNoticeClass,
|
|
2748
|
-
children: [/* @__PURE__ */ jsx("p", { children:
|
|
3246
|
+
children: [/* @__PURE__ */ jsx("p", { children: describeError(variantError, messages) }), /* @__PURE__ */ jsx("button", {
|
|
2749
3247
|
type: "button",
|
|
2750
3248
|
className: errorActionClass,
|
|
2751
3249
|
onClick: retryVariant,
|
|
2752
|
-
children: "
|
|
3250
|
+
children: text("retryAvailability")
|
|
2753
3251
|
})]
|
|
2754
3252
|
});
|
|
2755
3253
|
break;
|
|
@@ -2757,20 +3255,20 @@ function ProductDetailsPurchaseStatus({ className }) {
|
|
|
2757
3255
|
content = /* @__PURE__ */ jsx("p", {
|
|
2758
3256
|
role: "status",
|
|
2759
3257
|
className: errorNoticeClass,
|
|
2760
|
-
children: disabledReasons[status]
|
|
3258
|
+
children: text(disabledReasons[status])
|
|
2761
3259
|
});
|
|
2762
3260
|
break;
|
|
2763
3261
|
case "not-purchasable":
|
|
2764
3262
|
content = /* @__PURE__ */ jsx("p", {
|
|
2765
3263
|
className: "text-sm text-commerce-muted",
|
|
2766
|
-
children: disabledReasons[status]
|
|
3264
|
+
children: text(disabledReasons[status])
|
|
2767
3265
|
});
|
|
2768
3266
|
break;
|
|
2769
3267
|
case "checking":
|
|
2770
3268
|
content = /* @__PURE__ */ jsx("p", {
|
|
2771
3269
|
role: "status",
|
|
2772
3270
|
className: "text-sm text-commerce-muted",
|
|
2773
|
-
children: disabledReasons[status]
|
|
3271
|
+
children: text(disabledReasons[status])
|
|
2774
3272
|
});
|
|
2775
3273
|
break;
|
|
2776
3274
|
}
|
|
@@ -2782,13 +3280,14 @@ function ProductDetailsPurchaseStatus({ className }) {
|
|
|
2782
3280
|
/** Stock for the verified SKU. Renders nothing until a SKU is verified. */
|
|
2783
3281
|
function ProductDetailsAvailability({ className }) {
|
|
2784
3282
|
const { selectedSku, availableQuantity } = useProductDetailsInternal("ProductDetailsAvailability");
|
|
3283
|
+
const text = useMessage();
|
|
2785
3284
|
if (!selectedSku) return null;
|
|
2786
3285
|
return /* @__PURE__ */ jsx("div", {
|
|
2787
3286
|
...partProps("product-details-availability", className),
|
|
2788
3287
|
children: /* @__PURE__ */ jsx("p", {
|
|
2789
3288
|
className: "text-sm text-commerce-muted",
|
|
2790
3289
|
"aria-live": "polite",
|
|
2791
|
-
children: availableQuantity === null ? "
|
|
3290
|
+
children: availableQuantity === null ? text("availableToOrder") : isSoldOut(availableQuantity) ? text("variantOutOfStock") : text("availableCount", { count: availableQuantity })
|
|
2792
3291
|
})
|
|
2793
3292
|
});
|
|
2794
3293
|
}
|
|
@@ -2815,9 +3314,11 @@ function ProductDetailsQuantity({ className }) {
|
|
|
2815
3314
|
const { selectedSku, availableQuantity, quantity, setQuantity, name } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
2816
3315
|
const id = useId();
|
|
2817
3316
|
const problemId = useId();
|
|
3317
|
+
const text = useMessage();
|
|
2818
3318
|
const soldOut = !!selectedSku && isSoldOut(availableQuantity);
|
|
2819
3319
|
const max = selectedSku && availableQuantity !== null && availableQuantity > 0 ? availableQuantity : null;
|
|
2820
|
-
const
|
|
3320
|
+
const issue = soldOut ? null : quantityProblem(quantity, max);
|
|
3321
|
+
const problem = issue && text(issue.key, issue.values);
|
|
2821
3322
|
const current = Number.isInteger(quantity) ? quantity : 1;
|
|
2822
3323
|
return /* @__PURE__ */ jsxs("div", {
|
|
2823
3324
|
...partProps("product-details-quantity", className),
|
|
@@ -2825,7 +3326,7 @@ function ProductDetailsQuantity({ className }) {
|
|
|
2825
3326
|
/* @__PURE__ */ jsx("label", {
|
|
2826
3327
|
htmlFor: id,
|
|
2827
3328
|
className: "sr-only",
|
|
2828
|
-
children: "
|
|
3329
|
+
children: text("quantity")
|
|
2829
3330
|
}),
|
|
2830
3331
|
/* @__PURE__ */ jsxs("div", {
|
|
2831
3332
|
className: "inline-flex items-center rounded-lg border border-commerce-edge",
|
|
@@ -2833,7 +3334,7 @@ function ProductDetailsQuantity({ className }) {
|
|
|
2833
3334
|
/* @__PURE__ */ jsx("button", {
|
|
2834
3335
|
type: "button",
|
|
2835
3336
|
className: stepperButtonClass,
|
|
2836
|
-
"aria-label":
|
|
3337
|
+
"aria-label": text("decreaseQuantity", { name }),
|
|
2837
3338
|
"data-commerce-control": "decrease",
|
|
2838
3339
|
disabled: soldOut || current <= 1,
|
|
2839
3340
|
onClick: () => setQuantity(current - 1),
|
|
@@ -2855,7 +3356,7 @@ function ProductDetailsQuantity({ className }) {
|
|
|
2855
3356
|
/* @__PURE__ */ jsx("button", {
|
|
2856
3357
|
type: "button",
|
|
2857
3358
|
className: stepperButtonClass,
|
|
2858
|
-
"aria-label":
|
|
3359
|
+
"aria-label": text("increaseQuantity", { name }),
|
|
2859
3360
|
"data-commerce-control": "increase",
|
|
2860
3361
|
disabled: soldOut || max !== null && current >= max,
|
|
2861
3362
|
onClick: () => setQuantity(current + 1),
|
|
@@ -2880,6 +3381,7 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2880
3381
|
const { selectedSku, name, quantity, status, price, attributes, selections, requestMissing } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
2881
3382
|
const { config } = useCommerce();
|
|
2882
3383
|
const money$1 = useMoney();
|
|
3384
|
+
const text = useMessage();
|
|
2883
3385
|
const reasonId = useId();
|
|
2884
3386
|
if (selectedSku) {
|
|
2885
3387
|
const unit = price?.value?.value;
|
|
@@ -2892,8 +3394,8 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2892
3394
|
quantity,
|
|
2893
3395
|
className: buttonClass,
|
|
2894
3396
|
block: true,
|
|
2895
|
-
label: total &&
|
|
2896
|
-
labelName: total ?
|
|
3397
|
+
label: total && text("addToCartWithPrice", { price: total }),
|
|
3398
|
+
labelName: total ? text("addToCartWithPriceName", { price: total }) : void 0,
|
|
2897
3399
|
children
|
|
2898
3400
|
})
|
|
2899
3401
|
});
|
|
@@ -2910,7 +3412,7 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2910
3412
|
children: renderLabel(children, {
|
|
2911
3413
|
busy: false,
|
|
2912
3414
|
soldOut: false
|
|
2913
|
-
}, next ?
|
|
3415
|
+
}, next ? text("selectAttribute", { attribute: inSentence(next) }) : text("selectOption"))
|
|
2914
3416
|
})
|
|
2915
3417
|
});
|
|
2916
3418
|
}
|
|
@@ -2925,11 +3427,11 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2925
3427
|
children: renderLabel(children, {
|
|
2926
3428
|
busy: false,
|
|
2927
3429
|
soldOut: false
|
|
2928
|
-
}, "
|
|
3430
|
+
}, text("addToCart"))
|
|
2929
3431
|
}), /* @__PURE__ */ jsx("span", {
|
|
2930
3432
|
id: reasonId,
|
|
2931
3433
|
hidden: true,
|
|
2932
|
-
children: status === "ready" ? "" : disabledReasons[status]
|
|
3434
|
+
children: status === "ready" ? "" : text(disabledReasons[status])
|
|
2933
3435
|
})]
|
|
2934
3436
|
});
|
|
2935
3437
|
}
|
|
@@ -2958,6 +3460,7 @@ function ProductDetails() {
|
|
|
2958
3460
|
function ProductDetailsBackLink({ className, children }) {
|
|
2959
3461
|
useProductDetailsInternal("ProductDetailsBackLink");
|
|
2960
3462
|
const { config } = useCommerce();
|
|
3463
|
+
const text = useMessage();
|
|
2961
3464
|
return /* @__PURE__ */ jsx("div", {
|
|
2962
3465
|
...partProps("product-details-back-link", className),
|
|
2963
3466
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -2975,13 +3478,14 @@ function ProductDetailsBackLink({ className, children }) {
|
|
|
2975
3478
|
strokeLinecap: "round",
|
|
2976
3479
|
strokeLinejoin: "round"
|
|
2977
3480
|
})
|
|
2978
|
-
}), "
|
|
3481
|
+
}), text("backToShop")] }))
|
|
2979
3482
|
})
|
|
2980
3483
|
});
|
|
2981
3484
|
}
|
|
2982
3485
|
/** Main image with thumbnails; the selected SKU's images replace the product's when present. */
|
|
2983
3486
|
function ProductDetailsGallery({ className }) {
|
|
2984
3487
|
const { images, imageIndex, setImageIndex, name } = useProductDetailsInternal("ProductDetailsGallery");
|
|
3488
|
+
const text = useMessage();
|
|
2985
3489
|
return /* @__PURE__ */ jsxs("div", {
|
|
2986
3490
|
...partProps("product-details-gallery", className),
|
|
2987
3491
|
children: [/* @__PURE__ */ jsx("div", {
|
|
@@ -2995,7 +3499,7 @@ function ProductDetailsGallery({ className }) {
|
|
|
2995
3499
|
children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
|
|
2996
3500
|
type: "button",
|
|
2997
3501
|
className: `h-20 w-20 shrink-0 overflow-hidden bg-commerce-tint outline-offset-2 transition-opacity focus-visible:outline focus-visible:outline-2 focus-visible:outline-current ${imageIndex === index ? "outline outline-2 outline-current" : "opacity-60 hover:opacity-100"}`,
|
|
2998
|
-
"aria-label":
|
|
3502
|
+
"aria-label": text("viewImage", { number: index + 1 }),
|
|
2999
3503
|
"aria-pressed": imageIndex === index,
|
|
3000
3504
|
"data-commerce-control": "thumbnail",
|
|
3001
3505
|
onClick: () => setImageIndex(index),
|
|
@@ -3013,12 +3517,13 @@ function ProductDetailsGallery({ className }) {
|
|
|
3013
3517
|
*/
|
|
3014
3518
|
function ProductDetailsBadge({ className }) {
|
|
3015
3519
|
const { price, product } = useProductDetailsInternal("ProductDetailsBadge");
|
|
3520
|
+
const text = useMessage();
|
|
3016
3521
|
if (!(price ? isOnSale(price.value?.value, price.compareAtValue?.value) : isProductOnSale(product))) return null;
|
|
3017
3522
|
return /* @__PURE__ */ jsx("div", {
|
|
3018
3523
|
...partProps("product-details-badge", className),
|
|
3019
3524
|
children: /* @__PURE__ */ jsx("span", {
|
|
3020
3525
|
className: saleBadgeClass,
|
|
3021
|
-
children: "
|
|
3526
|
+
children: text("sale")
|
|
3022
3527
|
})
|
|
3023
3528
|
});
|
|
3024
3529
|
}
|
|
@@ -3041,6 +3546,7 @@ function ProductDetailsPrice({ className }) {
|
|
|
3041
3546
|
const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
|
|
3042
3547
|
const { config } = useCommerce();
|
|
3043
3548
|
const money$1 = useMoney();
|
|
3549
|
+
const text = useMessage();
|
|
3044
3550
|
let compareAt = null;
|
|
3045
3551
|
if (price?.compareAtValue?.value != null && isOnSale(price.value?.value, price.compareAtValue.value)) compareAt = money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode);
|
|
3046
3552
|
else if (!price) {
|
|
@@ -3053,9 +3559,9 @@ function ProductDetailsPrice({ className }) {
|
|
|
3053
3559
|
"aria-live": "polite",
|
|
3054
3560
|
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xl font-medium tabular-nums",
|
|
3055
3561
|
"data-testid": "product-price",
|
|
3056
|
-
children: [price?.value?.value != null ? money$1(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ?
|
|
3562
|
+
children: [price?.value?.value != null ? money$1(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? text("priceFrom", { price: money$1(product.priceRange.min) }) : text("priceUnavailable"), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
3057
3563
|
className: "sr-only",
|
|
3058
|
-
children: "
|
|
3564
|
+
children: text("wasPrice")
|
|
3059
3565
|
}), /* @__PURE__ */ jsx("del", {
|
|
3060
3566
|
className: "text-base font-normal text-commerce-muted",
|
|
3061
3567
|
children: compareAt
|
|
@@ -3087,10 +3593,11 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
|
3087
3593
|
* the products are the catalog's first ones. Renders nothing until they load, when none are left, if the request
|
|
3088
3594
|
* fails, or while no store is connected.
|
|
3089
3595
|
*/
|
|
3090
|
-
function RelatedProducts({ productId: productIdProp, title
|
|
3596
|
+
function RelatedProducts({ productId: productIdProp, title, limit = 4, className, children }) {
|
|
3091
3597
|
const { productId: routeProductId = "" } = useParams();
|
|
3092
3598
|
const productId = productIdProp ?? routeProductId;
|
|
3093
3599
|
const { config, connection } = useCommerce();
|
|
3600
|
+
const text = useMessage();
|
|
3094
3601
|
const headingId = useId();
|
|
3095
3602
|
const first = limit + 1;
|
|
3096
3603
|
const products = useQuery({
|
|
@@ -3117,12 +3624,12 @@ function RelatedProducts({ productId: productIdProp, title = "You may also like"
|
|
|
3117
3624
|
if (connection !== "ready" || !related.length) return null;
|
|
3118
3625
|
const card = hasChildren(children) ? children : packagedCard;
|
|
3119
3626
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsxs("section", {
|
|
3120
|
-
"aria-labelledby": headingId,
|
|
3627
|
+
"aria-labelledby": title === null ? void 0 : headingId,
|
|
3121
3628
|
...partProps("related-products", className),
|
|
3122
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
3629
|
+
children: [title !== null && /* @__PURE__ */ jsx("h2", {
|
|
3123
3630
|
id: headingId,
|
|
3124
3631
|
className: "mb-6 text-xl font-semibold tracking-tight",
|
|
3125
|
-
children: title
|
|
3632
|
+
children: title === void 0 ? text("relatedTitle") : title
|
|
3126
3633
|
}), /* @__PURE__ */ jsx("div", {
|
|
3127
3634
|
className: catalogGridClass,
|
|
3128
3635
|
children: related.map((product) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
@@ -3137,4 +3644,4 @@ function RelatedProducts({ productId: productIdProp, title = "You may also like"
|
|
|
3137
3644
|
}
|
|
3138
3645
|
|
|
3139
3646
|
//#endregion
|
|
3140
|
-
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, 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, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useMoney, useProductCard, useProductDetails, useProductDetailsOption };
|
|
3647
|
+
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, 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, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, moneyRange, useCart, useCartItem, useCatalog, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|