@lime-bundles/react 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +680 -237
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -2
- package/dist/index.d.ts +50 -2
- package/dist/index.js +635 -187
- package/dist/index.js.map +1 -1
- package/docs/css-variables.md +3 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/FixedBundle.tsx
|
|
2
|
-
import { useCallback as
|
|
2
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
|
|
3
3
|
import {
|
|
4
4
|
formatMoney,
|
|
5
5
|
formatUnitPrice,
|
|
@@ -208,8 +208,340 @@ function useWidgetConfigVars(config) {
|
|
|
208
208
|
);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
// src/
|
|
211
|
+
// src/hooks/useVariantSelection.ts
|
|
212
|
+
import { useEffect as useEffect4, useMemo as useMemo2, useState as useState3, useCallback as useCallback3 } from "react";
|
|
213
|
+
import { variants as variantsNs } from "@lime-bundles/core";
|
|
214
|
+
var { findVariantByOptions, isOptionValueAvailable, toPickerVariant } = variantsNs;
|
|
215
|
+
function defaultInitial(variants) {
|
|
216
|
+
return variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
|
|
217
|
+
}
|
|
218
|
+
function useVariantSelection({
|
|
219
|
+
variants,
|
|
220
|
+
optionNames,
|
|
221
|
+
initialVariant,
|
|
222
|
+
optionValuesByPosition
|
|
223
|
+
}) {
|
|
224
|
+
const seed = useMemo2(
|
|
225
|
+
() => initialVariant ?? defaultInitial(variants),
|
|
226
|
+
[initialVariant, variants]
|
|
227
|
+
);
|
|
228
|
+
const [selectedValues, setSelectedValues] = useState3(
|
|
229
|
+
() => seed ? seed.selectedOptions.map((o) => o.value) : optionNames.map(() => "")
|
|
230
|
+
);
|
|
231
|
+
useEffect4(() => {
|
|
232
|
+
if (!seed) return;
|
|
233
|
+
setSelectedValues(seed.selectedOptions.map((o) => o.value));
|
|
234
|
+
}, [seed?.id]);
|
|
235
|
+
const pickerVariants = useMemo2(
|
|
236
|
+
() => variants.map((v) => toPickerVariant(v)),
|
|
237
|
+
[variants]
|
|
238
|
+
);
|
|
239
|
+
const selectedVariant = useMemo2(() => {
|
|
240
|
+
if (selectedValues.length !== optionNames.length) return null;
|
|
241
|
+
const match = findVariantByOptions(pickerVariants, selectedValues);
|
|
242
|
+
if (!match) return null;
|
|
243
|
+
return variants.find((v) => v.id === match.id) ?? null;
|
|
244
|
+
}, [pickerVariants, selectedValues, variants, optionNames.length]);
|
|
245
|
+
const setOptionValue = useCallback3(
|
|
246
|
+
(optionIndex, value) => {
|
|
247
|
+
setSelectedValues((prev) => {
|
|
248
|
+
const next = prev.slice();
|
|
249
|
+
next[optionIndex] = value;
|
|
250
|
+
return next;
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
[]
|
|
254
|
+
);
|
|
255
|
+
const optionsByPosition = useMemo2(() => {
|
|
256
|
+
if (optionValuesByPosition) {
|
|
257
|
+
return optionValuesByPosition.map((values) => values.slice());
|
|
258
|
+
}
|
|
259
|
+
const out = optionNames.map(() => []);
|
|
260
|
+
for (const v of variants) {
|
|
261
|
+
v.selectedOptions.forEach((o, i) => {
|
|
262
|
+
if (i < out.length && !out[i].includes(o.value)) out[i].push(o.value);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}, [variants, optionNames, optionValuesByPosition]);
|
|
267
|
+
const optionsFor = useCallback3(
|
|
268
|
+
(optionIndex) => {
|
|
269
|
+
const values = optionsByPosition[optionIndex] ?? [];
|
|
270
|
+
return values.map((value) => ({
|
|
271
|
+
value,
|
|
272
|
+
disabled: !isOptionValueAvailable(
|
|
273
|
+
pickerVariants,
|
|
274
|
+
optionIndex,
|
|
275
|
+
value,
|
|
276
|
+
selectedValues
|
|
277
|
+
)
|
|
278
|
+
}));
|
|
279
|
+
},
|
|
280
|
+
[optionsByPosition, pickerVariants, selectedValues]
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
selectedValues,
|
|
284
|
+
selectedVariant,
|
|
285
|
+
setOptionValue,
|
|
286
|
+
optionsFor
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/components/VariantDropdown.tsx
|
|
291
|
+
import {
|
|
292
|
+
useEffect as useEffect5,
|
|
293
|
+
useId,
|
|
294
|
+
useLayoutEffect,
|
|
295
|
+
useMemo as useMemo3,
|
|
296
|
+
useRef as useRef3,
|
|
297
|
+
useState as useState4
|
|
298
|
+
} from "react";
|
|
299
|
+
import { dropdown } from "@lime-bundles/core";
|
|
212
300
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
301
|
+
var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = dropdown;
|
|
302
|
+
var ITEM_HEIGHT_PX = 32;
|
|
303
|
+
var LIST_PAD_Y = 8;
|
|
304
|
+
var MAX_VISIBLE_ITEMS = 8;
|
|
305
|
+
function VariantDropdown({
|
|
306
|
+
options,
|
|
307
|
+
value,
|
|
308
|
+
onChange,
|
|
309
|
+
ariaLabel,
|
|
310
|
+
className
|
|
311
|
+
}) {
|
|
312
|
+
const idBase = useId();
|
|
313
|
+
const triggerRef = useRef3(null);
|
|
314
|
+
const listboxRef = useRef3(null);
|
|
315
|
+
const typeAheadRef = useRef3(emptyTypeAheadState());
|
|
316
|
+
const [isOpen, setIsOpen] = useState4(false);
|
|
317
|
+
const [activeIndex, setActiveIndex] = useState4(-1);
|
|
318
|
+
const [position, setPosition] = useState4(null);
|
|
319
|
+
const selectedIndex = useMemo3(
|
|
320
|
+
() => options.findIndex((o) => o.value === value),
|
|
321
|
+
[options, value]
|
|
322
|
+
);
|
|
323
|
+
const optionsState = useMemo3(
|
|
324
|
+
() => options.map((o) => ({ disabled: !!o.disabled, label: o.label })),
|
|
325
|
+
[options]
|
|
326
|
+
);
|
|
327
|
+
const selectedLabel = selectedIndex >= 0 ? options[selectedIndex].label : "";
|
|
328
|
+
function open() {
|
|
329
|
+
if (isOpen) return;
|
|
330
|
+
setIsOpen(true);
|
|
331
|
+
setActiveIndex(
|
|
332
|
+
selectedIndex >= 0 && !optionsState[selectedIndex]?.disabled ? selectedIndex : optionsState.findIndex((o) => !o.disabled)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
function close(restoreFocus) {
|
|
336
|
+
setIsOpen(false);
|
|
337
|
+
setActiveIndex(-1);
|
|
338
|
+
if (restoreFocus) triggerRef.current?.focus();
|
|
339
|
+
}
|
|
340
|
+
function commit(index) {
|
|
341
|
+
const opt = options[index];
|
|
342
|
+
if (!opt || opt.disabled) return;
|
|
343
|
+
if (opt.value !== value) onChange(opt.value);
|
|
344
|
+
close(true);
|
|
345
|
+
}
|
|
346
|
+
useLayoutEffect(() => {
|
|
347
|
+
if (!isOpen) return;
|
|
348
|
+
const trigger = triggerRef.current;
|
|
349
|
+
if (!trigger) return;
|
|
350
|
+
const rect = trigger.getBoundingClientRect();
|
|
351
|
+
if (rect.width === 0) return;
|
|
352
|
+
const visibleCount = Math.min(options.length || 1, MAX_VISIBLE_ITEMS);
|
|
353
|
+
const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
|
|
354
|
+
setPosition(
|
|
355
|
+
computePosition({
|
|
356
|
+
trigger: {
|
|
357
|
+
top: rect.top,
|
|
358
|
+
bottom: rect.bottom,
|
|
359
|
+
left: rect.left,
|
|
360
|
+
width: rect.width
|
|
361
|
+
},
|
|
362
|
+
viewportHeight: window.innerHeight,
|
|
363
|
+
desiredHeight
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
}, [isOpen, options.length]);
|
|
367
|
+
useEffect5(() => {
|
|
368
|
+
if (!isOpen) return;
|
|
369
|
+
const isInsideDropdown = (event) => {
|
|
370
|
+
const path = event.composedPath();
|
|
371
|
+
const trigger = triggerRef.current;
|
|
372
|
+
const listbox = listboxRef.current;
|
|
373
|
+
return trigger != null && path.includes(trigger) || listbox != null && path.includes(listbox);
|
|
374
|
+
};
|
|
375
|
+
const onPointerdown = (event) => {
|
|
376
|
+
if (isInsideDropdown(event)) return;
|
|
377
|
+
close(false);
|
|
378
|
+
};
|
|
379
|
+
const onScroll = (event) => {
|
|
380
|
+
if (isInsideDropdown(event)) return;
|
|
381
|
+
close(false);
|
|
382
|
+
};
|
|
383
|
+
const onResize = () => close(false);
|
|
384
|
+
document.addEventListener("pointerdown", onPointerdown, true);
|
|
385
|
+
window.addEventListener("scroll", onScroll, true);
|
|
386
|
+
window.addEventListener("resize", onResize);
|
|
387
|
+
return () => {
|
|
388
|
+
document.removeEventListener("pointerdown", onPointerdown, true);
|
|
389
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
390
|
+
window.removeEventListener("resize", onResize);
|
|
391
|
+
};
|
|
392
|
+
}, [isOpen]);
|
|
393
|
+
useEffect5(() => {
|
|
394
|
+
if (!isOpen || activeIndex < 0) return;
|
|
395
|
+
const listbox = listboxRef.current;
|
|
396
|
+
if (!listbox) return;
|
|
397
|
+
const li = listbox.querySelector(
|
|
398
|
+
`#${CSS.escape(`${idBase}-opt-${activeIndex}`)}`
|
|
399
|
+
);
|
|
400
|
+
if (!li) return;
|
|
401
|
+
const liTop = li.offsetTop;
|
|
402
|
+
const liBottom = liTop + li.offsetHeight;
|
|
403
|
+
const visTop = listbox.scrollTop;
|
|
404
|
+
const visBottom = visTop + listbox.clientHeight;
|
|
405
|
+
if (liTop < visTop) {
|
|
406
|
+
listbox.scrollTop = liTop;
|
|
407
|
+
} else if (liBottom > visBottom) {
|
|
408
|
+
listbox.scrollTop = liBottom - listbox.clientHeight;
|
|
409
|
+
}
|
|
410
|
+
}, [activeIndex, isOpen, idBase]);
|
|
411
|
+
function onKeyDown(event) {
|
|
412
|
+
const action = handleKey(
|
|
413
|
+
{
|
|
414
|
+
key: event.key,
|
|
415
|
+
ctrlKey: event.ctrlKey,
|
|
416
|
+
metaKey: event.metaKey,
|
|
417
|
+
altKey: event.altKey,
|
|
418
|
+
shiftKey: event.shiftKey
|
|
419
|
+
},
|
|
420
|
+
{ isOpen, activeIndex, selectedIndex, options: optionsState }
|
|
421
|
+
);
|
|
422
|
+
if (action.preventDefault) event.preventDefault();
|
|
423
|
+
switch (action.type) {
|
|
424
|
+
case "open":
|
|
425
|
+
open();
|
|
426
|
+
if (action.activeIndex >= 0) setActiveIndex(action.activeIndex);
|
|
427
|
+
return;
|
|
428
|
+
case "close":
|
|
429
|
+
close(action.restoreFocus);
|
|
430
|
+
return;
|
|
431
|
+
case "move-active":
|
|
432
|
+
setActiveIndex(action.activeIndex);
|
|
433
|
+
return;
|
|
434
|
+
case "commit":
|
|
435
|
+
commit(action.index);
|
|
436
|
+
return;
|
|
437
|
+
case "type-ahead": {
|
|
438
|
+
const result = pushTypeAheadChar(
|
|
439
|
+
typeAheadRef.current,
|
|
440
|
+
action.char,
|
|
441
|
+
Date.now(),
|
|
442
|
+
optionsState
|
|
443
|
+
);
|
|
444
|
+
typeAheadRef.current = result.newState;
|
|
445
|
+
if (result.matchedIndex !== null) {
|
|
446
|
+
if (!isOpen) open();
|
|
447
|
+
setActiveIndex(result.matchedIndex);
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
case "passthrough":
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function onShellBlur(event) {
|
|
456
|
+
const next = event.relatedTarget;
|
|
457
|
+
if (next && event.currentTarget.contains(next)) return;
|
|
458
|
+
setTimeout(() => {
|
|
459
|
+
if (!isOpen) return;
|
|
460
|
+
const active = document.activeElement;
|
|
461
|
+
if (!triggerRef.current?.contains(active) && !listboxRef.current?.contains(active)) {
|
|
462
|
+
close(false);
|
|
463
|
+
}
|
|
464
|
+
}, 0);
|
|
465
|
+
}
|
|
466
|
+
const listboxId = `${idBase}-listbox`;
|
|
467
|
+
const activeDescendantId = activeIndex >= 0 ? `${idBase}-opt-${activeIndex}` : void 0;
|
|
468
|
+
const panelStyle = position ? { maxHeight: position.maxHeight } : void 0;
|
|
469
|
+
return /* @__PURE__ */ jsxs(
|
|
470
|
+
"div",
|
|
471
|
+
{
|
|
472
|
+
className: `lb-dropdown${className ? ` ${className}` : ""}`,
|
|
473
|
+
"data-lb-dropdown": "",
|
|
474
|
+
onBlur: onShellBlur,
|
|
475
|
+
children: [
|
|
476
|
+
/* @__PURE__ */ jsxs(
|
|
477
|
+
"button",
|
|
478
|
+
{
|
|
479
|
+
ref: triggerRef,
|
|
480
|
+
type: "button",
|
|
481
|
+
className: "lb-dropdown-trigger",
|
|
482
|
+
role: "combobox",
|
|
483
|
+
"aria-haspopup": "listbox",
|
|
484
|
+
"aria-expanded": isOpen,
|
|
485
|
+
"aria-controls": listboxId,
|
|
486
|
+
"aria-label": ariaLabel,
|
|
487
|
+
"aria-activedescendant": activeDescendantId,
|
|
488
|
+
onClick: (e) => {
|
|
489
|
+
e.preventDefault();
|
|
490
|
+
if (isOpen) close(false);
|
|
491
|
+
else open();
|
|
492
|
+
},
|
|
493
|
+
onKeyDown,
|
|
494
|
+
children: [
|
|
495
|
+
/* @__PURE__ */ jsx("span", { className: "lb-dropdown-trigger-value", children: selectedLabel }),
|
|
496
|
+
/* @__PURE__ */ jsx("span", { className: "lb-dropdown-chevron", "aria-hidden": "true" })
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
),
|
|
500
|
+
/* @__PURE__ */ jsx(
|
|
501
|
+
"ul",
|
|
502
|
+
{
|
|
503
|
+
ref: listboxRef,
|
|
504
|
+
id: listboxId,
|
|
505
|
+
className: "lb-dropdown-listbox",
|
|
506
|
+
role: "listbox",
|
|
507
|
+
"aria-label": ariaLabel,
|
|
508
|
+
hidden: !isOpen,
|
|
509
|
+
"data-placement": position?.placement,
|
|
510
|
+
style: panelStyle,
|
|
511
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
512
|
+
children: options.map((opt, i) => (
|
|
513
|
+
// Keyboard activation lives on the trigger combobox via
|
|
514
|
+
// aria-activedescendant — the WAI-ARIA combobox pattern keeps
|
|
515
|
+
// focus on the button and tracks the active option by id, so
|
|
516
|
+
// the option <li> elements deliberately have no key handlers.
|
|
517
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
|
|
518
|
+
/* @__PURE__ */ jsx(
|
|
519
|
+
"li",
|
|
520
|
+
{
|
|
521
|
+
id: `${idBase}-opt-${i}`,
|
|
522
|
+
className: `lb-dropdown-option${i === activeIndex ? " is-active" : ""}`,
|
|
523
|
+
role: "option",
|
|
524
|
+
"aria-selected": i === selectedIndex,
|
|
525
|
+
"aria-disabled": opt.disabled || void 0,
|
|
526
|
+
"data-value": opt.value,
|
|
527
|
+
onClick: () => commit(i),
|
|
528
|
+
onMouseMove: () => {
|
|
529
|
+
if (!opt.disabled && i !== activeIndex) setActiveIndex(i);
|
|
530
|
+
},
|
|
531
|
+
children: opt.label
|
|
532
|
+
},
|
|
533
|
+
opt.value
|
|
534
|
+
)
|
|
535
|
+
))
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/components/FixedBundle.tsx
|
|
544
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
213
545
|
function FixedBundle(props) {
|
|
214
546
|
const {
|
|
215
547
|
shopDomain,
|
|
@@ -233,11 +565,21 @@ function FixedBundle(props) {
|
|
|
233
565
|
bundleType: "fixed",
|
|
234
566
|
enabled: analyticsEnabled !== false
|
|
235
567
|
});
|
|
236
|
-
const [addingToCart, setAddingToCart] =
|
|
237
|
-
const [cartError, setCartError] =
|
|
568
|
+
const [addingToCart, setAddingToCart] = useState5(false);
|
|
569
|
+
const [cartError, setCartError] = useState5(null);
|
|
570
|
+
const [selectedVariants, setSelectedVariants] = useState5({});
|
|
571
|
+
const handleVariantChange = useCallback4(
|
|
572
|
+
(productId, variant) => {
|
|
573
|
+
setSelectedVariants((prev) => {
|
|
574
|
+
if (prev[productId] === variant) return prev;
|
|
575
|
+
return { ...prev, [productId]: variant };
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
[]
|
|
579
|
+
);
|
|
238
580
|
const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
|
|
239
581
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
240
|
-
|
|
582
|
+
useEffect6(() => {
|
|
241
583
|
if (result.status === "error") {
|
|
242
584
|
onError?.(result.error);
|
|
243
585
|
return;
|
|
@@ -250,10 +592,10 @@ function FixedBundle(props) {
|
|
|
250
592
|
);
|
|
251
593
|
}
|
|
252
594
|
}, [result, onError]);
|
|
253
|
-
const handleAddToCart =
|
|
595
|
+
const handleAddToCart = useCallback4(async () => {
|
|
254
596
|
if (!bundle) return;
|
|
255
597
|
const bundleLines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
|
|
256
|
-
const variant = p.variants.nodes.find((v) => v.availableForSale);
|
|
598
|
+
const variant = selectedVariants[p.id] ?? p.variants.nodes.find((v) => v.availableForSale);
|
|
257
599
|
return {
|
|
258
600
|
productId: p.id,
|
|
259
601
|
variant,
|
|
@@ -297,17 +639,17 @@ function FixedBundle(props) {
|
|
|
297
639
|
} finally {
|
|
298
640
|
setAddingToCart(false);
|
|
299
641
|
}
|
|
300
|
-
}, [bundle, onAddToCart, onError, trackAddToCart]);
|
|
642
|
+
}, [bundle, onAddToCart, onError, trackAddToCart, selectedVariants]);
|
|
301
643
|
if (result.status === "loading") {
|
|
302
|
-
return /* @__PURE__ */
|
|
303
|
-
/* @__PURE__ */
|
|
304
|
-
/* @__PURE__ */
|
|
644
|
+
return /* @__PURE__ */ jsxs2("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
645
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
646
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
305
647
|
] });
|
|
306
648
|
}
|
|
307
649
|
if (result.status === "error") return null;
|
|
308
650
|
if (!bundle) return null;
|
|
309
651
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
310
|
-
return /* @__PURE__ */
|
|
652
|
+
return /* @__PURE__ */ jsxs2(
|
|
311
653
|
"div",
|
|
312
654
|
{
|
|
313
655
|
ref: (el) => {
|
|
@@ -318,38 +660,19 @@ function FixedBundle(props) {
|
|
|
318
660
|
role: "region",
|
|
319
661
|
"aria-label": bundle.title,
|
|
320
662
|
children: [
|
|
321
|
-
/* @__PURE__ */
|
|
322
|
-
bundle.discountLabel && /* @__PURE__ */
|
|
323
|
-
/* @__PURE__ */
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
product
|
|
327
|
-
currency
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return /* @__PURE__ */ jsxs("div", { className: "lb-bundle__product", part: "product", children: [
|
|
335
|
-
product.featuredImage && /* @__PURE__ */ jsx(
|
|
336
|
-
"img",
|
|
337
|
-
{
|
|
338
|
-
src: product.featuredImage.url,
|
|
339
|
-
alt: product.featuredImage.altText ?? product.title,
|
|
340
|
-
className: "lb-bundle__product-image",
|
|
341
|
-
loading: "lazy"
|
|
342
|
-
}
|
|
343
|
-
),
|
|
344
|
-
/* @__PURE__ */ jsxs("div", { className: "lb-bundle__product-info", children: [
|
|
345
|
-
/* @__PURE__ */ jsx("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
346
|
-
/* @__PURE__ */ jsx("p", { className: "lb-bundle__product-price", children: priceText }),
|
|
347
|
-
unitPriceText && /* @__PURE__ */ jsx("p", { className: "lb-bundle__product-unit-price", children: unitPriceText })
|
|
348
|
-
] })
|
|
349
|
-
] }, product.id);
|
|
350
|
-
}) }),
|
|
351
|
-
cartError && /* @__PURE__ */ jsx("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
352
|
-
/* @__PURE__ */ jsx(
|
|
663
|
+
/* @__PURE__ */ jsx2("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
664
|
+
bundle.discountLabel && /* @__PURE__ */ jsx2("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
|
|
665
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ jsx2(
|
|
666
|
+
FixedProductRow,
|
|
667
|
+
{
|
|
668
|
+
product,
|
|
669
|
+
currency,
|
|
670
|
+
onVariantChange: handleVariantChange
|
|
671
|
+
},
|
|
672
|
+
product.id
|
|
673
|
+
)) }),
|
|
674
|
+
cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
675
|
+
/* @__PURE__ */ jsx2(
|
|
353
676
|
"button",
|
|
354
677
|
{
|
|
355
678
|
className: "lb-bundle__cta",
|
|
@@ -363,16 +686,72 @@ function FixedBundle(props) {
|
|
|
363
686
|
}
|
|
364
687
|
);
|
|
365
688
|
}
|
|
689
|
+
function FixedProductRow({ product, currency, onVariantChange }) {
|
|
690
|
+
const variants = product.variants.nodes;
|
|
691
|
+
const optionNames = useMemo4(() => {
|
|
692
|
+
const first = variants[0];
|
|
693
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
694
|
+
}, [variants]);
|
|
695
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
696
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
|
|
697
|
+
variants,
|
|
698
|
+
optionNames
|
|
699
|
+
});
|
|
700
|
+
useEffect6(() => {
|
|
701
|
+
onVariantChange(product.id, selectedVariant ?? null);
|
|
702
|
+
}, [selectedVariant, product.id, onVariantChange]);
|
|
703
|
+
const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0];
|
|
704
|
+
const priceText = displayVariant ? formatMoney(displayVariant.price.amount, currency) : formatMoney(product.priceRange.minVariantPrice.amount, currency);
|
|
705
|
+
const unitPriceText = displayVariant ? formatUnitPrice(
|
|
706
|
+
displayVariant.unitPrice,
|
|
707
|
+
displayVariant.unitPriceMeasurement,
|
|
708
|
+
currency
|
|
709
|
+
) : null;
|
|
710
|
+
const thumbImage = displayVariant?.image ?? product.featuredImage;
|
|
711
|
+
return /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product", part: "product", children: [
|
|
712
|
+
thumbImage && /* @__PURE__ */ jsx2(
|
|
713
|
+
"img",
|
|
714
|
+
{
|
|
715
|
+
src: thumbImage.url,
|
|
716
|
+
alt: thumbImage.altText ?? product.title,
|
|
717
|
+
className: "lb-bundle__product-image",
|
|
718
|
+
loading: "lazy"
|
|
719
|
+
}
|
|
720
|
+
),
|
|
721
|
+
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
|
|
722
|
+
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
723
|
+
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: priceText }),
|
|
724
|
+
unitPriceText && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
725
|
+
showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
726
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
727
|
+
value: o.value,
|
|
728
|
+
label: o.value,
|
|
729
|
+
disabled: o.disabled
|
|
730
|
+
}));
|
|
731
|
+
return /* @__PURE__ */ jsx2(
|
|
732
|
+
VariantDropdown,
|
|
733
|
+
{
|
|
734
|
+
options: dropdownOptions,
|
|
735
|
+
value: selectedValues[optionIndex] ?? null,
|
|
736
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
737
|
+
ariaLabel: optionName
|
|
738
|
+
},
|
|
739
|
+
optionName
|
|
740
|
+
);
|
|
741
|
+
}) })
|
|
742
|
+
] })
|
|
743
|
+
] });
|
|
744
|
+
}
|
|
366
745
|
|
|
367
746
|
// src/components/MixMatchBundle.tsx
|
|
368
|
-
import { useCallback as
|
|
747
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo5, useState as useState6 } from "react";
|
|
369
748
|
import {
|
|
370
749
|
formatMoney as formatMoney2,
|
|
371
750
|
formatUnitPrice as formatUnitPrice2,
|
|
372
751
|
validateQuantity,
|
|
373
752
|
resolveBundleQty as resolveBundleQty2
|
|
374
753
|
} from "@lime-bundles/core";
|
|
375
|
-
import { jsx as
|
|
754
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
376
755
|
function MixMatchBundle(props) {
|
|
377
756
|
const {
|
|
378
757
|
shopDomain,
|
|
@@ -396,14 +775,14 @@ function MixMatchBundle(props) {
|
|
|
396
775
|
bundleType: "mix_match",
|
|
397
776
|
enabled: analyticsEnabled !== false
|
|
398
777
|
});
|
|
399
|
-
const [selections, setSelections] =
|
|
778
|
+
const [selections, setSelections] = useState6(
|
|
400
779
|
/* @__PURE__ */ new Map()
|
|
401
780
|
);
|
|
402
|
-
const [addingToCart, setAddingToCart] =
|
|
403
|
-
const [cartError, setCartError] =
|
|
781
|
+
const [addingToCart, setAddingToCart] = useState6(false);
|
|
782
|
+
const [cartError, setCartError] = useState6(null);
|
|
404
783
|
const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
|
|
405
784
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
406
|
-
|
|
785
|
+
useEffect7(() => {
|
|
407
786
|
if (result.status === "error") {
|
|
408
787
|
onError?.(result.error);
|
|
409
788
|
return;
|
|
@@ -421,7 +800,7 @@ function MixMatchBundle(props) {
|
|
|
421
800
|
0
|
|
422
801
|
);
|
|
423
802
|
const validation = bundle ? validateQuantity(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
|
|
424
|
-
const toggleProduct =
|
|
803
|
+
const toggleProduct = useCallback5(
|
|
425
804
|
(productId, variant) => {
|
|
426
805
|
setSelections((prev) => {
|
|
427
806
|
const next = new Map(prev);
|
|
@@ -441,7 +820,7 @@ function MixMatchBundle(props) {
|
|
|
441
820
|
},
|
|
442
821
|
[bundle]
|
|
443
822
|
);
|
|
444
|
-
const updateQuantity =
|
|
823
|
+
const updateQuantity = useCallback5(
|
|
445
824
|
(productId, variantId, quantity) => {
|
|
446
825
|
setSelections((prev) => {
|
|
447
826
|
const next = new Map(prev);
|
|
@@ -456,7 +835,7 @@ function MixMatchBundle(props) {
|
|
|
456
835
|
},
|
|
457
836
|
[]
|
|
458
837
|
);
|
|
459
|
-
const handleAddToCart =
|
|
838
|
+
const handleAddToCart = useCallback5(async () => {
|
|
460
839
|
if (!bundle || !validation.valid) return;
|
|
461
840
|
const lines = Array.from(selections.values()).map((s) => ({
|
|
462
841
|
merchandiseId: s.variantId,
|
|
@@ -501,15 +880,15 @@ function MixMatchBundle(props) {
|
|
|
501
880
|
totalQuantity
|
|
502
881
|
]);
|
|
503
882
|
if (result.status === "loading") {
|
|
504
|
-
return /* @__PURE__ */
|
|
505
|
-
/* @__PURE__ */
|
|
506
|
-
/* @__PURE__ */
|
|
883
|
+
return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
884
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
885
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
507
886
|
] });
|
|
508
887
|
}
|
|
509
888
|
if (result.status === "error") return null;
|
|
510
889
|
if (!bundle) return null;
|
|
511
890
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
512
|
-
return /* @__PURE__ */
|
|
891
|
+
return /* @__PURE__ */ jsxs3(
|
|
513
892
|
"div",
|
|
514
893
|
{
|
|
515
894
|
ref: (el) => {
|
|
@@ -520,83 +899,23 @@ function MixMatchBundle(props) {
|
|
|
520
899
|
role: "region",
|
|
521
900
|
"aria-label": bundle.title,
|
|
522
901
|
children: [
|
|
523
|
-
/* @__PURE__ */
|
|
524
|
-
bundle.discountLabel && /* @__PURE__ */
|
|
525
|
-
/* @__PURE__ */
|
|
526
|
-
/* @__PURE__ */
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
alt: product.featuredImage.altText ?? product.title,
|
|
541
|
-
className: "lb-bundle__product-image",
|
|
542
|
-
loading: "lazy"
|
|
543
|
-
}
|
|
544
|
-
),
|
|
545
|
-
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
|
|
546
|
-
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
547
|
-
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: formatMoney2(defaultVariant.price.amount, currency) }),
|
|
548
|
-
(() => {
|
|
549
|
-
const unitPriceText = formatUnitPrice2(
|
|
550
|
-
defaultVariant.unitPrice,
|
|
551
|
-
defaultVariant.unitPriceMeasurement,
|
|
552
|
-
currency
|
|
553
|
-
);
|
|
554
|
-
return unitPriceText ? /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }) : null;
|
|
555
|
-
})()
|
|
556
|
-
] }),
|
|
557
|
-
/* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__quantity-control", children: [
|
|
558
|
-
/* @__PURE__ */ jsx2(
|
|
559
|
-
"button",
|
|
560
|
-
{
|
|
561
|
-
"aria-label": `Decrease ${product.title}`,
|
|
562
|
-
onClick: () => updateQuantity(
|
|
563
|
-
product.id,
|
|
564
|
-
defaultVariant.id,
|
|
565
|
-
selected.quantity - 1
|
|
566
|
-
),
|
|
567
|
-
children: "\u2212"
|
|
568
|
-
}
|
|
569
|
-
),
|
|
570
|
-
/* @__PURE__ */ jsx2("span", { children: selected.quantity }),
|
|
571
|
-
/* @__PURE__ */ jsx2(
|
|
572
|
-
"button",
|
|
573
|
-
{
|
|
574
|
-
"aria-label": `Increase ${product.title}`,
|
|
575
|
-
onClick: () => updateQuantity(
|
|
576
|
-
product.id,
|
|
577
|
-
defaultVariant.id,
|
|
578
|
-
selected.quantity + 1
|
|
579
|
-
),
|
|
580
|
-
children: "+"
|
|
581
|
-
}
|
|
582
|
-
)
|
|
583
|
-
] }) : /* @__PURE__ */ jsx2(
|
|
584
|
-
"button",
|
|
585
|
-
{
|
|
586
|
-
className: "lb-bundle__select-btn",
|
|
587
|
-
onClick: () => toggleProduct(product.id, defaultVariant),
|
|
588
|
-
disabled: !defaultVariant.availableForSale,
|
|
589
|
-
children: defaultVariant.availableForSale ? "Select" : "Sold out"
|
|
590
|
-
}
|
|
591
|
-
) })
|
|
592
|
-
]
|
|
593
|
-
},
|
|
594
|
-
product.id
|
|
595
|
-
);
|
|
596
|
-
}) }),
|
|
597
|
-
validation.message && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
|
|
598
|
-
cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
599
|
-
/* @__PURE__ */ jsx2(
|
|
902
|
+
/* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
903
|
+
bundle.discountLabel && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
|
|
904
|
+
/* @__PURE__ */ jsx3("p", { className: "lb-bundle__instructions", children: bundle.minQuantity && bundle.maxQuantity ? `Select ${bundle.minQuantity}\u2013${bundle.maxQuantity} items` : bundle.minQuantity ? `Select at least ${bundle.minQuantity} items` : bundle.maxQuantity ? `Select up to ${bundle.maxQuantity} items` : "Select your items" }),
|
|
905
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ jsx3(
|
|
906
|
+
MixMatchProductRow,
|
|
907
|
+
{
|
|
908
|
+
product,
|
|
909
|
+
currency,
|
|
910
|
+
selections,
|
|
911
|
+
onToggle: toggleProduct,
|
|
912
|
+
onUpdateQuantity: updateQuantity
|
|
913
|
+
},
|
|
914
|
+
product.id
|
|
915
|
+
)) }),
|
|
916
|
+
validation.message && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
|
|
917
|
+
cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
918
|
+
/* @__PURE__ */ jsx3(
|
|
600
919
|
"button",
|
|
601
920
|
{
|
|
602
921
|
className: "lb-bundle__cta",
|
|
@@ -610,16 +929,115 @@ function MixMatchBundle(props) {
|
|
|
610
929
|
}
|
|
611
930
|
);
|
|
612
931
|
}
|
|
932
|
+
function MixMatchProductRow({
|
|
933
|
+
product,
|
|
934
|
+
currency,
|
|
935
|
+
selections,
|
|
936
|
+
onToggle,
|
|
937
|
+
onUpdateQuantity
|
|
938
|
+
}) {
|
|
939
|
+
const variants = product.variants.nodes;
|
|
940
|
+
const optionNames = useMemo5(() => {
|
|
941
|
+
const first = variants[0];
|
|
942
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
943
|
+
}, [variants]);
|
|
944
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
945
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
946
|
+
const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
|
|
947
|
+
if (!displayVariant) return null;
|
|
948
|
+
const key = `${product.id}:${displayVariant.id}`;
|
|
949
|
+
const selected = selections.get(key);
|
|
950
|
+
const thumbImage = displayVariant.image ?? product.featuredImage;
|
|
951
|
+
const unitPriceText = formatUnitPrice2(
|
|
952
|
+
displayVariant.unitPrice,
|
|
953
|
+
displayVariant.unitPriceMeasurement,
|
|
954
|
+
currency
|
|
955
|
+
);
|
|
956
|
+
return /* @__PURE__ */ jsxs3(
|
|
957
|
+
"div",
|
|
958
|
+
{
|
|
959
|
+
className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
|
|
960
|
+
children: [
|
|
961
|
+
thumbImage && /* @__PURE__ */ jsx3(
|
|
962
|
+
"img",
|
|
963
|
+
{
|
|
964
|
+
src: thumbImage.url,
|
|
965
|
+
alt: thumbImage.altText ?? product.title,
|
|
966
|
+
className: "lb-bundle__product-image",
|
|
967
|
+
loading: "lazy"
|
|
968
|
+
}
|
|
969
|
+
),
|
|
970
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product-info", children: [
|
|
971
|
+
/* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
972
|
+
/* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-price", children: formatMoney2(displayVariant.price.amount, currency) }),
|
|
973
|
+
unitPriceText && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
974
|
+
showPicker && !selected && /* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
975
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
976
|
+
value: o.value,
|
|
977
|
+
label: o.value,
|
|
978
|
+
disabled: o.disabled
|
|
979
|
+
}));
|
|
980
|
+
return /* @__PURE__ */ jsx3(
|
|
981
|
+
VariantDropdown,
|
|
982
|
+
{
|
|
983
|
+
options: dropdownOptions,
|
|
984
|
+
value: selectedValues[optionIndex] ?? null,
|
|
985
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
986
|
+
ariaLabel: optionName
|
|
987
|
+
},
|
|
988
|
+
optionName
|
|
989
|
+
);
|
|
990
|
+
}) })
|
|
991
|
+
] }),
|
|
992
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
|
|
993
|
+
/* @__PURE__ */ jsx3(
|
|
994
|
+
"button",
|
|
995
|
+
{
|
|
996
|
+
"aria-label": `Decrease ${product.title}`,
|
|
997
|
+
onClick: () => onUpdateQuantity(
|
|
998
|
+
product.id,
|
|
999
|
+
displayVariant.id,
|
|
1000
|
+
selected.quantity - 1
|
|
1001
|
+
),
|
|
1002
|
+
children: "\u2212"
|
|
1003
|
+
}
|
|
1004
|
+
),
|
|
1005
|
+
/* @__PURE__ */ jsx3("span", { children: selected.quantity }),
|
|
1006
|
+
/* @__PURE__ */ jsx3(
|
|
1007
|
+
"button",
|
|
1008
|
+
{
|
|
1009
|
+
"aria-label": `Increase ${product.title}`,
|
|
1010
|
+
onClick: () => onUpdateQuantity(
|
|
1011
|
+
product.id,
|
|
1012
|
+
displayVariant.id,
|
|
1013
|
+
selected.quantity + 1
|
|
1014
|
+
),
|
|
1015
|
+
children: "+"
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
] }) : /* @__PURE__ */ jsx3(
|
|
1019
|
+
"button",
|
|
1020
|
+
{
|
|
1021
|
+
className: "lb-bundle__select-btn",
|
|
1022
|
+
onClick: () => onToggle(product.id, displayVariant),
|
|
1023
|
+
disabled: !displayVariant.availableForSale,
|
|
1024
|
+
children: displayVariant.availableForSale ? "Select" : "Sold out"
|
|
1025
|
+
}
|
|
1026
|
+
) })
|
|
1027
|
+
]
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
613
1031
|
|
|
614
1032
|
// src/components/VolumeBundle.tsx
|
|
615
|
-
import { useCallback as
|
|
1033
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo6, useState as useState7 } from "react";
|
|
616
1034
|
import {
|
|
617
1035
|
formatMoney as formatMoney3,
|
|
618
1036
|
formatUnitPrice as formatUnitPrice3,
|
|
619
1037
|
calculateTierSavings,
|
|
620
1038
|
getActiveTier
|
|
621
1039
|
} from "@lime-bundles/core";
|
|
622
|
-
import { jsx as
|
|
1040
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
623
1041
|
function VolumeBundle(props) {
|
|
624
1042
|
const {
|
|
625
1043
|
shopDomain,
|
|
@@ -643,12 +1061,12 @@ function VolumeBundle(props) {
|
|
|
643
1061
|
bundleType: "volume",
|
|
644
1062
|
enabled: analyticsEnabled !== false
|
|
645
1063
|
});
|
|
646
|
-
const [quantity, setQuantity] =
|
|
647
|
-
const [addingToCart, setAddingToCart] =
|
|
648
|
-
const [cartError, setCartError] =
|
|
1064
|
+
const [quantity, setQuantity] = useState7(1);
|
|
1065
|
+
const [addingToCart, setAddingToCart] = useState7(false);
|
|
1066
|
+
const [cartError, setCartError] = useState7(null);
|
|
649
1067
|
const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
|
|
650
1068
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
651
|
-
|
|
1069
|
+
useEffect8(() => {
|
|
652
1070
|
if (result.status === "error") {
|
|
653
1071
|
onError?.(result.error);
|
|
654
1072
|
return;
|
|
@@ -662,18 +1080,35 @@ function VolumeBundle(props) {
|
|
|
662
1080
|
}
|
|
663
1081
|
}, [result, onError]);
|
|
664
1082
|
const product = bundle?.products[0];
|
|
665
|
-
const
|
|
1083
|
+
const variants = useMemo6(() => product?.variants.nodes ?? [], [product]);
|
|
1084
|
+
const optionNames = useMemo6(() => {
|
|
1085
|
+
const first = variants[0];
|
|
1086
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
1087
|
+
}, [variants]);
|
|
1088
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
1089
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
1090
|
+
const displayVariant = selectedVariant ?? variants.find((v) => v.availableForSale) ?? variants[0];
|
|
1091
|
+
const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
|
|
666
1092
|
const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
) :
|
|
1093
|
+
const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
|
|
1094
|
+
const unitPriceText = displayVariant ? formatUnitPrice3(
|
|
1095
|
+
displayVariant.unitPrice,
|
|
1096
|
+
displayVariant.unitPriceMeasurement,
|
|
1097
|
+
currency
|
|
1098
|
+
) : null;
|
|
1099
|
+
const tierSavings = useMemo6(
|
|
1100
|
+
() => bundle ? calculateTierSavings(
|
|
1101
|
+
bundle.volumeTiers,
|
|
1102
|
+
basePrice,
|
|
1103
|
+
quantity,
|
|
1104
|
+
bundle.discountConfig.discountType
|
|
1105
|
+
) : [],
|
|
1106
|
+
[bundle, basePrice, quantity]
|
|
1107
|
+
);
|
|
673
1108
|
const activeTier = bundle ? getActiveTier(bundle.volumeTiers, quantity) : null;
|
|
674
|
-
const handleAddToCart =
|
|
1109
|
+
const handleAddToCart = useCallback6(async () => {
|
|
675
1110
|
if (!bundle || !product) return;
|
|
676
|
-
const variant = product.variants.nodes.find((v) => v.availableForSale);
|
|
1111
|
+
const variant = selectedVariant ?? product.variants.nodes.find((v) => v.availableForSale);
|
|
677
1112
|
if (!variant) return;
|
|
678
1113
|
const lines = [
|
|
679
1114
|
{
|
|
@@ -710,18 +1145,20 @@ function VolumeBundle(props) {
|
|
|
710
1145
|
basePrice,
|
|
711
1146
|
onAddToCart,
|
|
712
1147
|
onError,
|
|
713
|
-
trackAddToCart
|
|
1148
|
+
trackAddToCart,
|
|
1149
|
+
selectedVariant,
|
|
1150
|
+
tierSavings
|
|
714
1151
|
]);
|
|
715
1152
|
if (result.status === "loading") {
|
|
716
|
-
return /* @__PURE__ */
|
|
717
|
-
/* @__PURE__ */
|
|
718
|
-
/* @__PURE__ */
|
|
1153
|
+
return /* @__PURE__ */ jsxs4("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
1154
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
1155
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--tiers" })
|
|
719
1156
|
] });
|
|
720
1157
|
}
|
|
721
1158
|
if (result.status === "error") return null;
|
|
722
1159
|
if (!bundle) return null;
|
|
723
1160
|
if (!product) return null;
|
|
724
|
-
return /* @__PURE__ */
|
|
1161
|
+
return /* @__PURE__ */ jsxs4(
|
|
725
1162
|
"div",
|
|
726
1163
|
{
|
|
727
1164
|
ref: (el) => {
|
|
@@ -732,55 +1169,64 @@ function VolumeBundle(props) {
|
|
|
732
1169
|
role: "region",
|
|
733
1170
|
"aria-label": bundle.title,
|
|
734
1171
|
children: [
|
|
735
|
-
/* @__PURE__ */
|
|
736
|
-
/* @__PURE__ */
|
|
737
|
-
|
|
1172
|
+
/* @__PURE__ */ jsx4("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
1173
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
|
|
1174
|
+
thumbImage && /* @__PURE__ */ jsx4(
|
|
738
1175
|
"img",
|
|
739
1176
|
{
|
|
740
|
-
src:
|
|
741
|
-
alt:
|
|
1177
|
+
src: thumbImage.url,
|
|
1178
|
+
alt: thumbImage.altText ?? product.title,
|
|
742
1179
|
className: "lb-bundle__product-image",
|
|
743
1180
|
loading: "lazy"
|
|
744
1181
|
}
|
|
745
1182
|
),
|
|
746
|
-
/* @__PURE__ */
|
|
747
|
-
/* @__PURE__ */
|
|
748
|
-
/* @__PURE__ */
|
|
1183
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product-info", children: [
|
|
1184
|
+
/* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
1185
|
+
/* @__PURE__ */ jsxs4("p", { className: "lb-bundle__product-price", children: [
|
|
749
1186
|
formatMoney3(basePrice, currency),
|
|
750
1187
|
" each"
|
|
751
1188
|
] }),
|
|
752
|
-
(
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
)
|
|
759
|
-
return
|
|
760
|
-
|
|
1189
|
+
unitPriceText && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
1190
|
+
showPicker && /* @__PURE__ */ jsx4("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
1191
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1192
|
+
value: o.value,
|
|
1193
|
+
label: o.value,
|
|
1194
|
+
disabled: o.disabled
|
|
1195
|
+
}));
|
|
1196
|
+
return /* @__PURE__ */ jsx4(
|
|
1197
|
+
VariantDropdown,
|
|
1198
|
+
{
|
|
1199
|
+
options: dropdownOptions,
|
|
1200
|
+
value: selectedValues[optionIndex] ?? null,
|
|
1201
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
1202
|
+
ariaLabel: optionName
|
|
1203
|
+
},
|
|
1204
|
+
optionName
|
|
1205
|
+
);
|
|
1206
|
+
}) })
|
|
761
1207
|
] })
|
|
762
1208
|
] }),
|
|
763
|
-
/* @__PURE__ */
|
|
1209
|
+
/* @__PURE__ */ jsx4(
|
|
764
1210
|
"div",
|
|
765
1211
|
{
|
|
766
1212
|
className: "lb-bundle__tiers",
|
|
767
1213
|
role: "table",
|
|
768
1214
|
"aria-label": "Volume discounts",
|
|
769
|
-
children: tierSavings.map((ts) => /* @__PURE__ */
|
|
1215
|
+
children: tierSavings.map((ts) => /* @__PURE__ */ jsxs4(
|
|
770
1216
|
"div",
|
|
771
1217
|
{
|
|
772
1218
|
className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
|
|
773
1219
|
role: "row",
|
|
774
1220
|
children: [
|
|
775
|
-
/* @__PURE__ */
|
|
1221
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
|
|
776
1222
|
ts.tier.minQuantity,
|
|
777
1223
|
"+ items"
|
|
778
1224
|
] }),
|
|
779
|
-
/* @__PURE__ */
|
|
1225
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-price", role: "cell", children: [
|
|
780
1226
|
formatMoney3(ts.unitPrice, currency),
|
|
781
1227
|
" each"
|
|
782
1228
|
] }),
|
|
783
|
-
/* @__PURE__ */
|
|
1229
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
|
|
784
1230
|
"Save ",
|
|
785
1231
|
ts.savingsPercent.toFixed(0),
|
|
786
1232
|
"%"
|
|
@@ -791,10 +1237,10 @@ function VolumeBundle(props) {
|
|
|
791
1237
|
))
|
|
792
1238
|
}
|
|
793
1239
|
),
|
|
794
|
-
/* @__PURE__ */
|
|
795
|
-
/* @__PURE__ */
|
|
796
|
-
/* @__PURE__ */
|
|
797
|
-
/* @__PURE__ */
|
|
1240
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-selector", children: [
|
|
1241
|
+
/* @__PURE__ */ jsx4("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
|
|
1242
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-control", children: [
|
|
1243
|
+
/* @__PURE__ */ jsx4(
|
|
798
1244
|
"button",
|
|
799
1245
|
{
|
|
800
1246
|
"aria-label": "Decrease quantity",
|
|
@@ -802,7 +1248,7 @@ function VolumeBundle(props) {
|
|
|
802
1248
|
children: "\u2212"
|
|
803
1249
|
}
|
|
804
1250
|
),
|
|
805
|
-
/* @__PURE__ */
|
|
1251
|
+
/* @__PURE__ */ jsx4(
|
|
806
1252
|
"input",
|
|
807
1253
|
{
|
|
808
1254
|
id: `lb-qty-${bundle.id}`,
|
|
@@ -816,7 +1262,7 @@ function VolumeBundle(props) {
|
|
|
816
1262
|
className: "lb-bundle__quantity-input"
|
|
817
1263
|
}
|
|
818
1264
|
),
|
|
819
|
-
/* @__PURE__ */
|
|
1265
|
+
/* @__PURE__ */ jsx4(
|
|
820
1266
|
"button",
|
|
821
1267
|
{
|
|
822
1268
|
"aria-label": "Increase quantity",
|
|
@@ -826,8 +1272,8 @@ function VolumeBundle(props) {
|
|
|
826
1272
|
)
|
|
827
1273
|
] })
|
|
828
1274
|
] }),
|
|
829
|
-
cartError && /* @__PURE__ */
|
|
830
|
-
/* @__PURE__ */
|
|
1275
|
+
cartError && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
1276
|
+
/* @__PURE__ */ jsx4(
|
|
831
1277
|
"button",
|
|
832
1278
|
{
|
|
833
1279
|
className: "lb-bundle__cta",
|
|
@@ -843,7 +1289,7 @@ function VolumeBundle(props) {
|
|
|
843
1289
|
}
|
|
844
1290
|
|
|
845
1291
|
// src/hooks/useBundlesForProduct.ts
|
|
846
|
-
import { useState as
|
|
1292
|
+
import { useState as useState8, useEffect as useEffect9 } from "react";
|
|
847
1293
|
import {
|
|
848
1294
|
fetchBundlesForProduct,
|
|
849
1295
|
injectCustomCss as injectCustomCss2
|
|
@@ -854,10 +1300,10 @@ var INITIAL_STATE2 = {
|
|
|
854
1300
|
error: null
|
|
855
1301
|
};
|
|
856
1302
|
function useBundlesForProduct(options) {
|
|
857
|
-
const [state, setState] =
|
|
1303
|
+
const [state, setState] = useState8(
|
|
858
1304
|
INITIAL_STATE2
|
|
859
1305
|
);
|
|
860
|
-
|
|
1306
|
+
useEffect9(() => {
|
|
861
1307
|
const controller = new AbortController();
|
|
862
1308
|
setState(INITIAL_STATE2);
|
|
863
1309
|
fetchBundlesForProduct({
|
|
@@ -943,6 +1389,7 @@ export {
|
|
|
943
1389
|
MixMatchBundle,
|
|
944
1390
|
SHOP_CUSTOM_CSS_QUERY2 as SHOP_CUSTOM_CSS_QUERY,
|
|
945
1391
|
StorefrontApiError,
|
|
1392
|
+
VariantDropdown,
|
|
946
1393
|
VolumeBundle,
|
|
947
1394
|
WIDGET_CONFIG_DEFAULTS,
|
|
948
1395
|
applyABVariantB,
|
|
@@ -978,6 +1425,7 @@ export {
|
|
|
978
1425
|
useAnalytics,
|
|
979
1426
|
useBundleData,
|
|
980
1427
|
useBundlesForProduct,
|
|
1428
|
+
useVariantSelection,
|
|
981
1429
|
useWidgetConfigVars,
|
|
982
1430
|
validateQuantity2 as validateQuantity
|
|
983
1431
|
};
|