@lime-bundles/react 2.3.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +737 -238
- 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 +701 -191
- package/dist/index.js.map +1 -1
- package/docs/css-variables.md +14 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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,
|
|
6
6
|
calculateDiscount,
|
|
7
|
-
resolveBundleQty
|
|
7
|
+
resolveBundleQty,
|
|
8
|
+
isVariantFulfillable,
|
|
9
|
+
shouldShowLowStockBadge
|
|
8
10
|
} from "@lime-bundles/core";
|
|
9
11
|
|
|
10
12
|
// src/hooks/useBundleData.ts
|
|
@@ -208,8 +210,340 @@ function useWidgetConfigVars(config) {
|
|
|
208
210
|
);
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
// src/
|
|
213
|
+
// src/hooks/useVariantSelection.ts
|
|
214
|
+
import { useEffect as useEffect4, useMemo as useMemo2, useState as useState3, useCallback as useCallback3 } from "react";
|
|
215
|
+
import { variants as variantsNs } from "@lime-bundles/core";
|
|
216
|
+
var { findVariantByOptions, isOptionValueAvailable, toPickerVariant } = variantsNs;
|
|
217
|
+
function defaultInitial(variants) {
|
|
218
|
+
return variants.find((v) => v.availableForSale) ?? variants[0] ?? null;
|
|
219
|
+
}
|
|
220
|
+
function useVariantSelection({
|
|
221
|
+
variants,
|
|
222
|
+
optionNames,
|
|
223
|
+
initialVariant,
|
|
224
|
+
optionValuesByPosition
|
|
225
|
+
}) {
|
|
226
|
+
const seed = useMemo2(
|
|
227
|
+
() => initialVariant ?? defaultInitial(variants),
|
|
228
|
+
[initialVariant, variants]
|
|
229
|
+
);
|
|
230
|
+
const [selectedValues, setSelectedValues] = useState3(
|
|
231
|
+
() => seed ? seed.selectedOptions.map((o) => o.value) : optionNames.map(() => "")
|
|
232
|
+
);
|
|
233
|
+
useEffect4(() => {
|
|
234
|
+
if (!seed) return;
|
|
235
|
+
setSelectedValues(seed.selectedOptions.map((o) => o.value));
|
|
236
|
+
}, [seed?.id]);
|
|
237
|
+
const pickerVariants = useMemo2(
|
|
238
|
+
() => variants.map((v) => toPickerVariant(v)),
|
|
239
|
+
[variants]
|
|
240
|
+
);
|
|
241
|
+
const selectedVariant = useMemo2(() => {
|
|
242
|
+
if (selectedValues.length !== optionNames.length) return null;
|
|
243
|
+
const match = findVariantByOptions(pickerVariants, selectedValues);
|
|
244
|
+
if (!match) return null;
|
|
245
|
+
return variants.find((v) => v.id === match.id) ?? null;
|
|
246
|
+
}, [pickerVariants, selectedValues, variants, optionNames.length]);
|
|
247
|
+
const setOptionValue = useCallback3(
|
|
248
|
+
(optionIndex, value) => {
|
|
249
|
+
setSelectedValues((prev) => {
|
|
250
|
+
const next = prev.slice();
|
|
251
|
+
next[optionIndex] = value;
|
|
252
|
+
return next;
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
[]
|
|
256
|
+
);
|
|
257
|
+
const optionsByPosition = useMemo2(() => {
|
|
258
|
+
if (optionValuesByPosition) {
|
|
259
|
+
return optionValuesByPosition.map((values) => values.slice());
|
|
260
|
+
}
|
|
261
|
+
const out = optionNames.map(() => []);
|
|
262
|
+
for (const v of variants) {
|
|
263
|
+
v.selectedOptions.forEach((o, i) => {
|
|
264
|
+
if (i < out.length && !out[i].includes(o.value)) out[i].push(o.value);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}, [variants, optionNames, optionValuesByPosition]);
|
|
269
|
+
const optionsFor = useCallback3(
|
|
270
|
+
(optionIndex) => {
|
|
271
|
+
const values = optionsByPosition[optionIndex] ?? [];
|
|
272
|
+
return values.map((value) => ({
|
|
273
|
+
value,
|
|
274
|
+
disabled: !isOptionValueAvailable(
|
|
275
|
+
pickerVariants,
|
|
276
|
+
optionIndex,
|
|
277
|
+
value,
|
|
278
|
+
selectedValues
|
|
279
|
+
)
|
|
280
|
+
}));
|
|
281
|
+
},
|
|
282
|
+
[optionsByPosition, pickerVariants, selectedValues]
|
|
283
|
+
);
|
|
284
|
+
return {
|
|
285
|
+
selectedValues,
|
|
286
|
+
selectedVariant,
|
|
287
|
+
setOptionValue,
|
|
288
|
+
optionsFor
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/components/VariantDropdown.tsx
|
|
293
|
+
import {
|
|
294
|
+
useEffect as useEffect5,
|
|
295
|
+
useId,
|
|
296
|
+
useLayoutEffect,
|
|
297
|
+
useMemo as useMemo3,
|
|
298
|
+
useRef as useRef3,
|
|
299
|
+
useState as useState4
|
|
300
|
+
} from "react";
|
|
301
|
+
import { dropdown } from "@lime-bundles/core";
|
|
212
302
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
303
|
+
var { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } = dropdown;
|
|
304
|
+
var ITEM_HEIGHT_PX = 32;
|
|
305
|
+
var LIST_PAD_Y = 8;
|
|
306
|
+
var MAX_VISIBLE_ITEMS = 8;
|
|
307
|
+
function VariantDropdown({
|
|
308
|
+
options,
|
|
309
|
+
value,
|
|
310
|
+
onChange,
|
|
311
|
+
ariaLabel,
|
|
312
|
+
className
|
|
313
|
+
}) {
|
|
314
|
+
const idBase = useId();
|
|
315
|
+
const triggerRef = useRef3(null);
|
|
316
|
+
const listboxRef = useRef3(null);
|
|
317
|
+
const typeAheadRef = useRef3(emptyTypeAheadState());
|
|
318
|
+
const [isOpen, setIsOpen] = useState4(false);
|
|
319
|
+
const [activeIndex, setActiveIndex] = useState4(-1);
|
|
320
|
+
const [position, setPosition] = useState4(null);
|
|
321
|
+
const selectedIndex = useMemo3(
|
|
322
|
+
() => options.findIndex((o) => o.value === value),
|
|
323
|
+
[options, value]
|
|
324
|
+
);
|
|
325
|
+
const optionsState = useMemo3(
|
|
326
|
+
() => options.map((o) => ({ disabled: !!o.disabled, label: o.label })),
|
|
327
|
+
[options]
|
|
328
|
+
);
|
|
329
|
+
const selectedLabel = selectedIndex >= 0 ? options[selectedIndex].label : "";
|
|
330
|
+
function open() {
|
|
331
|
+
if (isOpen) return;
|
|
332
|
+
setIsOpen(true);
|
|
333
|
+
setActiveIndex(
|
|
334
|
+
selectedIndex >= 0 && !optionsState[selectedIndex]?.disabled ? selectedIndex : optionsState.findIndex((o) => !o.disabled)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
function close(restoreFocus) {
|
|
338
|
+
setIsOpen(false);
|
|
339
|
+
setActiveIndex(-1);
|
|
340
|
+
if (restoreFocus) triggerRef.current?.focus();
|
|
341
|
+
}
|
|
342
|
+
function commit(index) {
|
|
343
|
+
const opt = options[index];
|
|
344
|
+
if (!opt || opt.disabled) return;
|
|
345
|
+
if (opt.value !== value) onChange(opt.value);
|
|
346
|
+
close(true);
|
|
347
|
+
}
|
|
348
|
+
useLayoutEffect(() => {
|
|
349
|
+
if (!isOpen) return;
|
|
350
|
+
const trigger = triggerRef.current;
|
|
351
|
+
if (!trigger) return;
|
|
352
|
+
const rect = trigger.getBoundingClientRect();
|
|
353
|
+
if (rect.width === 0) return;
|
|
354
|
+
const visibleCount = Math.min(options.length || 1, MAX_VISIBLE_ITEMS);
|
|
355
|
+
const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;
|
|
356
|
+
setPosition(
|
|
357
|
+
computePosition({
|
|
358
|
+
trigger: {
|
|
359
|
+
top: rect.top,
|
|
360
|
+
bottom: rect.bottom,
|
|
361
|
+
left: rect.left,
|
|
362
|
+
width: rect.width
|
|
363
|
+
},
|
|
364
|
+
viewportHeight: window.innerHeight,
|
|
365
|
+
desiredHeight
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
}, [isOpen, options.length]);
|
|
369
|
+
useEffect5(() => {
|
|
370
|
+
if (!isOpen) return;
|
|
371
|
+
const isInsideDropdown = (event) => {
|
|
372
|
+
const path = event.composedPath();
|
|
373
|
+
const trigger = triggerRef.current;
|
|
374
|
+
const listbox = listboxRef.current;
|
|
375
|
+
return trigger != null && path.includes(trigger) || listbox != null && path.includes(listbox);
|
|
376
|
+
};
|
|
377
|
+
const onPointerdown = (event) => {
|
|
378
|
+
if (isInsideDropdown(event)) return;
|
|
379
|
+
close(false);
|
|
380
|
+
};
|
|
381
|
+
const onScroll = (event) => {
|
|
382
|
+
if (isInsideDropdown(event)) return;
|
|
383
|
+
close(false);
|
|
384
|
+
};
|
|
385
|
+
const onResize = () => close(false);
|
|
386
|
+
document.addEventListener("pointerdown", onPointerdown, true);
|
|
387
|
+
window.addEventListener("scroll", onScroll, true);
|
|
388
|
+
window.addEventListener("resize", onResize);
|
|
389
|
+
return () => {
|
|
390
|
+
document.removeEventListener("pointerdown", onPointerdown, true);
|
|
391
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
392
|
+
window.removeEventListener("resize", onResize);
|
|
393
|
+
};
|
|
394
|
+
}, [isOpen]);
|
|
395
|
+
useEffect5(() => {
|
|
396
|
+
if (!isOpen || activeIndex < 0) return;
|
|
397
|
+
const listbox = listboxRef.current;
|
|
398
|
+
if (!listbox) return;
|
|
399
|
+
const li = listbox.querySelector(
|
|
400
|
+
`#${CSS.escape(`${idBase}-opt-${activeIndex}`)}`
|
|
401
|
+
);
|
|
402
|
+
if (!li) return;
|
|
403
|
+
const liTop = li.offsetTop;
|
|
404
|
+
const liBottom = liTop + li.offsetHeight;
|
|
405
|
+
const visTop = listbox.scrollTop;
|
|
406
|
+
const visBottom = visTop + listbox.clientHeight;
|
|
407
|
+
if (liTop < visTop) {
|
|
408
|
+
listbox.scrollTop = liTop;
|
|
409
|
+
} else if (liBottom > visBottom) {
|
|
410
|
+
listbox.scrollTop = liBottom - listbox.clientHeight;
|
|
411
|
+
}
|
|
412
|
+
}, [activeIndex, isOpen, idBase]);
|
|
413
|
+
function onKeyDown(event) {
|
|
414
|
+
const action = handleKey(
|
|
415
|
+
{
|
|
416
|
+
key: event.key,
|
|
417
|
+
ctrlKey: event.ctrlKey,
|
|
418
|
+
metaKey: event.metaKey,
|
|
419
|
+
altKey: event.altKey,
|
|
420
|
+
shiftKey: event.shiftKey
|
|
421
|
+
},
|
|
422
|
+
{ isOpen, activeIndex, selectedIndex, options: optionsState }
|
|
423
|
+
);
|
|
424
|
+
if (action.preventDefault) event.preventDefault();
|
|
425
|
+
switch (action.type) {
|
|
426
|
+
case "open":
|
|
427
|
+
open();
|
|
428
|
+
if (action.activeIndex >= 0) setActiveIndex(action.activeIndex);
|
|
429
|
+
return;
|
|
430
|
+
case "close":
|
|
431
|
+
close(action.restoreFocus);
|
|
432
|
+
return;
|
|
433
|
+
case "move-active":
|
|
434
|
+
setActiveIndex(action.activeIndex);
|
|
435
|
+
return;
|
|
436
|
+
case "commit":
|
|
437
|
+
commit(action.index);
|
|
438
|
+
return;
|
|
439
|
+
case "type-ahead": {
|
|
440
|
+
const result = pushTypeAheadChar(
|
|
441
|
+
typeAheadRef.current,
|
|
442
|
+
action.char,
|
|
443
|
+
Date.now(),
|
|
444
|
+
optionsState
|
|
445
|
+
);
|
|
446
|
+
typeAheadRef.current = result.newState;
|
|
447
|
+
if (result.matchedIndex !== null) {
|
|
448
|
+
if (!isOpen) open();
|
|
449
|
+
setActiveIndex(result.matchedIndex);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
case "passthrough":
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function onShellBlur(event) {
|
|
458
|
+
const next = event.relatedTarget;
|
|
459
|
+
if (next && event.currentTarget.contains(next)) return;
|
|
460
|
+
setTimeout(() => {
|
|
461
|
+
if (!isOpen) return;
|
|
462
|
+
const active = document.activeElement;
|
|
463
|
+
if (!triggerRef.current?.contains(active) && !listboxRef.current?.contains(active)) {
|
|
464
|
+
close(false);
|
|
465
|
+
}
|
|
466
|
+
}, 0);
|
|
467
|
+
}
|
|
468
|
+
const listboxId = `${idBase}-listbox`;
|
|
469
|
+
const activeDescendantId = activeIndex >= 0 ? `${idBase}-opt-${activeIndex}` : void 0;
|
|
470
|
+
const panelStyle = position ? { maxHeight: position.maxHeight } : void 0;
|
|
471
|
+
return /* @__PURE__ */ jsxs(
|
|
472
|
+
"div",
|
|
473
|
+
{
|
|
474
|
+
className: `lb-dropdown${className ? ` ${className}` : ""}`,
|
|
475
|
+
"data-lb-dropdown": "",
|
|
476
|
+
onBlur: onShellBlur,
|
|
477
|
+
children: [
|
|
478
|
+
/* @__PURE__ */ jsxs(
|
|
479
|
+
"button",
|
|
480
|
+
{
|
|
481
|
+
ref: triggerRef,
|
|
482
|
+
type: "button",
|
|
483
|
+
className: "lb-dropdown-trigger",
|
|
484
|
+
role: "combobox",
|
|
485
|
+
"aria-haspopup": "listbox",
|
|
486
|
+
"aria-expanded": isOpen,
|
|
487
|
+
"aria-controls": listboxId,
|
|
488
|
+
"aria-label": ariaLabel,
|
|
489
|
+
"aria-activedescendant": activeDescendantId,
|
|
490
|
+
onClick: (e) => {
|
|
491
|
+
e.preventDefault();
|
|
492
|
+
if (isOpen) close(false);
|
|
493
|
+
else open();
|
|
494
|
+
},
|
|
495
|
+
onKeyDown,
|
|
496
|
+
children: [
|
|
497
|
+
/* @__PURE__ */ jsx("span", { className: "lb-dropdown-trigger-value", children: selectedLabel }),
|
|
498
|
+
/* @__PURE__ */ jsx("span", { className: "lb-dropdown-chevron", "aria-hidden": "true" })
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
),
|
|
502
|
+
/* @__PURE__ */ jsx(
|
|
503
|
+
"ul",
|
|
504
|
+
{
|
|
505
|
+
ref: listboxRef,
|
|
506
|
+
id: listboxId,
|
|
507
|
+
className: "lb-dropdown-listbox",
|
|
508
|
+
role: "listbox",
|
|
509
|
+
"aria-label": ariaLabel,
|
|
510
|
+
hidden: !isOpen,
|
|
511
|
+
"data-placement": position?.placement,
|
|
512
|
+
style: panelStyle,
|
|
513
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
514
|
+
children: options.map((opt, i) => (
|
|
515
|
+
// Keyboard activation lives on the trigger combobox via
|
|
516
|
+
// aria-activedescendant — the WAI-ARIA combobox pattern keeps
|
|
517
|
+
// focus on the button and tracks the active option by id, so
|
|
518
|
+
// the option <li> elements deliberately have no key handlers.
|
|
519
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
|
|
520
|
+
/* @__PURE__ */ jsx(
|
|
521
|
+
"li",
|
|
522
|
+
{
|
|
523
|
+
id: `${idBase}-opt-${i}`,
|
|
524
|
+
className: `lb-dropdown-option${i === activeIndex ? " is-active" : ""}`,
|
|
525
|
+
role: "option",
|
|
526
|
+
"aria-selected": i === selectedIndex,
|
|
527
|
+
"aria-disabled": opt.disabled || void 0,
|
|
528
|
+
"data-value": opt.value,
|
|
529
|
+
onClick: () => commit(i),
|
|
530
|
+
onMouseMove: () => {
|
|
531
|
+
if (!opt.disabled && i !== activeIndex) setActiveIndex(i);
|
|
532
|
+
},
|
|
533
|
+
children: opt.label
|
|
534
|
+
},
|
|
535
|
+
opt.value
|
|
536
|
+
)
|
|
537
|
+
))
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/components/FixedBundle.tsx
|
|
546
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
213
547
|
function FixedBundle(props) {
|
|
214
548
|
const {
|
|
215
549
|
shopDomain,
|
|
@@ -233,11 +567,21 @@ function FixedBundle(props) {
|
|
|
233
567
|
bundleType: "fixed",
|
|
234
568
|
enabled: analyticsEnabled !== false
|
|
235
569
|
});
|
|
236
|
-
const [addingToCart, setAddingToCart] =
|
|
237
|
-
const [cartError, setCartError] =
|
|
570
|
+
const [addingToCart, setAddingToCart] = useState5(false);
|
|
571
|
+
const [cartError, setCartError] = useState5(null);
|
|
572
|
+
const [selectedVariants, setSelectedVariants] = useState5({});
|
|
573
|
+
const handleVariantChange = useCallback4(
|
|
574
|
+
(productId, variant) => {
|
|
575
|
+
setSelectedVariants((prev) => {
|
|
576
|
+
if (prev[productId] === variant) return prev;
|
|
577
|
+
return { ...prev, [productId]: variant };
|
|
578
|
+
});
|
|
579
|
+
},
|
|
580
|
+
[]
|
|
581
|
+
);
|
|
238
582
|
const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
|
|
239
583
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
240
|
-
|
|
584
|
+
useEffect6(() => {
|
|
241
585
|
if (result.status === "error") {
|
|
242
586
|
onError?.(result.error);
|
|
243
587
|
return;
|
|
@@ -250,10 +594,16 @@ function FixedBundle(props) {
|
|
|
250
594
|
);
|
|
251
595
|
}
|
|
252
596
|
}, [result, onError]);
|
|
253
|
-
const handleAddToCart =
|
|
597
|
+
const handleAddToCart = useCallback4(async () => {
|
|
254
598
|
if (!bundle) return;
|
|
255
|
-
const bundleLines = bundle.products.filter(
|
|
256
|
-
|
|
599
|
+
const bundleLines = bundle.products.filter(
|
|
600
|
+
(p) => p.variants.nodes.some(
|
|
601
|
+
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, p.id, v.id))
|
|
602
|
+
)
|
|
603
|
+
).map((p) => {
|
|
604
|
+
const variant = selectedVariants[p.id] ?? p.variants.nodes.find(
|
|
605
|
+
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, p.id, v.id))
|
|
606
|
+
);
|
|
257
607
|
return {
|
|
258
608
|
productId: p.id,
|
|
259
609
|
variant,
|
|
@@ -297,17 +647,17 @@ function FixedBundle(props) {
|
|
|
297
647
|
} finally {
|
|
298
648
|
setAddingToCart(false);
|
|
299
649
|
}
|
|
300
|
-
}, [bundle, onAddToCart, onError, trackAddToCart]);
|
|
650
|
+
}, [bundle, onAddToCart, onError, trackAddToCart, selectedVariants]);
|
|
301
651
|
if (result.status === "loading") {
|
|
302
|
-
return /* @__PURE__ */
|
|
303
|
-
/* @__PURE__ */
|
|
304
|
-
/* @__PURE__ */
|
|
652
|
+
return /* @__PURE__ */ jsxs2("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
653
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
654
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
305
655
|
] });
|
|
306
656
|
}
|
|
307
657
|
if (result.status === "error") return null;
|
|
308
658
|
if (!bundle) return null;
|
|
309
659
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
310
|
-
return /* @__PURE__ */
|
|
660
|
+
return /* @__PURE__ */ jsxs2(
|
|
311
661
|
"div",
|
|
312
662
|
{
|
|
313
663
|
ref: (el) => {
|
|
@@ -318,38 +668,20 @@ function FixedBundle(props) {
|
|
|
318
668
|
role: "region",
|
|
319
669
|
"aria-label": bundle.title,
|
|
320
670
|
children: [
|
|
321
|
-
/* @__PURE__ */
|
|
322
|
-
bundle.discountLabel && /* @__PURE__ */
|
|
323
|
-
/* @__PURE__ */
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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(
|
|
671
|
+
/* @__PURE__ */ jsx2("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
672
|
+
bundle.discountLabel && /* @__PURE__ */ jsx2("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
|
|
673
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ jsx2(
|
|
674
|
+
FixedProductRow,
|
|
675
|
+
{
|
|
676
|
+
bundle,
|
|
677
|
+
product,
|
|
678
|
+
currency,
|
|
679
|
+
onVariantChange: handleVariantChange
|
|
680
|
+
},
|
|
681
|
+
product.id
|
|
682
|
+
)) }),
|
|
683
|
+
cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
684
|
+
/* @__PURE__ */ jsx2(
|
|
353
685
|
"button",
|
|
354
686
|
{
|
|
355
687
|
className: "lb-bundle__cta",
|
|
@@ -363,16 +695,87 @@ function FixedBundle(props) {
|
|
|
363
695
|
}
|
|
364
696
|
);
|
|
365
697
|
}
|
|
698
|
+
function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
699
|
+
const variants = product.variants.nodes;
|
|
700
|
+
const optionNames = useMemo4(() => {
|
|
701
|
+
const first = variants[0];
|
|
702
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
703
|
+
}, [variants]);
|
|
704
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
705
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
|
|
706
|
+
variants,
|
|
707
|
+
optionNames
|
|
708
|
+
});
|
|
709
|
+
useEffect6(() => {
|
|
710
|
+
onVariantChange(product.id, selectedVariant ?? null);
|
|
711
|
+
}, [selectedVariant, product.id, onVariantChange]);
|
|
712
|
+
const displayVariant = selectedVariant ?? variants.find(
|
|
713
|
+
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id))
|
|
714
|
+
) ?? variants[0];
|
|
715
|
+
const priceText = displayVariant ? formatMoney(displayVariant.price.amount, currency) : formatMoney(product.priceRange.minVariantPrice.amount, currency);
|
|
716
|
+
const unitPriceText = displayVariant ? formatUnitPrice(
|
|
717
|
+
displayVariant.unitPrice,
|
|
718
|
+
displayVariant.unitPriceMeasurement,
|
|
719
|
+
currency
|
|
720
|
+
) : null;
|
|
721
|
+
const thumbImage = displayVariant?.image ?? product.featuredImage;
|
|
722
|
+
const showLowStock = !!displayVariant && shouldShowLowStockBadge(
|
|
723
|
+
displayVariant,
|
|
724
|
+
resolveBundleQty(bundle, product.id, displayVariant.id),
|
|
725
|
+
bundle.widgetConfig.lowStockThreshold,
|
|
726
|
+
bundle.widgetConfig.showLowStockBadge
|
|
727
|
+
);
|
|
728
|
+
return /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product", part: "product", children: [
|
|
729
|
+
thumbImage && /* @__PURE__ */ jsx2(
|
|
730
|
+
"img",
|
|
731
|
+
{
|
|
732
|
+
src: thumbImage.url,
|
|
733
|
+
alt: thumbImage.altText ?? product.title,
|
|
734
|
+
className: "lb-bundle__product-image",
|
|
735
|
+
loading: "lazy"
|
|
736
|
+
}
|
|
737
|
+
),
|
|
738
|
+
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
|
|
739
|
+
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
740
|
+
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: priceText }),
|
|
741
|
+
unitPriceText && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
742
|
+
showLowStock && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
743
|
+
"Only ",
|
|
744
|
+
displayVariant.quantityAvailable,
|
|
745
|
+
" left"
|
|
746
|
+
] }),
|
|
747
|
+
showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
748
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
749
|
+
value: o.value,
|
|
750
|
+
label: o.value,
|
|
751
|
+
disabled: o.disabled
|
|
752
|
+
}));
|
|
753
|
+
return /* @__PURE__ */ jsx2(
|
|
754
|
+
VariantDropdown,
|
|
755
|
+
{
|
|
756
|
+
options: dropdownOptions,
|
|
757
|
+
value: selectedValues[optionIndex] ?? null,
|
|
758
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
759
|
+
ariaLabel: optionName
|
|
760
|
+
},
|
|
761
|
+
optionName
|
|
762
|
+
);
|
|
763
|
+
}) })
|
|
764
|
+
] })
|
|
765
|
+
] });
|
|
766
|
+
}
|
|
366
767
|
|
|
367
768
|
// src/components/MixMatchBundle.tsx
|
|
368
|
-
import { useCallback as
|
|
769
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo5, useState as useState6 } from "react";
|
|
369
770
|
import {
|
|
370
771
|
formatMoney as formatMoney2,
|
|
371
772
|
formatUnitPrice as formatUnitPrice2,
|
|
372
773
|
validateQuantity,
|
|
373
|
-
resolveBundleQty as resolveBundleQty2
|
|
774
|
+
resolveBundleQty as resolveBundleQty2,
|
|
775
|
+
isVariantFulfillable as isVariantFulfillable2,
|
|
776
|
+
shouldShowLowStockBadge as shouldShowLowStockBadge2
|
|
374
777
|
} from "@lime-bundles/core";
|
|
375
|
-
import { jsx as
|
|
778
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
376
779
|
function MixMatchBundle(props) {
|
|
377
780
|
const {
|
|
378
781
|
shopDomain,
|
|
@@ -396,14 +799,14 @@ function MixMatchBundle(props) {
|
|
|
396
799
|
bundleType: "mix_match",
|
|
397
800
|
enabled: analyticsEnabled !== false
|
|
398
801
|
});
|
|
399
|
-
const [selections, setSelections] =
|
|
802
|
+
const [selections, setSelections] = useState6(
|
|
400
803
|
/* @__PURE__ */ new Map()
|
|
401
804
|
);
|
|
402
|
-
const [addingToCart, setAddingToCart] =
|
|
403
|
-
const [cartError, setCartError] =
|
|
805
|
+
const [addingToCart, setAddingToCart] = useState6(false);
|
|
806
|
+
const [cartError, setCartError] = useState6(null);
|
|
404
807
|
const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
|
|
405
808
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
406
|
-
|
|
809
|
+
useEffect7(() => {
|
|
407
810
|
if (result.status === "error") {
|
|
408
811
|
onError?.(result.error);
|
|
409
812
|
return;
|
|
@@ -421,7 +824,7 @@ function MixMatchBundle(props) {
|
|
|
421
824
|
0
|
|
422
825
|
);
|
|
423
826
|
const validation = bundle ? validateQuantity(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
|
|
424
|
-
const toggleProduct =
|
|
827
|
+
const toggleProduct = useCallback5(
|
|
425
828
|
(productId, variant) => {
|
|
426
829
|
setSelections((prev) => {
|
|
427
830
|
const next = new Map(prev);
|
|
@@ -441,7 +844,7 @@ function MixMatchBundle(props) {
|
|
|
441
844
|
},
|
|
442
845
|
[bundle]
|
|
443
846
|
);
|
|
444
|
-
const updateQuantity =
|
|
847
|
+
const updateQuantity = useCallback5(
|
|
445
848
|
(productId, variantId, quantity) => {
|
|
446
849
|
setSelections((prev) => {
|
|
447
850
|
const next = new Map(prev);
|
|
@@ -456,7 +859,7 @@ function MixMatchBundle(props) {
|
|
|
456
859
|
},
|
|
457
860
|
[]
|
|
458
861
|
);
|
|
459
|
-
const handleAddToCart =
|
|
862
|
+
const handleAddToCart = useCallback5(async () => {
|
|
460
863
|
if (!bundle || !validation.valid) return;
|
|
461
864
|
const lines = Array.from(selections.values()).map((s) => ({
|
|
462
865
|
merchandiseId: s.variantId,
|
|
@@ -501,15 +904,15 @@ function MixMatchBundle(props) {
|
|
|
501
904
|
totalQuantity
|
|
502
905
|
]);
|
|
503
906
|
if (result.status === "loading") {
|
|
504
|
-
return /* @__PURE__ */
|
|
505
|
-
/* @__PURE__ */
|
|
506
|
-
/* @__PURE__ */
|
|
907
|
+
return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
908
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
909
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
507
910
|
] });
|
|
508
911
|
}
|
|
509
912
|
if (result.status === "error") return null;
|
|
510
913
|
if (!bundle) return null;
|
|
511
914
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
512
|
-
return /* @__PURE__ */
|
|
915
|
+
return /* @__PURE__ */ jsxs3(
|
|
513
916
|
"div",
|
|
514
917
|
{
|
|
515
918
|
ref: (el) => {
|
|
@@ -520,83 +923,24 @@ function MixMatchBundle(props) {
|
|
|
520
923
|
role: "region",
|
|
521
924
|
"aria-label": bundle.title,
|
|
522
925
|
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
|
-
|
|
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(
|
|
926
|
+
/* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
927
|
+
bundle.discountLabel && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
|
|
928
|
+
/* @__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" }),
|
|
929
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ jsx3(
|
|
930
|
+
MixMatchProductRow,
|
|
931
|
+
{
|
|
932
|
+
bundle,
|
|
933
|
+
product,
|
|
934
|
+
currency,
|
|
935
|
+
selections,
|
|
936
|
+
onToggle: toggleProduct,
|
|
937
|
+
onUpdateQuantity: updateQuantity
|
|
938
|
+
},
|
|
939
|
+
product.id
|
|
940
|
+
)) }),
|
|
941
|
+
validation.message && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
|
|
942
|
+
cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
943
|
+
/* @__PURE__ */ jsx3(
|
|
600
944
|
"button",
|
|
601
945
|
{
|
|
602
946
|
className: "lb-bundle__cta",
|
|
@@ -610,16 +954,139 @@ function MixMatchBundle(props) {
|
|
|
610
954
|
}
|
|
611
955
|
);
|
|
612
956
|
}
|
|
957
|
+
function MixMatchProductRow({
|
|
958
|
+
bundle,
|
|
959
|
+
product,
|
|
960
|
+
currency,
|
|
961
|
+
selections,
|
|
962
|
+
onToggle,
|
|
963
|
+
onUpdateQuantity
|
|
964
|
+
}) {
|
|
965
|
+
const variants = product.variants.nodes;
|
|
966
|
+
const optionNames = useMemo5(() => {
|
|
967
|
+
const first = variants[0];
|
|
968
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
969
|
+
}, [variants]);
|
|
970
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
971
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
972
|
+
const displayVariant = selectedVariant ?? variants.find(
|
|
973
|
+
(v) => isVariantFulfillable2(v, resolveBundleQty2(bundle, product.id, v.id))
|
|
974
|
+
) ?? variants[0] ?? null;
|
|
975
|
+
if (!displayVariant) return null;
|
|
976
|
+
const displayVariantQty = resolveBundleQty2(
|
|
977
|
+
bundle,
|
|
978
|
+
product.id,
|
|
979
|
+
displayVariant.id
|
|
980
|
+
);
|
|
981
|
+
const displayVariantFulfillable = isVariantFulfillable2(
|
|
982
|
+
displayVariant,
|
|
983
|
+
displayVariantQty
|
|
984
|
+
);
|
|
985
|
+
const key = `${product.id}:${displayVariant.id}`;
|
|
986
|
+
const selected = selections.get(key);
|
|
987
|
+
const thumbImage = displayVariant.image ?? product.featuredImage;
|
|
988
|
+
const unitPriceText = formatUnitPrice2(
|
|
989
|
+
displayVariant.unitPrice,
|
|
990
|
+
displayVariant.unitPriceMeasurement,
|
|
991
|
+
currency
|
|
992
|
+
);
|
|
993
|
+
return /* @__PURE__ */ jsxs3(
|
|
994
|
+
"div",
|
|
995
|
+
{
|
|
996
|
+
className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
|
|
997
|
+
children: [
|
|
998
|
+
thumbImage && /* @__PURE__ */ jsx3(
|
|
999
|
+
"img",
|
|
1000
|
+
{
|
|
1001
|
+
src: thumbImage.url,
|
|
1002
|
+
alt: thumbImage.altText ?? product.title,
|
|
1003
|
+
className: "lb-bundle__product-image",
|
|
1004
|
+
loading: "lazy"
|
|
1005
|
+
}
|
|
1006
|
+
),
|
|
1007
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product-info", children: [
|
|
1008
|
+
/* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
1009
|
+
/* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-price", children: formatMoney2(displayVariant.price.amount, currency) }),
|
|
1010
|
+
unitPriceText && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
1011
|
+
shouldShowLowStockBadge2(
|
|
1012
|
+
displayVariant,
|
|
1013
|
+
displayVariantQty,
|
|
1014
|
+
bundle.widgetConfig.lowStockThreshold,
|
|
1015
|
+
bundle.widgetConfig.showLowStockBadge
|
|
1016
|
+
) && /* @__PURE__ */ jsxs3("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
1017
|
+
"Only ",
|
|
1018
|
+
displayVariant.quantityAvailable,
|
|
1019
|
+
" left"
|
|
1020
|
+
] }),
|
|
1021
|
+
showPicker && !selected && /* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
1022
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1023
|
+
value: o.value,
|
|
1024
|
+
label: o.value,
|
|
1025
|
+
disabled: o.disabled
|
|
1026
|
+
}));
|
|
1027
|
+
return /* @__PURE__ */ jsx3(
|
|
1028
|
+
VariantDropdown,
|
|
1029
|
+
{
|
|
1030
|
+
options: dropdownOptions,
|
|
1031
|
+
value: selectedValues[optionIndex] ?? null,
|
|
1032
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
1033
|
+
ariaLabel: optionName
|
|
1034
|
+
},
|
|
1035
|
+
optionName
|
|
1036
|
+
);
|
|
1037
|
+
}) })
|
|
1038
|
+
] }),
|
|
1039
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
|
|
1040
|
+
/* @__PURE__ */ jsx3(
|
|
1041
|
+
"button",
|
|
1042
|
+
{
|
|
1043
|
+
"aria-label": `Decrease ${product.title}`,
|
|
1044
|
+
onClick: () => onUpdateQuantity(
|
|
1045
|
+
product.id,
|
|
1046
|
+
displayVariant.id,
|
|
1047
|
+
selected.quantity - 1
|
|
1048
|
+
),
|
|
1049
|
+
children: "\u2212"
|
|
1050
|
+
}
|
|
1051
|
+
),
|
|
1052
|
+
/* @__PURE__ */ jsx3("span", { children: selected.quantity }),
|
|
1053
|
+
/* @__PURE__ */ jsx3(
|
|
1054
|
+
"button",
|
|
1055
|
+
{
|
|
1056
|
+
"aria-label": `Increase ${product.title}`,
|
|
1057
|
+
onClick: () => onUpdateQuantity(
|
|
1058
|
+
product.id,
|
|
1059
|
+
displayVariant.id,
|
|
1060
|
+
selected.quantity + 1
|
|
1061
|
+
),
|
|
1062
|
+
children: "+"
|
|
1063
|
+
}
|
|
1064
|
+
)
|
|
1065
|
+
] }) : /* @__PURE__ */ jsx3(
|
|
1066
|
+
"button",
|
|
1067
|
+
{
|
|
1068
|
+
className: "lb-bundle__select-btn",
|
|
1069
|
+
onClick: () => onToggle(product.id, displayVariant),
|
|
1070
|
+
disabled: !displayVariantFulfillable,
|
|
1071
|
+
children: displayVariantFulfillable ? "Select" : "Sold out"
|
|
1072
|
+
}
|
|
1073
|
+
) })
|
|
1074
|
+
]
|
|
1075
|
+
}
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
613
1078
|
|
|
614
1079
|
// src/components/VolumeBundle.tsx
|
|
615
|
-
import { useCallback as
|
|
1080
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo6, useState as useState7 } from "react";
|
|
616
1081
|
import {
|
|
617
1082
|
formatMoney as formatMoney3,
|
|
618
1083
|
formatUnitPrice as formatUnitPrice3,
|
|
619
1084
|
calculateTierSavings,
|
|
620
|
-
getActiveTier
|
|
1085
|
+
getActiveTier,
|
|
1086
|
+
isVariantFulfillable as isVariantFulfillable3,
|
|
1087
|
+
shouldShowLowStockBadge as shouldShowLowStockBadge3
|
|
621
1088
|
} from "@lime-bundles/core";
|
|
622
|
-
import { jsx as
|
|
1089
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
623
1090
|
function VolumeBundle(props) {
|
|
624
1091
|
const {
|
|
625
1092
|
shopDomain,
|
|
@@ -643,12 +1110,12 @@ function VolumeBundle(props) {
|
|
|
643
1110
|
bundleType: "volume",
|
|
644
1111
|
enabled: analyticsEnabled !== false
|
|
645
1112
|
});
|
|
646
|
-
const [quantity, setQuantity] =
|
|
647
|
-
const [addingToCart, setAddingToCart] =
|
|
648
|
-
const [cartError, setCartError] =
|
|
1113
|
+
const [quantity, setQuantity] = useState7(1);
|
|
1114
|
+
const [addingToCart, setAddingToCart] = useState7(false);
|
|
1115
|
+
const [cartError, setCartError] = useState7(null);
|
|
649
1116
|
const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
|
|
650
1117
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
651
|
-
|
|
1118
|
+
useEffect8(() => {
|
|
652
1119
|
if (result.status === "error") {
|
|
653
1120
|
onError?.(result.error);
|
|
654
1121
|
return;
|
|
@@ -662,18 +1129,38 @@ function VolumeBundle(props) {
|
|
|
662
1129
|
}
|
|
663
1130
|
}, [result, onError]);
|
|
664
1131
|
const product = bundle?.products[0];
|
|
665
|
-
const
|
|
1132
|
+
const variants = useMemo6(() => product?.variants.nodes ?? [], [product]);
|
|
1133
|
+
const optionNames = useMemo6(() => {
|
|
1134
|
+
const first = variants[0];
|
|
1135
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
1136
|
+
}, [variants]);
|
|
1137
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
1138
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
1139
|
+
const minTierQty = bundle?.volumeTiers[0]?.minQuantity ?? 1;
|
|
1140
|
+
const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable3(v, minTierQty)) ?? variants[0];
|
|
1141
|
+
const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
|
|
666
1142
|
const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
) :
|
|
1143
|
+
const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
|
|
1144
|
+
const unitPriceText = displayVariant ? formatUnitPrice3(
|
|
1145
|
+
displayVariant.unitPrice,
|
|
1146
|
+
displayVariant.unitPriceMeasurement,
|
|
1147
|
+
currency
|
|
1148
|
+
) : null;
|
|
1149
|
+
const tierSavings = useMemo6(
|
|
1150
|
+
() => bundle ? calculateTierSavings(
|
|
1151
|
+
bundle.volumeTiers,
|
|
1152
|
+
basePrice,
|
|
1153
|
+
quantity,
|
|
1154
|
+
bundle.discountConfig.discountType
|
|
1155
|
+
) : [],
|
|
1156
|
+
[bundle, basePrice, quantity]
|
|
1157
|
+
);
|
|
673
1158
|
const activeTier = bundle ? getActiveTier(bundle.volumeTiers, quantity) : null;
|
|
674
|
-
const handleAddToCart =
|
|
1159
|
+
const handleAddToCart = useCallback6(async () => {
|
|
675
1160
|
if (!bundle || !product) return;
|
|
676
|
-
const variant = product.variants.nodes.find(
|
|
1161
|
+
const variant = selectedVariant ?? product.variants.nodes.find(
|
|
1162
|
+
(v) => isVariantFulfillable3(v, quantity)
|
|
1163
|
+
);
|
|
677
1164
|
if (!variant) return;
|
|
678
1165
|
const lines = [
|
|
679
1166
|
{
|
|
@@ -710,18 +1197,20 @@ function VolumeBundle(props) {
|
|
|
710
1197
|
basePrice,
|
|
711
1198
|
onAddToCart,
|
|
712
1199
|
onError,
|
|
713
|
-
trackAddToCart
|
|
1200
|
+
trackAddToCart,
|
|
1201
|
+
selectedVariant,
|
|
1202
|
+
tierSavings
|
|
714
1203
|
]);
|
|
715
1204
|
if (result.status === "loading") {
|
|
716
|
-
return /* @__PURE__ */
|
|
717
|
-
/* @__PURE__ */
|
|
718
|
-
/* @__PURE__ */
|
|
1205
|
+
return /* @__PURE__ */ jsxs4("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
1206
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
1207
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--tiers" })
|
|
719
1208
|
] });
|
|
720
1209
|
}
|
|
721
1210
|
if (result.status === "error") return null;
|
|
722
1211
|
if (!bundle) return null;
|
|
723
1212
|
if (!product) return null;
|
|
724
|
-
return /* @__PURE__ */
|
|
1213
|
+
return /* @__PURE__ */ jsxs4(
|
|
725
1214
|
"div",
|
|
726
1215
|
{
|
|
727
1216
|
ref: (el) => {
|
|
@@ -732,55 +1221,64 @@ function VolumeBundle(props) {
|
|
|
732
1221
|
role: "region",
|
|
733
1222
|
"aria-label": bundle.title,
|
|
734
1223
|
children: [
|
|
735
|
-
/* @__PURE__ */
|
|
736
|
-
/* @__PURE__ */
|
|
737
|
-
|
|
1224
|
+
/* @__PURE__ */ jsx4("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
1225
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
|
|
1226
|
+
thumbImage && /* @__PURE__ */ jsx4(
|
|
738
1227
|
"img",
|
|
739
1228
|
{
|
|
740
|
-
src:
|
|
741
|
-
alt:
|
|
1229
|
+
src: thumbImage.url,
|
|
1230
|
+
alt: thumbImage.altText ?? product.title,
|
|
742
1231
|
className: "lb-bundle__product-image",
|
|
743
1232
|
loading: "lazy"
|
|
744
1233
|
}
|
|
745
1234
|
),
|
|
746
|
-
/* @__PURE__ */
|
|
747
|
-
/* @__PURE__ */
|
|
748
|
-
/* @__PURE__ */
|
|
1235
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__product-info", children: [
|
|
1236
|
+
/* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
1237
|
+
/* @__PURE__ */ jsxs4("p", { className: "lb-bundle__product-price", children: [
|
|
749
1238
|
formatMoney3(basePrice, currency),
|
|
750
1239
|
" each"
|
|
751
1240
|
] }),
|
|
752
|
-
(
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
)
|
|
759
|
-
return
|
|
760
|
-
|
|
1241
|
+
unitPriceText && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
1242
|
+
showPicker && /* @__PURE__ */ jsx4("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
1243
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1244
|
+
value: o.value,
|
|
1245
|
+
label: o.value,
|
|
1246
|
+
disabled: o.disabled
|
|
1247
|
+
}));
|
|
1248
|
+
return /* @__PURE__ */ jsx4(
|
|
1249
|
+
VariantDropdown,
|
|
1250
|
+
{
|
|
1251
|
+
options: dropdownOptions,
|
|
1252
|
+
value: selectedValues[optionIndex] ?? null,
|
|
1253
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
1254
|
+
ariaLabel: optionName
|
|
1255
|
+
},
|
|
1256
|
+
optionName
|
|
1257
|
+
);
|
|
1258
|
+
}) })
|
|
761
1259
|
] })
|
|
762
1260
|
] }),
|
|
763
|
-
/* @__PURE__ */
|
|
1261
|
+
/* @__PURE__ */ jsx4(
|
|
764
1262
|
"div",
|
|
765
1263
|
{
|
|
766
1264
|
className: "lb-bundle__tiers",
|
|
767
1265
|
role: "table",
|
|
768
1266
|
"aria-label": "Volume discounts",
|
|
769
|
-
children: tierSavings.map((ts) => /* @__PURE__ */
|
|
1267
|
+
children: tierSavings.map((ts) => /* @__PURE__ */ jsxs4(
|
|
770
1268
|
"div",
|
|
771
1269
|
{
|
|
772
1270
|
className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
|
|
773
1271
|
role: "row",
|
|
774
1272
|
children: [
|
|
775
|
-
/* @__PURE__ */
|
|
1273
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
|
|
776
1274
|
ts.tier.minQuantity,
|
|
777
1275
|
"+ items"
|
|
778
1276
|
] }),
|
|
779
|
-
/* @__PURE__ */
|
|
1277
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-price", role: "cell", children: [
|
|
780
1278
|
formatMoney3(ts.unitPrice, currency),
|
|
781
1279
|
" each"
|
|
782
1280
|
] }),
|
|
783
|
-
/* @__PURE__ */
|
|
1281
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
|
|
784
1282
|
"Save ",
|
|
785
1283
|
ts.savingsPercent.toFixed(0),
|
|
786
1284
|
"%"
|
|
@@ -791,10 +1289,10 @@ function VolumeBundle(props) {
|
|
|
791
1289
|
))
|
|
792
1290
|
}
|
|
793
1291
|
),
|
|
794
|
-
/* @__PURE__ */
|
|
795
|
-
/* @__PURE__ */
|
|
796
|
-
/* @__PURE__ */
|
|
797
|
-
/* @__PURE__ */
|
|
1292
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-selector", children: [
|
|
1293
|
+
/* @__PURE__ */ jsx4("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
|
|
1294
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-control", children: [
|
|
1295
|
+
/* @__PURE__ */ jsx4(
|
|
798
1296
|
"button",
|
|
799
1297
|
{
|
|
800
1298
|
"aria-label": "Decrease quantity",
|
|
@@ -802,7 +1300,7 @@ function VolumeBundle(props) {
|
|
|
802
1300
|
children: "\u2212"
|
|
803
1301
|
}
|
|
804
1302
|
),
|
|
805
|
-
/* @__PURE__ */
|
|
1303
|
+
/* @__PURE__ */ jsx4(
|
|
806
1304
|
"input",
|
|
807
1305
|
{
|
|
808
1306
|
id: `lb-qty-${bundle.id}`,
|
|
@@ -816,7 +1314,7 @@ function VolumeBundle(props) {
|
|
|
816
1314
|
className: "lb-bundle__quantity-input"
|
|
817
1315
|
}
|
|
818
1316
|
),
|
|
819
|
-
/* @__PURE__ */
|
|
1317
|
+
/* @__PURE__ */ jsx4(
|
|
820
1318
|
"button",
|
|
821
1319
|
{
|
|
822
1320
|
"aria-label": "Increase quantity",
|
|
@@ -826,8 +1324,18 @@ function VolumeBundle(props) {
|
|
|
826
1324
|
)
|
|
827
1325
|
] })
|
|
828
1326
|
] }),
|
|
829
|
-
cartError && /* @__PURE__ */
|
|
830
|
-
|
|
1327
|
+
cartError && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
1328
|
+
bundle && displayVariant && shouldShowLowStockBadge3(
|
|
1329
|
+
displayVariant,
|
|
1330
|
+
minTierQty,
|
|
1331
|
+
bundle.widgetConfig.lowStockThreshold,
|
|
1332
|
+
bundle.widgetConfig.showLowStockBadge
|
|
1333
|
+
) && /* @__PURE__ */ jsxs4("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
1334
|
+
"Only ",
|
|
1335
|
+
displayVariant.quantityAvailable,
|
|
1336
|
+
" left"
|
|
1337
|
+
] }),
|
|
1338
|
+
/* @__PURE__ */ jsx4(
|
|
831
1339
|
"button",
|
|
832
1340
|
{
|
|
833
1341
|
className: "lb-bundle__cta",
|
|
@@ -843,7 +1351,7 @@ function VolumeBundle(props) {
|
|
|
843
1351
|
}
|
|
844
1352
|
|
|
845
1353
|
// src/hooks/useBundlesForProduct.ts
|
|
846
|
-
import { useState as
|
|
1354
|
+
import { useState as useState8, useEffect as useEffect9 } from "react";
|
|
847
1355
|
import {
|
|
848
1356
|
fetchBundlesForProduct,
|
|
849
1357
|
injectCustomCss as injectCustomCss2
|
|
@@ -854,10 +1362,10 @@ var INITIAL_STATE2 = {
|
|
|
854
1362
|
error: null
|
|
855
1363
|
};
|
|
856
1364
|
function useBundlesForProduct(options) {
|
|
857
|
-
const [state, setState] =
|
|
1365
|
+
const [state, setState] = useState8(
|
|
858
1366
|
INITIAL_STATE2
|
|
859
1367
|
);
|
|
860
|
-
|
|
1368
|
+
useEffect9(() => {
|
|
861
1369
|
const controller = new AbortController();
|
|
862
1370
|
setState(INITIAL_STATE2);
|
|
863
1371
|
fetchBundlesForProduct({
|
|
@@ -943,6 +1451,7 @@ export {
|
|
|
943
1451
|
MixMatchBundle,
|
|
944
1452
|
SHOP_CUSTOM_CSS_QUERY2 as SHOP_CUSTOM_CSS_QUERY,
|
|
945
1453
|
StorefrontApiError,
|
|
1454
|
+
VariantDropdown,
|
|
946
1455
|
VolumeBundle,
|
|
947
1456
|
WIDGET_CONFIG_DEFAULTS,
|
|
948
1457
|
applyABVariantB,
|
|
@@ -978,6 +1487,7 @@ export {
|
|
|
978
1487
|
useAnalytics,
|
|
979
1488
|
useBundleData,
|
|
980
1489
|
useBundlesForProduct,
|
|
1490
|
+
useVariantSelection,
|
|
981
1491
|
useWidgetConfigVars,
|
|
982
1492
|
validateQuantity2 as validateQuantity
|
|
983
1493
|
};
|