@lime-bundles/react 5.0.0 → 6.0.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/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/index.cjs +3250 -633
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -11
- package/dist/index.d.ts +54 -11
- package/dist/index.js +3263 -610
- package/dist/index.js.map +1 -1
- package/docs/css-variables.md +44 -70
- package/docs/react-nextjs.md +3 -1
- package/docs/web-component.md +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/FixedBundle.tsx
|
|
2
|
-
import { useCallback as
|
|
2
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
|
|
3
3
|
import {
|
|
4
4
|
formatMoney,
|
|
5
5
|
formatUnitPrice,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
BUNDLE_METAOBJECT_QUERY,
|
|
23
23
|
withInContext,
|
|
24
24
|
parseMetaobjectBundleStrict,
|
|
25
|
+
isVisibleInMarket,
|
|
25
26
|
BundleParseError
|
|
26
27
|
} from "@lime-bundles/core";
|
|
27
28
|
async function fetchBundleData(options) {
|
|
@@ -61,7 +62,7 @@ async function fetchBundleDataWithWarnings(options) {
|
|
|
61
62
|
data.metaobject.id,
|
|
62
63
|
data.metaobject.fields
|
|
63
64
|
);
|
|
64
|
-
if (bundle
|
|
65
|
+
if (!isVisibleInMarket(bundle, options.marketId)) {
|
|
65
66
|
return {
|
|
66
67
|
bundle: null,
|
|
67
68
|
warnings: [
|
|
@@ -77,13 +78,19 @@ async function fetchBundleDataWithWarnings(options) {
|
|
|
77
78
|
return { bundle, warnings: [] };
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
// src/
|
|
81
|
+
// src/fetchShopSettings.ts
|
|
81
82
|
import {
|
|
82
83
|
createStorefrontClient as createStorefrontClient2,
|
|
83
|
-
|
|
84
|
+
SHOP_SETTINGS_QUERY,
|
|
85
|
+
DEFAULT_OUT_OF_STOCK_BEHAVIOR,
|
|
86
|
+
parseOutOfStockBehavior
|
|
84
87
|
} from "@lime-bundles/core";
|
|
88
|
+
var FALLBACK = {
|
|
89
|
+
customCss: null,
|
|
90
|
+
outOfStockBehavior: DEFAULT_OUT_OF_STOCK_BEHAVIOR
|
|
91
|
+
};
|
|
85
92
|
var cache = /* @__PURE__ */ new Map();
|
|
86
|
-
async function
|
|
93
|
+
async function fetchShopSettings(options) {
|
|
87
94
|
const cached = cache.get(options.shopDomain);
|
|
88
95
|
if (cached) return cached;
|
|
89
96
|
const promise = (async () => {
|
|
@@ -93,11 +100,16 @@ async function fetchShopCustomCss(options) {
|
|
|
93
100
|
buyerIp: options.buyerIp
|
|
94
101
|
});
|
|
95
102
|
const data = await client.query(
|
|
96
|
-
|
|
103
|
+
SHOP_SETTINGS_QUERY,
|
|
97
104
|
void 0,
|
|
98
105
|
{ signal: options.signal }
|
|
99
106
|
);
|
|
100
|
-
return
|
|
107
|
+
return {
|
|
108
|
+
customCss: data.shop?.customCss?.value ?? null,
|
|
109
|
+
outOfStockBehavior: parseOutOfStockBehavior(
|
|
110
|
+
data.shop?.outOfStockBehavior?.value
|
|
111
|
+
)
|
|
112
|
+
};
|
|
101
113
|
})();
|
|
102
114
|
cache.set(options.shopDomain, promise);
|
|
103
115
|
promise.catch(() => {
|
|
@@ -107,6 +119,13 @@ async function fetchShopCustomCss(options) {
|
|
|
107
119
|
});
|
|
108
120
|
return promise;
|
|
109
121
|
}
|
|
122
|
+
async function fetchShopSettingsOrDefault(options) {
|
|
123
|
+
try {
|
|
124
|
+
return await fetchShopSettings(options);
|
|
125
|
+
} catch {
|
|
126
|
+
return FALLBACK;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
110
129
|
|
|
111
130
|
// src/hooks/useBundleData.ts
|
|
112
131
|
var INITIAL_STATE = {
|
|
@@ -139,13 +158,13 @@ function useBundleData(options) {
|
|
|
139
158
|
error: err instanceof Error ? err : new Error(String(err))
|
|
140
159
|
});
|
|
141
160
|
});
|
|
142
|
-
|
|
161
|
+
fetchShopSettings({
|
|
143
162
|
shopDomain: options.shopDomain,
|
|
144
163
|
storefrontAccessToken: options.storefrontAccessToken,
|
|
145
164
|
signal: controller.signal
|
|
146
|
-
}).then((
|
|
165
|
+
}).then((settings) => {
|
|
147
166
|
if (controller.signal.aborted) return;
|
|
148
|
-
injectCustomCss(options.shopDomain,
|
|
167
|
+
injectCustomCss(options.shopDomain, settings.customCss);
|
|
149
168
|
}).catch(() => {
|
|
150
169
|
});
|
|
151
170
|
return () => {
|
|
@@ -192,8 +211,7 @@ function useAnalytics(options) {
|
|
|
192
211
|
impressionFiredRef.current = true;
|
|
193
212
|
reportImpression(config, {
|
|
194
213
|
bundleGid: options.bundleGid,
|
|
195
|
-
bundleType: options.bundleType
|
|
196
|
-
linkGroupId: options.linkGroupId
|
|
214
|
+
bundleType: options.bundleType
|
|
197
215
|
});
|
|
198
216
|
});
|
|
199
217
|
return cleanup;
|
|
@@ -202,8 +220,7 @@ function useAnalytics(options) {
|
|
|
202
220
|
config,
|
|
203
221
|
options.enabled,
|
|
204
222
|
options.bundleGid,
|
|
205
|
-
options.bundleType
|
|
206
|
-
options.linkGroupId
|
|
223
|
+
options.bundleType
|
|
207
224
|
]);
|
|
208
225
|
const trackAddToCart = useCallback(
|
|
209
226
|
(event) => {
|
|
@@ -213,16 +230,14 @@ function useAnalytics(options) {
|
|
|
213
230
|
bundleType: options.bundleType,
|
|
214
231
|
productId: event.productId,
|
|
215
232
|
quantity: event.quantity,
|
|
216
|
-
totalPrice: event.totalPrice
|
|
217
|
-
linkGroupId: options.linkGroupId
|
|
233
|
+
totalPrice: event.totalPrice
|
|
218
234
|
});
|
|
219
235
|
},
|
|
220
236
|
[
|
|
221
237
|
config,
|
|
222
238
|
options.enabled,
|
|
223
239
|
options.bundleGid,
|
|
224
|
-
options.bundleType
|
|
225
|
-
options.linkGroupId
|
|
240
|
+
options.bundleType
|
|
226
241
|
]
|
|
227
242
|
);
|
|
228
243
|
return { elementRef, trackAddToCart };
|
|
@@ -326,13 +341,58 @@ function useVariantSelection({
|
|
|
326
341
|
};
|
|
327
342
|
}
|
|
328
343
|
|
|
344
|
+
// src/hooks/useEdgeFade.ts
|
|
345
|
+
import { useCallback as useCallback4, useRef as useRef3 } from "react";
|
|
346
|
+
function attachEdgeFade(el) {
|
|
347
|
+
function update() {
|
|
348
|
+
const scrollable = el.scrollHeight - el.clientHeight;
|
|
349
|
+
if (scrollable <= 1) {
|
|
350
|
+
el.removeAttribute("data-edge-fade");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const atTop = el.scrollTop <= 1;
|
|
354
|
+
const atBottom = el.scrollTop >= scrollable - 1;
|
|
355
|
+
el.setAttribute(
|
|
356
|
+
"data-edge-fade",
|
|
357
|
+
atTop ? "bottom" : atBottom ? "top" : "both"
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
361
|
+
let resizeObserver = null;
|
|
362
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
363
|
+
resizeObserver = new ResizeObserver(update);
|
|
364
|
+
resizeObserver.observe(el);
|
|
365
|
+
}
|
|
366
|
+
let mutationObserver = null;
|
|
367
|
+
if (typeof MutationObserver !== "undefined") {
|
|
368
|
+
mutationObserver = new MutationObserver(update);
|
|
369
|
+
mutationObserver.observe(el, { childList: true, subtree: true });
|
|
370
|
+
}
|
|
371
|
+
update();
|
|
372
|
+
return () => {
|
|
373
|
+
el.removeEventListener("scroll", update);
|
|
374
|
+
resizeObserver?.disconnect();
|
|
375
|
+
mutationObserver?.disconnect();
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function useEdgeFade() {
|
|
379
|
+
const cleanupRef = useRef3(null);
|
|
380
|
+
return useCallback4((el) => {
|
|
381
|
+
cleanupRef.current?.();
|
|
382
|
+
cleanupRef.current = null;
|
|
383
|
+
if (el) {
|
|
384
|
+
cleanupRef.current = attachEdgeFade(el);
|
|
385
|
+
}
|
|
386
|
+
}, []);
|
|
387
|
+
}
|
|
388
|
+
|
|
329
389
|
// src/components/VariantDropdown.tsx
|
|
330
390
|
import {
|
|
331
391
|
useEffect as useEffect5,
|
|
332
392
|
useId,
|
|
333
393
|
useLayoutEffect,
|
|
334
394
|
useMemo as useMemo3,
|
|
335
|
-
useRef as
|
|
395
|
+
useRef as useRef4,
|
|
336
396
|
useState as useState4
|
|
337
397
|
} from "react";
|
|
338
398
|
import { dropdown } from "@lime-bundles/core";
|
|
@@ -363,9 +423,9 @@ function VariantDropdown({
|
|
|
363
423
|
className
|
|
364
424
|
}) {
|
|
365
425
|
const idBase = useId();
|
|
366
|
-
const triggerRef =
|
|
367
|
-
const listboxRef =
|
|
368
|
-
const typeAheadRef =
|
|
426
|
+
const triggerRef = useRef4(null);
|
|
427
|
+
const listboxRef = useRef4(null);
|
|
428
|
+
const typeAheadRef = useRef4(emptyTypeAheadState());
|
|
369
429
|
const [isOpen, setIsOpen] = useState4(false);
|
|
370
430
|
const [activeIndex, setActiveIndex] = useState4(-1);
|
|
371
431
|
const [position, setPosition] = useState4(null);
|
|
@@ -627,7 +687,7 @@ function FixedBundle(props) {
|
|
|
627
687
|
const [addingToCart, setAddingToCart] = useState5(false);
|
|
628
688
|
const [cartError, setCartError] = useState5(null);
|
|
629
689
|
const [selectedVariants, setSelectedVariants] = useState5({});
|
|
630
|
-
const handleVariantChange =
|
|
690
|
+
const handleVariantChange = useCallback5(
|
|
631
691
|
(productId, variant) => {
|
|
632
692
|
setSelectedVariants((prev) => {
|
|
633
693
|
if (prev[productId] === variant) return prev;
|
|
@@ -638,6 +698,7 @@ function FixedBundle(props) {
|
|
|
638
698
|
);
|
|
639
699
|
const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
|
|
640
700
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
701
|
+
const productsEdgeFadeRef = useEdgeFade();
|
|
641
702
|
useEffect6(() => {
|
|
642
703
|
if (result.status === "error") {
|
|
643
704
|
onError?.(result.error);
|
|
@@ -651,7 +712,17 @@ function FixedBundle(props) {
|
|
|
651
712
|
);
|
|
652
713
|
}
|
|
653
714
|
}, [result, onError]);
|
|
654
|
-
const
|
|
715
|
+
const oosProductIds = useMemo4(() => {
|
|
716
|
+
if (!bundle) return /* @__PURE__ */ new Set();
|
|
717
|
+
return new Set(
|
|
718
|
+
bundle.products.filter(
|
|
719
|
+
(p) => !p.variants.nodes.some(
|
|
720
|
+
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, p.id, v.id))
|
|
721
|
+
)
|
|
722
|
+
).map((p) => p.id)
|
|
723
|
+
);
|
|
724
|
+
}, [bundle]);
|
|
725
|
+
const handleAddToCart = useCallback5(async () => {
|
|
655
726
|
if (!bundle) return;
|
|
656
727
|
const bundleLines = bundle.products.filter(
|
|
657
728
|
(p) => p.variants.nodes.some(
|
|
@@ -714,6 +785,27 @@ function FixedBundle(props) {
|
|
|
714
785
|
if (result.status === "error") return null;
|
|
715
786
|
if (!bundle) return null;
|
|
716
787
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
788
|
+
let comparePrice = 0;
|
|
789
|
+
let salePrice = 0;
|
|
790
|
+
for (const product of bundle.products) {
|
|
791
|
+
const variants = product.variants.nodes;
|
|
792
|
+
const variant = selectedVariants[product.id] ?? variants.find(
|
|
793
|
+
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id))
|
|
794
|
+
) ?? variants[0];
|
|
795
|
+
if (!variant) continue;
|
|
796
|
+
const qty = resolveBundleQty(bundle, product.id, variant.id);
|
|
797
|
+
const unit = parseFloat(variant.price.amount);
|
|
798
|
+
comparePrice += unit * qty;
|
|
799
|
+
salePrice += calculateDiscount(
|
|
800
|
+
unit,
|
|
801
|
+
bundle.discountConfig.discountType,
|
|
802
|
+
bundle.discountConfig.discountValue
|
|
803
|
+
) * qty;
|
|
804
|
+
}
|
|
805
|
+
const savings = Math.max(0, comparePrice - salePrice);
|
|
806
|
+
const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
|
|
807
|
+
const showCompare = bundle.widgetConfig.pricing.showComparePrice && savings > 0;
|
|
808
|
+
const showSavings = bundle.widgetConfig.savingsBar.visible && savings > 0;
|
|
717
809
|
return /* @__PURE__ */ jsxs2(
|
|
718
810
|
"div",
|
|
719
811
|
{
|
|
@@ -725,34 +817,58 @@ function FixedBundle(props) {
|
|
|
725
817
|
role: "region",
|
|
726
818
|
"aria-label": bundle.title,
|
|
727
819
|
children: [
|
|
728
|
-
/* @__PURE__ */ jsx2("
|
|
729
|
-
|
|
730
|
-
|
|
820
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-bundle-header", children: /* @__PURE__ */ jsxs2("div", { className: "lb-bundle-header__content", children: [
|
|
821
|
+
/* @__PURE__ */ jsx2("h3", { className: "lb-bundle-title", children: bundle.title }),
|
|
822
|
+
bundle.description && /* @__PURE__ */ jsx2("p", { className: "lb-bundle-subtitle", children: bundle.description })
|
|
823
|
+
] }) }),
|
|
824
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-bundle__products lb-edge-fade", ref: productsEdgeFadeRef, children: bundle.products.map((product) => /* @__PURE__ */ jsx2(
|
|
731
825
|
FixedProductRow,
|
|
732
826
|
{
|
|
733
827
|
bundle,
|
|
734
828
|
product,
|
|
735
829
|
currency,
|
|
830
|
+
isOos: oosProductIds.has(product.id),
|
|
736
831
|
onVariantChange: handleVariantChange
|
|
737
832
|
},
|
|
738
833
|
product.id
|
|
739
834
|
)) }),
|
|
835
|
+
/* @__PURE__ */ jsx2("div", { className: "lb-bundle-divider" }),
|
|
836
|
+
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle-summary", children: [
|
|
837
|
+
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle-summary__text", children: [
|
|
838
|
+
/* @__PURE__ */ jsx2("span", { className: "lb-bundle-summary__label", children: "Bundle price" }),
|
|
839
|
+
showSavings && /* @__PURE__ */ jsxs2("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
|
|
840
|
+
"You save",
|
|
841
|
+
" ",
|
|
842
|
+
/* @__PURE__ */ jsx2("span", { "data-savings-amount": true, children: formatMoney(savings, currency) }),
|
|
843
|
+
" ",
|
|
844
|
+
/* @__PURE__ */ jsxs2("span", { "data-savings-percent": true, children: [
|
|
845
|
+
"(",
|
|
846
|
+
savingsPercent,
|
|
847
|
+
"%)"
|
|
848
|
+
] })
|
|
849
|
+
] })
|
|
850
|
+
] }),
|
|
851
|
+
/* @__PURE__ */ jsxs2("span", { className: "lb-bundle-summary__prices", children: [
|
|
852
|
+
showCompare && /* @__PURE__ */ jsx2("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: formatMoney(comparePrice, currency) }),
|
|
853
|
+
/* @__PURE__ */ jsx2("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: formatMoney(salePrice, currency) })
|
|
854
|
+
] })
|
|
855
|
+
] }),
|
|
740
856
|
cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
741
857
|
/* @__PURE__ */ jsx2(
|
|
742
858
|
"button",
|
|
743
859
|
{
|
|
744
860
|
className: "lb-bundle__cta",
|
|
745
861
|
onClick: handleAddToCart,
|
|
746
|
-
disabled: addingToCart,
|
|
862
|
+
disabled: addingToCart || oosProductIds.size > 0,
|
|
747
863
|
"aria-busy": addingToCart,
|
|
748
|
-
children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? "Add Bundle to Cart"
|
|
864
|
+
children: addingToCart ? "Adding..." : oosProductIds.size > 0 ? `${oosProductIds.size} item${oosProductIds.size === 1 ? "" : "s"} unavailable` : bundle.widgetConfig.cta.ctaText ?? "Add Bundle to Cart"
|
|
749
865
|
}
|
|
750
866
|
)
|
|
751
867
|
]
|
|
752
868
|
}
|
|
753
869
|
);
|
|
754
870
|
}
|
|
755
|
-
function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
871
|
+
function FixedProductRow({ bundle, product, currency, isOos, onVariantChange }) {
|
|
756
872
|
const variants = product.variants.nodes;
|
|
757
873
|
const optionNames = useMemo4(() => {
|
|
758
874
|
const first = variants[0];
|
|
@@ -770,11 +886,13 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
|
770
886
|
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id))
|
|
771
887
|
) ?? variants[0];
|
|
772
888
|
const priceText = displayVariant ? formatMoney(displayVariant.price.amount, currency) : formatMoney(product.priceRange.minVariantPrice.amount, currency);
|
|
889
|
+
const compareAt = displayVariant?.compareAtPrice && parseFloat(displayVariant.compareAtPrice.amount) > parseFloat(displayVariant.price.amount) ? formatMoney(displayVariant.compareAtPrice.amount, currency) : null;
|
|
773
890
|
const unitPriceText = displayVariant ? formatUnitPrice(
|
|
774
891
|
displayVariant.unitPrice,
|
|
775
892
|
displayVariant.unitPriceMeasurement,
|
|
776
893
|
currency
|
|
777
894
|
) : null;
|
|
895
|
+
const displayQty = displayVariant ? resolveBundleQty(bundle, product.id, displayVariant.id) : 1;
|
|
778
896
|
const thumbImage = displayVariant?.image ?? product.featuredImage;
|
|
779
897
|
const showLowStock = !!displayVariant && shouldShowLowStockBadge(
|
|
780
898
|
displayVariant,
|
|
@@ -782,304 +900,15 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
|
782
900
|
bundle.widgetConfig.lowStockThreshold,
|
|
783
901
|
bundle.widgetConfig.showLowStockBadge
|
|
784
902
|
);
|
|
785
|
-
return /* @__PURE__ */ jsxs2(
|
|
786
|
-
thumbImage && /* @__PURE__ */ jsx2(
|
|
787
|
-
"img",
|
|
788
|
-
{
|
|
789
|
-
src: thumbImage.url,
|
|
790
|
-
alt: thumbImage.altText ?? product.title,
|
|
791
|
-
className: "lb-bundle__product-image",
|
|
792
|
-
loading: "lazy"
|
|
793
|
-
}
|
|
794
|
-
),
|
|
795
|
-
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
|
|
796
|
-
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
797
|
-
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: priceText }),
|
|
798
|
-
unitPriceText && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
799
|
-
showLowStock && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
800
|
-
"Only ",
|
|
801
|
-
displayVariant.quantityAvailable,
|
|
802
|
-
" left"
|
|
803
|
-
] }),
|
|
804
|
-
showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
805
|
-
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
806
|
-
value: o.value,
|
|
807
|
-
label: o.value,
|
|
808
|
-
disabled: o.disabled
|
|
809
|
-
}));
|
|
810
|
-
return /* @__PURE__ */ jsx2(
|
|
811
|
-
VariantDropdown,
|
|
812
|
-
{
|
|
813
|
-
options: dropdownOptions,
|
|
814
|
-
value: selectedValues[optionIndex] ?? null,
|
|
815
|
-
onChange: (v) => setOptionValue(optionIndex, v),
|
|
816
|
-
ariaLabel: optionName
|
|
817
|
-
},
|
|
818
|
-
optionName
|
|
819
|
-
);
|
|
820
|
-
}) })
|
|
821
|
-
] })
|
|
822
|
-
] });
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// src/components/MixMatchBundle.tsx
|
|
826
|
-
import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo5, useState as useState6 } from "react";
|
|
827
|
-
import {
|
|
828
|
-
formatMoney as formatMoney2,
|
|
829
|
-
formatUnitPrice as formatUnitPrice2,
|
|
830
|
-
isVariantFulfillable as isVariantFulfillable2,
|
|
831
|
-
shouldShowLowStockBadge as shouldShowLowStockBadge2,
|
|
832
|
-
maxAddableQuantity,
|
|
833
|
-
DEFAULT_PRODUCT_RULE
|
|
834
|
-
} from "@lime-bundles/core";
|
|
835
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
836
|
-
function ruleFor(bundle, productId) {
|
|
837
|
-
return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;
|
|
838
|
-
}
|
|
839
|
-
function MixMatchBundle(props) {
|
|
840
|
-
const {
|
|
841
|
-
shopDomain,
|
|
842
|
-
storefrontAccessToken,
|
|
843
|
-
bundleGid,
|
|
844
|
-
appUrl,
|
|
845
|
-
analyticsEnabled,
|
|
846
|
-
onAddToCart,
|
|
847
|
-
onError,
|
|
848
|
-
className
|
|
849
|
-
} = props;
|
|
850
|
-
const result = useBundleData({
|
|
851
|
-
shopDomain,
|
|
852
|
-
storefrontAccessToken,
|
|
853
|
-
bundleGid
|
|
854
|
-
});
|
|
855
|
-
const { elementRef, trackAddToCart } = useAnalytics({
|
|
856
|
-
shopDomain,
|
|
857
|
-
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
858
|
-
bundleGid,
|
|
859
|
-
bundleType: "mix_match",
|
|
860
|
-
enabled: analyticsEnabled !== false
|
|
861
|
-
});
|
|
862
|
-
const [selections, setSelections] = useState6(
|
|
863
|
-
/* @__PURE__ */ new Map()
|
|
864
|
-
);
|
|
865
|
-
const [addingToCart, setAddingToCart] = useState6(false);
|
|
866
|
-
const [cartError, setCartError] = useState6(null);
|
|
867
|
-
const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
|
|
868
|
-
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
869
|
-
useEffect7(() => {
|
|
870
|
-
if (result.status === "error") {
|
|
871
|
-
onError?.(result.error);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
|
|
875
|
-
onError?.(
|
|
876
|
-
new Error(
|
|
877
|
-
`MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
|
|
878
|
-
)
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
}, [result, onError]);
|
|
882
|
-
const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
|
|
883
|
-
const distinctSelectedProducts = useMemo5(() => {
|
|
884
|
-
const ids = /* @__PURE__ */ new Set();
|
|
885
|
-
for (const sel of selections.values()) {
|
|
886
|
-
if (sel.quantity > 0) ids.add(sel.productId);
|
|
887
|
-
}
|
|
888
|
-
return ids;
|
|
889
|
-
}, [selections]);
|
|
890
|
-
const totalQuantity = useMemo5(
|
|
891
|
-
() => Array.from(selections.values()).reduce((sum, s) => sum + s.quantity, 0),
|
|
892
|
-
[selections]
|
|
893
|
-
);
|
|
894
|
-
const requiredPicks = bundle?.minQuantity ?? 0;
|
|
895
|
-
const meetsMinPicks = distinctSelectedProducts.size >= requiredPicks;
|
|
896
|
-
const valid = !!bundle && meetsMinPicks;
|
|
897
|
-
const remaining = Math.max(0, requiredPicks - distinctSelectedProducts.size);
|
|
898
|
-
const validationMessage = valid ? null : remaining > 0 ? `Pick ${remaining} more product${remaining === 1 ? "" : "s"}` : null;
|
|
899
|
-
const selectVariant = useCallback5(
|
|
900
|
-
(productId, variant) => {
|
|
901
|
-
if (!bundle) return;
|
|
902
|
-
const rule = ruleFor(bundle, productId);
|
|
903
|
-
const maxAddable = maxAddableQuantity(variant, rule.max, 0);
|
|
904
|
-
if (maxAddable < rule.min) return;
|
|
905
|
-
setSelections((prev) => {
|
|
906
|
-
const next = new Map(prev);
|
|
907
|
-
next.set(`${productId}:${variant.id}`, {
|
|
908
|
-
productId,
|
|
909
|
-
variantId: variant.id,
|
|
910
|
-
quantity: rule.min
|
|
911
|
-
});
|
|
912
|
-
return next;
|
|
913
|
-
});
|
|
914
|
-
},
|
|
915
|
-
[bundle]
|
|
916
|
-
);
|
|
917
|
-
const deselect = useCallback5((productId, variantId) => {
|
|
918
|
-
setSelections((prev) => {
|
|
919
|
-
if (!prev.has(`${productId}:${variantId}`)) return prev;
|
|
920
|
-
const next = new Map(prev);
|
|
921
|
-
next.delete(`${productId}:${variantId}`);
|
|
922
|
-
return next;
|
|
923
|
-
});
|
|
924
|
-
}, []);
|
|
925
|
-
const setQuantity = useCallback5(
|
|
926
|
-
(productId, variant, quantity) => {
|
|
927
|
-
if (!bundle) return;
|
|
928
|
-
const rule = ruleFor(bundle, productId);
|
|
929
|
-
const maxAddable = maxAddableQuantity(variant, rule.max, 0);
|
|
930
|
-
const key = `${productId}:${variant.id}`;
|
|
931
|
-
setSelections((prev) => {
|
|
932
|
-
const next = new Map(prev);
|
|
933
|
-
if (quantity < rule.min || maxAddable < rule.min) {
|
|
934
|
-
next.delete(key);
|
|
935
|
-
return next;
|
|
936
|
-
}
|
|
937
|
-
const clamped = Math.min(quantity, maxAddable);
|
|
938
|
-
next.set(key, {
|
|
939
|
-
productId,
|
|
940
|
-
variantId: variant.id,
|
|
941
|
-
quantity: clamped
|
|
942
|
-
});
|
|
943
|
-
return next;
|
|
944
|
-
});
|
|
945
|
-
},
|
|
946
|
-
[bundle]
|
|
947
|
-
);
|
|
948
|
-
const handleAddToCart = useCallback5(async () => {
|
|
949
|
-
if (!bundle || !valid) return;
|
|
950
|
-
const byVariant = /* @__PURE__ */ new Map();
|
|
951
|
-
for (const sel of selections.values()) {
|
|
952
|
-
if (sel.quantity <= 0) continue;
|
|
953
|
-
byVariant.set(
|
|
954
|
-
sel.variantId,
|
|
955
|
-
(byVariant.get(sel.variantId) ?? 0) + sel.quantity
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
const lines = Array.from(byVariant.entries()).map(
|
|
959
|
-
([variantId, quantity]) => ({
|
|
960
|
-
merchandiseId: variantId,
|
|
961
|
-
quantity,
|
|
962
|
-
attributes: [
|
|
963
|
-
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
964
|
-
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
965
|
-
]
|
|
966
|
-
})
|
|
967
|
-
);
|
|
968
|
-
setAddingToCart(true);
|
|
969
|
-
setCartError(null);
|
|
970
|
-
try {
|
|
971
|
-
await onAddToCart(lines);
|
|
972
|
-
const totalPrice = lines.reduce((sum, line) => {
|
|
973
|
-
const product = bundle.products.find(
|
|
974
|
-
(p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
|
|
975
|
-
);
|
|
976
|
-
const variant = product?.variants.nodes.find(
|
|
977
|
-
(v) => v.id === line.merchandiseId
|
|
978
|
-
);
|
|
979
|
-
return sum + parseFloat(variant?.price.amount ?? "0") * line.quantity;
|
|
980
|
-
}, 0);
|
|
981
|
-
trackAddToCart({
|
|
982
|
-
quantity: totalQuantity,
|
|
983
|
-
totalPrice: Math.round(totalPrice * 100) / 100
|
|
984
|
-
});
|
|
985
|
-
setSelections(/* @__PURE__ */ new Map());
|
|
986
|
-
} catch (err) {
|
|
987
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
988
|
-
setCartError(error.message);
|
|
989
|
-
onError?.(error);
|
|
990
|
-
} finally {
|
|
991
|
-
setAddingToCart(false);
|
|
992
|
-
}
|
|
993
|
-
}, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
|
|
994
|
-
if (result.status === "loading") {
|
|
995
|
-
return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
996
|
-
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
997
|
-
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
998
|
-
] });
|
|
999
|
-
}
|
|
1000
|
-
if (result.status === "error") return null;
|
|
1001
|
-
if (!bundle) return null;
|
|
1002
|
-
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
1003
|
-
return /* @__PURE__ */ jsxs3(
|
|
1004
|
-
"div",
|
|
1005
|
-
{
|
|
1006
|
-
ref: (el) => {
|
|
1007
|
-
elementRef(el);
|
|
1008
|
-
setConfigVarsRef(el);
|
|
1009
|
-
},
|
|
1010
|
-
className: `lb-bundle lb-bundle--mix-match ${className ?? ""}`,
|
|
1011
|
-
role: "region",
|
|
1012
|
-
"aria-label": bundle.title,
|
|
1013
|
-
children: [
|
|
1014
|
-
/* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
1015
|
-
bundle.discountLabel && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
|
|
1016
|
-
/* @__PURE__ */ jsx3("p", { className: "lb-bundle__instructions", children: requiredPicks > 0 ? `Pick ${requiredPicks} product${requiredPicks === 1 ? "" : "s"}` : "Pick your products" }),
|
|
1017
|
-
/* @__PURE__ */ jsx3("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ jsx3(
|
|
1018
|
-
MixMatchProductRow,
|
|
1019
|
-
{
|
|
1020
|
-
bundle,
|
|
1021
|
-
product,
|
|
1022
|
-
currency,
|
|
1023
|
-
showStepper,
|
|
1024
|
-
selections,
|
|
1025
|
-
onSelect: selectVariant,
|
|
1026
|
-
onDeselect: deselect,
|
|
1027
|
-
onSetQuantity: setQuantity
|
|
1028
|
-
},
|
|
1029
|
-
product.id
|
|
1030
|
-
)) }),
|
|
1031
|
-
validationMessage && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validationMessage }),
|
|
1032
|
-
cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
1033
|
-
/* @__PURE__ */ jsx3(
|
|
1034
|
-
"button",
|
|
1035
|
-
{
|
|
1036
|
-
className: "lb-bundle__cta",
|
|
1037
|
-
onClick: handleAddToCart,
|
|
1038
|
-
disabled: addingToCart || !valid,
|
|
1039
|
-
"aria-busy": addingToCart,
|
|
1040
|
-
children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
|
|
1041
|
-
}
|
|
1042
|
-
)
|
|
1043
|
-
]
|
|
1044
|
-
}
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1047
|
-
function MixMatchProductRow({
|
|
1048
|
-
bundle,
|
|
1049
|
-
product,
|
|
1050
|
-
currency,
|
|
1051
|
-
showStepper,
|
|
1052
|
-
selections,
|
|
1053
|
-
onSelect,
|
|
1054
|
-
onDeselect,
|
|
1055
|
-
onSetQuantity
|
|
1056
|
-
}) {
|
|
1057
|
-
const variants = product.variants.nodes;
|
|
1058
|
-
const optionNames = useMemo5(() => {
|
|
1059
|
-
const first = variants[0];
|
|
1060
|
-
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
1061
|
-
}, [variants]);
|
|
1062
|
-
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
1063
|
-
const rule = ruleFor(bundle, product.id);
|
|
1064
|
-
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
1065
|
-
const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable2(v, rule.min)) ?? variants[0] ?? null;
|
|
1066
|
-
if (!displayVariant) return null;
|
|
1067
|
-
const fulfillable = isVariantFulfillable2(displayVariant, rule.min);
|
|
1068
|
-
const maxAddable = maxAddableQuantity(displayVariant, rule.max, 0);
|
|
1069
|
-
const key = `${product.id}:${displayVariant.id}`;
|
|
1070
|
-
const selected = selections.get(key);
|
|
1071
|
-
const thumbImage = displayVariant.image ?? product.featuredImage;
|
|
1072
|
-
const unitPriceText = formatUnitPrice2(
|
|
1073
|
-
displayVariant.unitPrice,
|
|
1074
|
-
displayVariant.unitPriceMeasurement,
|
|
1075
|
-
currency
|
|
1076
|
-
);
|
|
1077
|
-
return /* @__PURE__ */ jsxs3(
|
|
903
|
+
return /* @__PURE__ */ jsxs2(
|
|
1078
904
|
"div",
|
|
1079
905
|
{
|
|
1080
|
-
className:
|
|
906
|
+
className: isOos ? "lb-bundle-product-row lb-bundle-product-row--oos" : "lb-bundle-product-row",
|
|
907
|
+
"aria-disabled": isOos || void 0,
|
|
908
|
+
part: "product",
|
|
1081
909
|
children: [
|
|
1082
|
-
|
|
910
|
+
isOos && /* @__PURE__ */ jsx2("span", { className: "lb-bundle-product-row__oos", children: "Out of stock" }),
|
|
911
|
+
thumbImage && /* @__PURE__ */ jsx2(
|
|
1083
912
|
"img",
|
|
1084
913
|
{
|
|
1085
914
|
src: thumbImage.url,
|
|
@@ -1088,27 +917,40 @@ function MixMatchProductRow({
|
|
|
1088
917
|
loading: "lazy"
|
|
1089
918
|
}
|
|
1090
919
|
),
|
|
1091
|
-
/* @__PURE__ */
|
|
1092
|
-
/* @__PURE__ */
|
|
1093
|
-
/* @__PURE__ */
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
920
|
+
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
|
|
921
|
+
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
922
|
+
!showPicker && displayVariant && variants.length > 1 && /* @__PURE__ */ jsx2("span", { className: "lb-bundle-variant-badge", children: displayVariant.title }),
|
|
923
|
+
/* @__PURE__ */ jsxs2("span", { className: "lb-bundle-product-prices", children: [
|
|
924
|
+
compareAt && /* @__PURE__ */ jsx2(
|
|
925
|
+
"span",
|
|
926
|
+
{
|
|
927
|
+
className: "lb-bundle-product-compare-price",
|
|
928
|
+
"data-product-compare-price": true,
|
|
929
|
+
children: compareAt
|
|
930
|
+
}
|
|
931
|
+
),
|
|
932
|
+
/* @__PURE__ */ jsx2("span", { className: "lb-bundle__product-price", "data-product-price": true, children: priceText })
|
|
933
|
+
] }),
|
|
934
|
+
unitPriceText && /* @__PURE__ */ jsx2(
|
|
935
|
+
"span",
|
|
936
|
+
{
|
|
937
|
+
className: "lb-bundle__product-unit-price",
|
|
938
|
+
"data-product-unit-price": true,
|
|
939
|
+
children: unitPriceText
|
|
940
|
+
}
|
|
941
|
+
),
|
|
942
|
+
showLowStock && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
1101
943
|
"Only ",
|
|
1102
944
|
displayVariant.quantityAvailable,
|
|
1103
945
|
" left"
|
|
1104
946
|
] }),
|
|
1105
|
-
showPicker &&
|
|
947
|
+
showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
1106
948
|
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1107
949
|
value: o.value,
|
|
1108
950
|
label: o.value,
|
|
1109
951
|
disabled: o.disabled
|
|
1110
952
|
}));
|
|
1111
|
-
return /* @__PURE__ */
|
|
953
|
+
return /* @__PURE__ */ jsx2(
|
|
1112
954
|
VariantDropdown,
|
|
1113
955
|
{
|
|
1114
956
|
options: dropdownOptions,
|
|
@@ -1120,171 +962,2842 @@ function MixMatchProductRow({
|
|
|
1120
962
|
);
|
|
1121
963
|
}) })
|
|
1122
964
|
] }),
|
|
1123
|
-
/* @__PURE__ */
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
"aria-label": `Decrease ${product.title}`,
|
|
1128
|
-
onClick: () => onSetQuantity(
|
|
1129
|
-
product.id,
|
|
1130
|
-
displayVariant,
|
|
1131
|
-
selected.quantity - 1
|
|
1132
|
-
),
|
|
1133
|
-
disabled: selected.quantity <= rule.min,
|
|
1134
|
-
children: "\u2212"
|
|
1135
|
-
}
|
|
1136
|
-
),
|
|
1137
|
-
/* @__PURE__ */ jsx3("span", { "aria-live": "polite", children: selected.quantity }),
|
|
1138
|
-
/* @__PURE__ */ jsx3(
|
|
1139
|
-
"button",
|
|
1140
|
-
{
|
|
1141
|
-
"aria-label": `Increase ${product.title}`,
|
|
1142
|
-
onClick: () => onSetQuantity(
|
|
1143
|
-
product.id,
|
|
1144
|
-
displayVariant,
|
|
1145
|
-
selected.quantity + 1
|
|
1146
|
-
),
|
|
1147
|
-
disabled: selected.quantity >= maxAddable,
|
|
1148
|
-
children: "+"
|
|
1149
|
-
}
|
|
1150
|
-
),
|
|
1151
|
-
/* @__PURE__ */ jsx3(
|
|
1152
|
-
"button",
|
|
1153
|
-
{
|
|
1154
|
-
className: "lb-bundle__remove-btn",
|
|
1155
|
-
"aria-label": `Remove ${product.title}`,
|
|
1156
|
-
onClick: () => onDeselect(product.id, displayVariant.id),
|
|
1157
|
-
children: "Remove"
|
|
1158
|
-
}
|
|
1159
|
-
)
|
|
1160
|
-
] }) : /* @__PURE__ */ jsx3(
|
|
1161
|
-
"button",
|
|
1162
|
-
{
|
|
1163
|
-
className: "lb-bundle__select-btn lb-bundle__select-btn--selected",
|
|
1164
|
-
"aria-label": `Remove ${product.title}`,
|
|
1165
|
-
onClick: () => onDeselect(product.id, displayVariant.id),
|
|
1166
|
-
children: "Selected"
|
|
1167
|
-
}
|
|
1168
|
-
) : /* @__PURE__ */ jsx3(
|
|
1169
|
-
"button",
|
|
1170
|
-
{
|
|
1171
|
-
className: "lb-bundle__select-btn",
|
|
1172
|
-
onClick: () => onSelect(product.id, displayVariant),
|
|
1173
|
-
disabled: !fulfillable || maxAddable <= 0,
|
|
1174
|
-
children: fulfillable && maxAddable > 0 ? "Select" : "Sold out"
|
|
1175
|
-
}
|
|
1176
|
-
) })
|
|
965
|
+
displayVariant && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-qty-chip", "data-qty-inline": true, children: [
|
|
966
|
+
"\xD7",
|
|
967
|
+
displayQty
|
|
968
|
+
] })
|
|
1177
969
|
]
|
|
1178
970
|
}
|
|
1179
971
|
);
|
|
1180
972
|
}
|
|
1181
973
|
|
|
1182
|
-
// src/components/
|
|
1183
|
-
import { useCallback as useCallback6, useEffect as
|
|
974
|
+
// src/components/MixMatchBundle.tsx
|
|
975
|
+
import { useCallback as useCallback6, useEffect as useEffect11, useMemo as useMemo6, useState as useState8 } from "react";
|
|
1184
976
|
import {
|
|
1185
977
|
formatMoney as formatMoney3,
|
|
1186
978
|
formatUnitPrice as formatUnitPrice3,
|
|
1187
|
-
|
|
1188
|
-
getActiveTier,
|
|
979
|
+
calculateDiscount as calculateDiscount2,
|
|
1189
980
|
isVariantFulfillable as isVariantFulfillable3,
|
|
1190
|
-
|
|
981
|
+
DEFAULT_PRODUCT_RULE as DEFAULT_PRODUCT_RULE2
|
|
1191
982
|
} from "@lime-bundles/core";
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
983
|
+
|
|
984
|
+
// src/hooks/useShopSettings.ts
|
|
985
|
+
import { useEffect as useEffect7, useState as useState6 } from "react";
|
|
986
|
+
import {
|
|
987
|
+
DEFAULT_OUT_OF_STOCK_BEHAVIOR as DEFAULT_OUT_OF_STOCK_BEHAVIOR2
|
|
988
|
+
} from "@lime-bundles/core";
|
|
989
|
+
function useShopSettings(options) {
|
|
990
|
+
const [outOfStockBehavior, setOutOfStockBehavior] = useState6(DEFAULT_OUT_OF_STOCK_BEHAVIOR2);
|
|
991
|
+
const { shopDomain, storefrontAccessToken } = options;
|
|
992
|
+
useEffect7(() => {
|
|
993
|
+
const controller = new AbortController();
|
|
994
|
+
let active = true;
|
|
995
|
+
fetchShopSettingsOrDefault({
|
|
996
|
+
shopDomain,
|
|
997
|
+
storefrontAccessToken,
|
|
998
|
+
signal: controller.signal
|
|
999
|
+
}).then((settings) => {
|
|
1000
|
+
if (!active || controller.signal.aborted) return;
|
|
1001
|
+
setOutOfStockBehavior(settings.outOfStockBehavior);
|
|
1002
|
+
});
|
|
1003
|
+
return () => {
|
|
1004
|
+
active = false;
|
|
1005
|
+
controller.abort();
|
|
1006
|
+
};
|
|
1007
|
+
}, [shopDomain, storefrontAccessToken]);
|
|
1008
|
+
return { outOfStockBehavior };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/components/MixMatchPicker.tsx
|
|
1012
|
+
import {
|
|
1013
|
+
useEffect as useEffect10,
|
|
1014
|
+
useMemo as useMemo5,
|
|
1015
|
+
useRef as useRef6,
|
|
1016
|
+
useState as useState7
|
|
1017
|
+
} from "react";
|
|
1018
|
+
import { createPortal } from "react-dom";
|
|
1019
|
+
import {
|
|
1020
|
+
formatMoney as formatMoney2,
|
|
1021
|
+
formatUnitPrice as formatUnitPrice2,
|
|
1022
|
+
isVariantFulfillable as isVariantFulfillable2,
|
|
1023
|
+
maxAddableQuantity,
|
|
1024
|
+
DEFAULT_PRODUCT_RULE
|
|
1025
|
+
} from "@lime-bundles/core";
|
|
1026
|
+
|
|
1027
|
+
// src/hooks/useScrollLock.ts
|
|
1028
|
+
import { useEffect as useEffect8 } from "react";
|
|
1029
|
+
var lockCount = 0;
|
|
1030
|
+
var savedY = 0;
|
|
1031
|
+
function lock() {
|
|
1032
|
+
if (typeof document === "undefined") return;
|
|
1033
|
+
lockCount += 1;
|
|
1034
|
+
if (lockCount > 1) return;
|
|
1035
|
+
savedY = window.pageYOffset || document.documentElement.scrollTop;
|
|
1036
|
+
const body = document.body;
|
|
1037
|
+
body.style.position = "fixed";
|
|
1038
|
+
body.style.top = `-${savedY}px`;
|
|
1039
|
+
body.style.left = "0";
|
|
1040
|
+
body.style.right = "0";
|
|
1041
|
+
body.style.overflow = "hidden";
|
|
1042
|
+
}
|
|
1043
|
+
function unlock() {
|
|
1044
|
+
if (typeof document === "undefined") return;
|
|
1045
|
+
if (lockCount <= 0) return;
|
|
1046
|
+
lockCount -= 1;
|
|
1047
|
+
if (lockCount > 0) return;
|
|
1048
|
+
const body = document.body;
|
|
1049
|
+
body.style.position = "";
|
|
1050
|
+
body.style.top = "";
|
|
1051
|
+
body.style.left = "";
|
|
1052
|
+
body.style.right = "";
|
|
1053
|
+
body.style.overflow = "";
|
|
1054
|
+
window.scrollTo(0, savedY);
|
|
1055
|
+
}
|
|
1056
|
+
function useScrollLock(active) {
|
|
1057
|
+
useEffect8(() => {
|
|
1058
|
+
if (!active) return;
|
|
1059
|
+
lock();
|
|
1060
|
+
return unlock;
|
|
1061
|
+
}, [active]);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/hooks/useFocusTrap.ts
|
|
1065
|
+
import { useEffect as useEffect9, useRef as useRef5 } from "react";
|
|
1066
|
+
var FOCUSABLE_SELECTOR = [
|
|
1067
|
+
"a[href]",
|
|
1068
|
+
"button:not([disabled])",
|
|
1069
|
+
"input:not([disabled])",
|
|
1070
|
+
"select:not([disabled])",
|
|
1071
|
+
"textarea:not([disabled])",
|
|
1072
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
1073
|
+
].join(",");
|
|
1074
|
+
function getFocusable(container) {
|
|
1075
|
+
const nodes = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
1076
|
+
const result = [];
|
|
1077
|
+
nodes.forEach((node) => {
|
|
1078
|
+
if (node.offsetParent !== null || typeof node.offsetParent === "undefined") {
|
|
1079
|
+
result.push(node);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
return result;
|
|
1083
|
+
}
|
|
1084
|
+
function useFocusTrap({
|
|
1085
|
+
active,
|
|
1086
|
+
containerRef,
|
|
1087
|
+
onEscape,
|
|
1088
|
+
returnFocusRef
|
|
1089
|
+
}) {
|
|
1090
|
+
const previouslyFocused = useRef5(null);
|
|
1091
|
+
useEffect9(() => {
|
|
1092
|
+
if (!active) return;
|
|
1093
|
+
const container = containerRef.current;
|
|
1094
|
+
previouslyFocused.current = document.activeElement ?? null;
|
|
1095
|
+
container?.focus();
|
|
1096
|
+
function onKeyDown(e) {
|
|
1097
|
+
if (e.key === "Escape") {
|
|
1098
|
+
e.preventDefault();
|
|
1099
|
+
onEscape();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (e.key !== "Tab") return;
|
|
1103
|
+
const node = containerRef.current;
|
|
1104
|
+
if (!node) return;
|
|
1105
|
+
const focusable = getFocusable(node);
|
|
1106
|
+
if (focusable.length === 0) {
|
|
1107
|
+
e.preventDefault();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const first = focusable[0];
|
|
1111
|
+
const last = focusable[focusable.length - 1];
|
|
1112
|
+
const activeEl = document.activeElement;
|
|
1113
|
+
if (e.shiftKey && activeEl === first) {
|
|
1114
|
+
e.preventDefault();
|
|
1115
|
+
last.focus();
|
|
1116
|
+
} else if (!e.shiftKey && activeEl === last) {
|
|
1117
|
+
e.preventDefault();
|
|
1118
|
+
first.focus();
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1122
|
+
return () => {
|
|
1123
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
1124
|
+
const target = returnFocusRef?.current ?? previouslyFocused.current;
|
|
1125
|
+
if (target && typeof target.focus === "function") {
|
|
1126
|
+
target.focus();
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
}, [active]);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/components/MixMatchPicker.tsx
|
|
1133
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1134
|
+
function ruleFor(bundle, productId) {
|
|
1135
|
+
return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;
|
|
1136
|
+
}
|
|
1137
|
+
function sanitizeId(gid) {
|
|
1138
|
+
return gid.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1139
|
+
}
|
|
1140
|
+
function buildEligibleProducts(bundle, oosBehavior) {
|
|
1141
|
+
const result = [];
|
|
1142
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1143
|
+
for (const product of bundle.products) {
|
|
1144
|
+
if (seen.has(product.id)) continue;
|
|
1145
|
+
seen.add(product.id);
|
|
1146
|
+
const rule = ruleFor(bundle, product.id);
|
|
1147
|
+
const available = product.variants.nodes.filter(
|
|
1148
|
+
(v) => isVariantFulfillable2(v, rule.min)
|
|
1149
|
+
);
|
|
1150
|
+
const isOos = available.length === 0;
|
|
1151
|
+
if (isOos && oosBehavior === "hide") continue;
|
|
1152
|
+
result.push({
|
|
1153
|
+
product,
|
|
1154
|
+
variants: product.variants.nodes,
|
|
1155
|
+
firstAvailableVariant: available[0] ?? null,
|
|
1156
|
+
isOos
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
function normalizeText(str) {
|
|
1162
|
+
return str.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
1163
|
+
}
|
|
1164
|
+
var FILTERS_ALL = "__all__";
|
|
1165
|
+
function MixMatchPicker({
|
|
1166
|
+
outOfStockBehavior,
|
|
1167
|
+
bundle,
|
|
1168
|
+
currency,
|
|
1169
|
+
requiredQty,
|
|
1170
|
+
showStepper,
|
|
1171
|
+
selections,
|
|
1172
|
+
onAdd,
|
|
1173
|
+
onUpdateQuantity,
|
|
1174
|
+
onRemoveVariant,
|
|
1175
|
+
canRemoveVariant,
|
|
1176
|
+
swapUnitsFor,
|
|
1177
|
+
onSwap,
|
|
1178
|
+
onClose,
|
|
1179
|
+
styleVarsRef
|
|
1180
|
+
}) {
|
|
1181
|
+
const wc = bundle.widgetConfig;
|
|
1182
|
+
const overlayRef = useRef6(null);
|
|
1183
|
+
const dialogRef = useRef6(null);
|
|
1184
|
+
const mouseDownTarget = useRef6(null);
|
|
1185
|
+
const [open, setOpen] = useState7(false);
|
|
1186
|
+
const [query, setQuery] = useState7("");
|
|
1187
|
+
const [activeType, setActiveType] = useState7(FILTERS_ALL);
|
|
1188
|
+
const eligible = useMemo5(
|
|
1189
|
+
() => buildEligibleProducts(bundle, outOfStockBehavior),
|
|
1190
|
+
[bundle, outOfStockBehavior]
|
|
1191
|
+
);
|
|
1192
|
+
const productTypes = useMemo5(() => {
|
|
1193
|
+
const types = [];
|
|
1194
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1195
|
+
for (const ep of eligible) {
|
|
1196
|
+
const ty = (ep.product.productType ?? "").trim();
|
|
1197
|
+
if (ty && !seen.has(ty)) {
|
|
1198
|
+
seen.add(ty);
|
|
1199
|
+
types.push(ty);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return types;
|
|
1203
|
+
}, [eligible]);
|
|
1204
|
+
const showFilters = wc.mixMatchShowTypeFilters && productTypes.length >= 2;
|
|
1205
|
+
const totalUnits = selections.reduce((sum, s) => sum + s.quantity, 0);
|
|
1206
|
+
const atCapacity = totalUnits >= requiredQty;
|
|
1207
|
+
const shownUnits = Math.min(totalUnits, requiredQty);
|
|
1208
|
+
useEffect10(() => {
|
|
1209
|
+
const id = requestAnimationFrame(() => setOpen(true));
|
|
1210
|
+
return () => cancelAnimationFrame(id);
|
|
1211
|
+
}, []);
|
|
1212
|
+
useScrollLock(true);
|
|
1213
|
+
useFocusTrap({
|
|
1214
|
+
active: true,
|
|
1215
|
+
containerRef: dialogRef,
|
|
1216
|
+
onEscape: onClose
|
|
1217
|
+
});
|
|
1218
|
+
const remaining = Math.max(0, requiredQty - shownUnits);
|
|
1219
|
+
const subtitle = (() => {
|
|
1220
|
+
if (remaining <= 0) return "Your bundle is complete";
|
|
1221
|
+
const pct = bundle.discountConfig.discountType === "percentage" && bundle.discountConfig.discountValue ? Math.round(bundle.discountConfig.discountValue) : 0;
|
|
1222
|
+
return pct > 0 ? `Choose ${remaining} more to unlock ${pct}% off` : `Choose ${remaining} more to complete your bundle`;
|
|
1223
|
+
})();
|
|
1224
|
+
const filtered = useMemo5(() => {
|
|
1225
|
+
const nq = normalizeText(query.trim());
|
|
1226
|
+
return eligible.filter((ep) => {
|
|
1227
|
+
const matchesQuery = !nq || normalizeText(ep.product.title).includes(nq);
|
|
1228
|
+
const matchesType = activeType === FILTERS_ALL || (ep.product.productType ?? "") === activeType;
|
|
1229
|
+
return matchesQuery && matchesType;
|
|
1230
|
+
});
|
|
1231
|
+
}, [eligible, query, activeType]);
|
|
1232
|
+
const showEmpty = filtered.length === 0 && query.trim().length > 0;
|
|
1233
|
+
function onOverlayMouseDown(e) {
|
|
1234
|
+
mouseDownTarget.current = e.target;
|
|
1235
|
+
}
|
|
1236
|
+
function onOverlayMouseUp(e) {
|
|
1237
|
+
if (e.target === overlayRef.current && mouseDownTarget.current === overlayRef.current) {
|
|
1238
|
+
onClose();
|
|
1239
|
+
}
|
|
1240
|
+
mouseDownTarget.current = null;
|
|
1241
|
+
}
|
|
1242
|
+
const titleId = `lb-modal-title-${sanitizeId(bundle.id)}`;
|
|
1243
|
+
const modal = (
|
|
1244
|
+
// The overlay is a backdrop: click-outside-to-close is a mouse convenience;
|
|
1245
|
+
// keyboard users close via Escape (handled by useFocusTrap), and the dialog
|
|
1246
|
+
// inside carries the interactive role + focus trap. So the static-element
|
|
1247
|
+
// interaction lint does not apply to this backdrop.
|
|
1248
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
1249
|
+
/* @__PURE__ */ jsx3(
|
|
1250
|
+
"div",
|
|
1251
|
+
{
|
|
1252
|
+
ref: (el) => {
|
|
1253
|
+
overlayRef.current = el;
|
|
1254
|
+
styleVarsRef(el);
|
|
1255
|
+
},
|
|
1256
|
+
className: `lb-mix-match__modal-overlay${open ? " lb-mix-match__modal-overlay--open" : ""}`,
|
|
1257
|
+
"data-modal-overlay": true,
|
|
1258
|
+
"data-bundle-gid": bundle.id,
|
|
1259
|
+
onMouseDown: onOverlayMouseDown,
|
|
1260
|
+
onMouseUp: onOverlayMouseUp,
|
|
1261
|
+
children: /* @__PURE__ */ jsxs3(
|
|
1262
|
+
"div",
|
|
1263
|
+
{
|
|
1264
|
+
ref: dialogRef,
|
|
1265
|
+
className: `lb-mix-match__modal${showFilters ? "" : " lb-mix-match__modal--filters-hidden"}`,
|
|
1266
|
+
role: "dialog",
|
|
1267
|
+
"aria-modal": "true",
|
|
1268
|
+
"aria-labelledby": titleId,
|
|
1269
|
+
tabIndex: -1,
|
|
1270
|
+
children: [
|
|
1271
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-header", children: [
|
|
1272
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-header-top", children: [
|
|
1273
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-heading", children: [
|
|
1274
|
+
/* @__PURE__ */ jsx3("h4", { className: "lb-mix-match__modal-title", id: titleId, children: "Add to your bundle" }),
|
|
1275
|
+
/* @__PURE__ */ jsx3("p", { className: "lb-mix-match__modal-subtitle", "data-modal-subtitle": true, children: subtitle })
|
|
1276
|
+
] }),
|
|
1277
|
+
/* @__PURE__ */ jsx3(
|
|
1278
|
+
"button",
|
|
1279
|
+
{
|
|
1280
|
+
type: "button",
|
|
1281
|
+
className: "lb-mix-match__modal-close",
|
|
1282
|
+
"data-modal-close": true,
|
|
1283
|
+
"aria-label": "Close",
|
|
1284
|
+
onClick: onClose,
|
|
1285
|
+
children: /* @__PURE__ */ jsx3(CloseIcon, {})
|
|
1286
|
+
}
|
|
1287
|
+
)
|
|
1288
|
+
] }),
|
|
1289
|
+
/* @__PURE__ */ jsx3(
|
|
1290
|
+
"div",
|
|
1291
|
+
{
|
|
1292
|
+
className: "lb-mix-match__progress lb-mix-match__modal-progress",
|
|
1293
|
+
"data-progress": true,
|
|
1294
|
+
children: /* @__PURE__ */ jsx3(
|
|
1295
|
+
"div",
|
|
1296
|
+
{
|
|
1297
|
+
className: "lb-mix-match__progress-segments",
|
|
1298
|
+
role: "progressbar",
|
|
1299
|
+
"aria-valuenow": shownUnits,
|
|
1300
|
+
"aria-valuemin": 0,
|
|
1301
|
+
"aria-valuemax": requiredQty,
|
|
1302
|
+
children: Array.from({ length: requiredQty }).map((_, i) => /* @__PURE__ */ jsx3(
|
|
1303
|
+
"span",
|
|
1304
|
+
{
|
|
1305
|
+
className: `lb-mix-match__progress-segment${i < shownUnits ? " lb-mix-match__progress-segment--filled" : ""}`,
|
|
1306
|
+
"data-progress-segment": true
|
|
1307
|
+
},
|
|
1308
|
+
i
|
|
1309
|
+
))
|
|
1310
|
+
}
|
|
1311
|
+
)
|
|
1312
|
+
}
|
|
1313
|
+
)
|
|
1314
|
+
] }),
|
|
1315
|
+
wc.showSearch && /* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-search", children: [
|
|
1316
|
+
/* @__PURE__ */ jsx3(
|
|
1317
|
+
"input",
|
|
1318
|
+
{
|
|
1319
|
+
type: "text",
|
|
1320
|
+
className: "lb-mix-match__modal-search-input",
|
|
1321
|
+
"data-modal-search": true,
|
|
1322
|
+
role: "searchbox",
|
|
1323
|
+
"aria-label": "Search products",
|
|
1324
|
+
placeholder: "Search products",
|
|
1325
|
+
autoComplete: "off",
|
|
1326
|
+
value: query,
|
|
1327
|
+
onChange: (e) => setQuery(e.target.value)
|
|
1328
|
+
}
|
|
1329
|
+
),
|
|
1330
|
+
query.length > 0 && /* @__PURE__ */ jsx3(
|
|
1331
|
+
"button",
|
|
1332
|
+
{
|
|
1333
|
+
type: "button",
|
|
1334
|
+
className: "lb-mix-match__modal-search-clear",
|
|
1335
|
+
"data-modal-search-clear": true,
|
|
1336
|
+
"aria-label": "Clear search",
|
|
1337
|
+
onClick: () => setQuery(""),
|
|
1338
|
+
children: /* @__PURE__ */ jsx3(SearchClearIcon, {})
|
|
1339
|
+
}
|
|
1340
|
+
)
|
|
1341
|
+
] }),
|
|
1342
|
+
showFilters && /* @__PURE__ */ jsx3(
|
|
1343
|
+
"div",
|
|
1344
|
+
{
|
|
1345
|
+
className: "lb-mix-match__filters",
|
|
1346
|
+
"data-modal-filters": true,
|
|
1347
|
+
role: "group",
|
|
1348
|
+
"aria-label": "Filter by product type",
|
|
1349
|
+
children: [FILTERS_ALL, ...productTypes].map((value) => {
|
|
1350
|
+
const isActive = value === activeType;
|
|
1351
|
+
return /* @__PURE__ */ jsx3(
|
|
1352
|
+
"button",
|
|
1353
|
+
{
|
|
1354
|
+
type: "button",
|
|
1355
|
+
className: `lb-mix-match__filter${isActive ? " lb-mix-match__filter--active" : ""}`,
|
|
1356
|
+
"data-filter": value,
|
|
1357
|
+
"aria-pressed": isActive,
|
|
1358
|
+
onClick: () => setActiveType(value),
|
|
1359
|
+
children: value === FILTERS_ALL ? "All" : value
|
|
1360
|
+
},
|
|
1361
|
+
value
|
|
1362
|
+
);
|
|
1363
|
+
})
|
|
1364
|
+
}
|
|
1365
|
+
),
|
|
1366
|
+
/* @__PURE__ */ jsx3("div", { className: "lb-mix-match__modal-list", "data-modal-list": true, children: filtered.map((ep) => /* @__PURE__ */ jsx3(
|
|
1367
|
+
MixMatchPickerRow,
|
|
1368
|
+
{
|
|
1369
|
+
bundle,
|
|
1370
|
+
eligible: ep,
|
|
1371
|
+
currency,
|
|
1372
|
+
showStepper,
|
|
1373
|
+
selections,
|
|
1374
|
+
atCapacity,
|
|
1375
|
+
remainingUnits: remaining,
|
|
1376
|
+
onAdd,
|
|
1377
|
+
onUpdateQuantity,
|
|
1378
|
+
onRemoveVariant,
|
|
1379
|
+
canRemoveVariant,
|
|
1380
|
+
swapUnitsFor,
|
|
1381
|
+
onSwap
|
|
1382
|
+
},
|
|
1383
|
+
ep.product.id
|
|
1384
|
+
)) }),
|
|
1385
|
+
showEmpty && /* @__PURE__ */ jsx3("div", { className: "lb-mix-match__modal-empty", "data-modal-empty": true, children: /* @__PURE__ */ jsx3("p", { children: "No products match your search" }) }),
|
|
1386
|
+
/* @__PURE__ */ jsx3("span", { "data-modal-live": true, "aria-live": "polite", className: "lb-visually-hidden", children: `${filtered.length} products shown` }),
|
|
1387
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-footer", children: [
|
|
1388
|
+
/* @__PURE__ */ jsx3(
|
|
1389
|
+
"span",
|
|
1390
|
+
{
|
|
1391
|
+
className: "lb-mix-match__modal-footer-count",
|
|
1392
|
+
"data-modal-footer-count": true,
|
|
1393
|
+
"aria-live": "polite",
|
|
1394
|
+
children: `${shownUnits} of ${requiredQty} added`
|
|
1395
|
+
}
|
|
1396
|
+
),
|
|
1397
|
+
/* @__PURE__ */ jsx3(
|
|
1398
|
+
"button",
|
|
1399
|
+
{
|
|
1400
|
+
type: "button",
|
|
1401
|
+
className: "lb-mix-match__modal-done",
|
|
1402
|
+
"data-modal-done": true,
|
|
1403
|
+
onClick: onClose,
|
|
1404
|
+
children: "Done"
|
|
1405
|
+
}
|
|
1406
|
+
)
|
|
1407
|
+
] })
|
|
1408
|
+
]
|
|
1409
|
+
}
|
|
1410
|
+
)
|
|
1411
|
+
}
|
|
1412
|
+
)
|
|
1413
|
+
);
|
|
1414
|
+
if (typeof document === "undefined") return null;
|
|
1415
|
+
return createPortal(modal, document.body);
|
|
1416
|
+
}
|
|
1417
|
+
function MixMatchPickerRow({
|
|
1418
|
+
bundle,
|
|
1419
|
+
eligible,
|
|
1420
|
+
currency,
|
|
1421
|
+
showStepper,
|
|
1422
|
+
selections,
|
|
1423
|
+
atCapacity,
|
|
1424
|
+
remainingUnits,
|
|
1425
|
+
onAdd,
|
|
1426
|
+
onUpdateQuantity,
|
|
1427
|
+
onRemoveVariant,
|
|
1428
|
+
canRemoveVariant,
|
|
1429
|
+
swapUnitsFor,
|
|
1430
|
+
onSwap
|
|
1431
|
+
}) {
|
|
1432
|
+
const { product, variants, isOos } = eligible;
|
|
1433
|
+
const rule = ruleFor(bundle, product.id);
|
|
1434
|
+
const optionNames = useMemo5(() => {
|
|
1435
|
+
const first = variants[0];
|
|
1436
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
1437
|
+
}, [variants]);
|
|
1438
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
1439
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
1440
|
+
const currentVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable2(v, rule.min)) ?? eligible.firstAvailableVariant ?? variants[0] ?? null;
|
|
1441
|
+
const alreadyInBundle = useMemo5(() => {
|
|
1442
|
+
if (!currentVariant) return 0;
|
|
1443
|
+
let sum = 0;
|
|
1444
|
+
for (const s of selections) {
|
|
1445
|
+
if (s.productId === product.id && s.variantId === currentVariant.id) {
|
|
1446
|
+
sum += s.quantity;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return sum;
|
|
1450
|
+
}, [selections, product.id, currentVariant]);
|
|
1451
|
+
const committedSelection = useMemo5(
|
|
1452
|
+
() => currentVariant ? selections.find((s) => s.variantId === currentVariant.id) ?? null : null,
|
|
1453
|
+
[selections, currentVariant]
|
|
1454
|
+
);
|
|
1455
|
+
const variantInBundle = committedSelection !== null;
|
|
1456
|
+
const committedQty = committedSelection?.quantity ?? 0;
|
|
1457
|
+
const productOtherUnits = useMemo5(() => {
|
|
1458
|
+
if (!currentVariant) return 0;
|
|
1459
|
+
let sum = 0;
|
|
1460
|
+
for (const s of selections) {
|
|
1461
|
+
if (s.productId === product.id && s.variantId !== currentVariant.id) {
|
|
1462
|
+
sum += s.quantity;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return sum;
|
|
1466
|
+
}, [selections, product.id, currentVariant]);
|
|
1467
|
+
const swapUnits = currentVariant ? swapUnitsFor?.(product.id, currentVariant.id) ?? 0 : 0;
|
|
1468
|
+
const inSwapState = swapUnits >= rule.min;
|
|
1469
|
+
const spotCeiling = inSwapState ? swapUnits : Math.min(rule.max - productOtherUnits, committedQty + remainingUnits);
|
|
1470
|
+
const cap = currentVariant ? maxAddableQuantity(
|
|
1471
|
+
currentVariant,
|
|
1472
|
+
spotCeiling,
|
|
1473
|
+
Math.max(0, alreadyInBundle - committedQty)
|
|
1474
|
+
) : 0;
|
|
1475
|
+
const stepperMax = Math.max(rule.min, cap);
|
|
1476
|
+
const [qty, setQty] = useState7(rule.min);
|
|
1477
|
+
useEffect10(() => {
|
|
1478
|
+
setQty((prev) => Math.max(rule.min, Math.min(prev, stepperMax)));
|
|
1479
|
+
}, [stepperMax, rule.min]);
|
|
1480
|
+
const currentVariantId = currentVariant?.id ?? null;
|
|
1481
|
+
useEffect10(() => {
|
|
1482
|
+
if (!variantInBundle) setQty(rule.min);
|
|
1483
|
+
}, [variantInBundle, currentVariantId, rule.min]);
|
|
1484
|
+
if (!currentVariant) return null;
|
|
1485
|
+
const thumbImage = currentVariant.image ?? product.featuredImage ?? null;
|
|
1486
|
+
const unitPriceText = formatUnitPrice2(
|
|
1487
|
+
currentVariant.unitPrice,
|
|
1488
|
+
currentVariant.unitPriceMeasurement,
|
|
1489
|
+
currency
|
|
1490
|
+
);
|
|
1491
|
+
const noStock = cap < rule.min;
|
|
1492
|
+
const needsMoreSpots = !variantInBundle && !inSwapState && rule.min > remainingUnits;
|
|
1493
|
+
const addDisabled = !variantInBundle && (inSwapState ? noStock : atCapacity || noStock || needsMoreSpots);
|
|
1494
|
+
const lockedRequired = variantInBundle && currentVariant != null && !(canRemoveVariant?.(currentVariant.id) ?? true);
|
|
1495
|
+
const stepperDisabled = isOos || !variantInBundle && addDisabled;
|
|
1496
|
+
const shownQty = variantInBundle ? committedQty : qty;
|
|
1497
|
+
function handleAddOrRemove() {
|
|
1498
|
+
if (variantInBundle && currentVariant) {
|
|
1499
|
+
onRemoveVariant(currentVariant.id);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (!currentVariant || noStock) return;
|
|
1503
|
+
const pickedQty = showStepper ? qty : rule.min;
|
|
1504
|
+
const item = {
|
|
1505
|
+
productId: product.id,
|
|
1506
|
+
variantId: currentVariant.id,
|
|
1507
|
+
title: product.title,
|
|
1508
|
+
url: `/products/${product.handle}`,
|
|
1509
|
+
variantTitle: currentVariant.title,
|
|
1510
|
+
featuredImage: thumbImage?.url ?? null,
|
|
1511
|
+
price: parseFloat(currentVariant.price.amount),
|
|
1512
|
+
compareAtPrice: currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null,
|
|
1513
|
+
unitPrice: unitPriceText,
|
|
1514
|
+
quantity: pickedQty
|
|
1515
|
+
};
|
|
1516
|
+
if (inSwapState && onSwap) {
|
|
1517
|
+
onSwap({ ...item, quantity: Math.min(pickedQty, swapUnits) });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (atCapacity || needsMoreSpots) return;
|
|
1521
|
+
onAdd(item);
|
|
1522
|
+
}
|
|
1523
|
+
const priceCents = parseFloat(currentVariant.price.amount);
|
|
1524
|
+
const compareCents = currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null;
|
|
1525
|
+
return /* @__PURE__ */ jsxs3(
|
|
1526
|
+
"div",
|
|
1527
|
+
{
|
|
1528
|
+
className: `lb-mix-match__modal-product${isOos ? " lb-mix-match__modal-product--sold-out" : ""}${variantInBundle ? " lb-mix-match__modal-product--in-bundle" : ""}`,
|
|
1529
|
+
"data-product-item": true,
|
|
1530
|
+
"data-title": normalizeText(product.title),
|
|
1531
|
+
"data-type": product.productType ?? "",
|
|
1532
|
+
"aria-disabled": isOos || void 0,
|
|
1533
|
+
children: [
|
|
1534
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-product-thumb", children: [
|
|
1535
|
+
thumbImage && /* @__PURE__ */ jsx3(
|
|
1536
|
+
"img",
|
|
1537
|
+
{
|
|
1538
|
+
src: thumbImage.url,
|
|
1539
|
+
alt: thumbImage.altText ?? product.title,
|
|
1540
|
+
loading: "lazy"
|
|
1541
|
+
}
|
|
1542
|
+
),
|
|
1543
|
+
variantInBundle && /* @__PURE__ */ jsx3("span", { className: "lb-mix-match__modal-added-badge", children: "Added" })
|
|
1544
|
+
] }),
|
|
1545
|
+
/* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-product-info", children: [
|
|
1546
|
+
/* @__PURE__ */ jsx3("p", { className: "lb-mix-match__modal-product-title", children: /* @__PURE__ */ jsx3(
|
|
1547
|
+
"a",
|
|
1548
|
+
{
|
|
1549
|
+
href: `/products/${product.handle}`,
|
|
1550
|
+
target: "_blank",
|
|
1551
|
+
rel: "noopener noreferrer",
|
|
1552
|
+
children: product.title
|
|
1553
|
+
}
|
|
1554
|
+
) }),
|
|
1555
|
+
/* @__PURE__ */ jsxs3("p", { className: "lb-mix-match__modal-product-price", children: [
|
|
1556
|
+
/* @__PURE__ */ jsx3("span", { "data-row-price-sale": true, children: formatMoney2(priceCents, currency) }),
|
|
1557
|
+
compareCents !== null && compareCents > priceCents && /* @__PURE__ */ jsx3("s", { className: "lb-mix-match__modal-product-compare", "data-row-compare": true, children: formatMoney2(compareCents, currency) })
|
|
1558
|
+
] }),
|
|
1559
|
+
unitPriceText && /* @__PURE__ */ jsx3("p", { className: "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price", children: unitPriceText }),
|
|
1560
|
+
showPicker && /* @__PURE__ */ jsx3("div", { className: "lb-bundle-variant-option-groups", children: optionNames.map((optionName, optionIndex) => {
|
|
1561
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1562
|
+
value: o.value,
|
|
1563
|
+
label: o.value,
|
|
1564
|
+
disabled: o.disabled
|
|
1565
|
+
}));
|
|
1566
|
+
return /* @__PURE__ */ jsxs3(
|
|
1567
|
+
"div",
|
|
1568
|
+
{
|
|
1569
|
+
className: "lb-bundle-variant-option-group",
|
|
1570
|
+
children: [
|
|
1571
|
+
/* @__PURE__ */ jsx3("span", { className: "lb-bundle-variant-option-label", children: optionName }),
|
|
1572
|
+
/* @__PURE__ */ jsx3(
|
|
1573
|
+
VariantDropdown,
|
|
1574
|
+
{
|
|
1575
|
+
className: "lb-mix-match__variant-select-dropdown",
|
|
1576
|
+
options: dropdownOptions,
|
|
1577
|
+
value: selectedValues[optionIndex] ?? null,
|
|
1578
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
1579
|
+
ariaLabel: optionName
|
|
1580
|
+
}
|
|
1581
|
+
)
|
|
1582
|
+
]
|
|
1583
|
+
},
|
|
1584
|
+
optionName
|
|
1585
|
+
);
|
|
1586
|
+
}) }),
|
|
1587
|
+
!showPicker && variants.length === 1 && currentVariant.title !== "Default Title" && /* @__PURE__ */ jsx3("span", { className: "lb-mix-match__filled-variant", children: currentVariant.title }),
|
|
1588
|
+
!isOos && needsMoreSpots && remainingUnits > 0 && /* @__PURE__ */ jsx3("span", { className: "lb-mix-match__modal-needs-spots", children: rule.min === 1 ? "Needs 1 spot" : `Needs ${rule.min} spots` }),
|
|
1589
|
+
isOos ? /* @__PURE__ */ jsx3("span", { className: "lb-mix-match__modal-sold-out-label", children: "Sold out" }) : /* @__PURE__ */ jsxs3("div", { className: "lb-mix-match__modal-product-actions", children: [
|
|
1590
|
+
showStepper && /* @__PURE__ */ jsx3("div", { className: "lb-bundle-variant-option-group lb-mix-match__qty-stepper-group", children: /* @__PURE__ */ jsxs3(
|
|
1591
|
+
"div",
|
|
1592
|
+
{
|
|
1593
|
+
className: "lb-mix-match__qty-stepper",
|
|
1594
|
+
role: "group",
|
|
1595
|
+
"aria-label": "Quantity",
|
|
1596
|
+
children: [
|
|
1597
|
+
/* @__PURE__ */ jsx3(
|
|
1598
|
+
"button",
|
|
1599
|
+
{
|
|
1600
|
+
type: "button",
|
|
1601
|
+
className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus",
|
|
1602
|
+
"aria-label": "Decrease quantity",
|
|
1603
|
+
disabled: stepperDisabled || shownQty <= rule.min,
|
|
1604
|
+
onClick: () => {
|
|
1605
|
+
if (variantInBundle && currentVariant) {
|
|
1606
|
+
onUpdateQuantity(
|
|
1607
|
+
product.id,
|
|
1608
|
+
currentVariant.id,
|
|
1609
|
+
Math.max(rule.min, committedQty - 1)
|
|
1610
|
+
);
|
|
1611
|
+
} else {
|
|
1612
|
+
setQty((q) => Math.max(rule.min, q - 1));
|
|
1613
|
+
}
|
|
1614
|
+
},
|
|
1615
|
+
children: "\u2212"
|
|
1616
|
+
}
|
|
1617
|
+
),
|
|
1618
|
+
/* @__PURE__ */ jsx3(
|
|
1619
|
+
"span",
|
|
1620
|
+
{
|
|
1621
|
+
className: "lb-mix-match__qty-stepper-value",
|
|
1622
|
+
"data-row-qty": true,
|
|
1623
|
+
"aria-live": "polite",
|
|
1624
|
+
children: shownQty
|
|
1625
|
+
}
|
|
1626
|
+
),
|
|
1627
|
+
/* @__PURE__ */ jsx3(
|
|
1628
|
+
"button",
|
|
1629
|
+
{
|
|
1630
|
+
type: "button",
|
|
1631
|
+
className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus",
|
|
1632
|
+
"aria-label": "Increase quantity",
|
|
1633
|
+
disabled: stepperDisabled || shownQty >= stepperMax,
|
|
1634
|
+
onClick: () => {
|
|
1635
|
+
if (variantInBundle && currentVariant) {
|
|
1636
|
+
onUpdateQuantity(
|
|
1637
|
+
product.id,
|
|
1638
|
+
currentVariant.id,
|
|
1639
|
+
Math.min(stepperMax, committedQty + 1)
|
|
1640
|
+
);
|
|
1641
|
+
} else {
|
|
1642
|
+
setQty((q) => Math.min(stepperMax, q + 1));
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
children: "+"
|
|
1646
|
+
}
|
|
1647
|
+
)
|
|
1648
|
+
]
|
|
1649
|
+
}
|
|
1650
|
+
) }),
|
|
1651
|
+
/* @__PURE__ */ jsx3(
|
|
1652
|
+
"button",
|
|
1653
|
+
{
|
|
1654
|
+
type: "button",
|
|
1655
|
+
className: `lb-mix-match__modal-add${variantInBundle ? " lb-mix-match__modal-add--added" : ""}${lockedRequired ? " lb-mix-match__modal-add--required" : ""}`,
|
|
1656
|
+
"data-add-product": true,
|
|
1657
|
+
"aria-label": variantInBundle ? lockedRequired ? `${product.title} is required and cannot be removed` : `Remove ${product.title}` : `${inSwapState ? "Swap" : "Add"} ${product.title}`,
|
|
1658
|
+
disabled: addDisabled || lockedRequired,
|
|
1659
|
+
onClick: handleAddOrRemove,
|
|
1660
|
+
children: variantInBundle ? lockedRequired ? "Required" : "Remove" : inSwapState ? "Swap" : "Add"
|
|
1661
|
+
}
|
|
1662
|
+
)
|
|
1663
|
+
] })
|
|
1664
|
+
] })
|
|
1665
|
+
]
|
|
1666
|
+
}
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
function CloseIcon() {
|
|
1670
|
+
return /* @__PURE__ */ jsxs3(
|
|
1671
|
+
"svg",
|
|
1672
|
+
{
|
|
1673
|
+
width: "20",
|
|
1674
|
+
height: "20",
|
|
1675
|
+
viewBox: "0 0 20 20",
|
|
1676
|
+
fill: "none",
|
|
1677
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1678
|
+
"aria-hidden": "true",
|
|
1679
|
+
children: [
|
|
1680
|
+
/* @__PURE__ */ jsx3("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
1681
|
+
/* @__PURE__ */ jsx3("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
|
|
1682
|
+
]
|
|
1683
|
+
}
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
function SearchClearIcon() {
|
|
1687
|
+
return /* @__PURE__ */ jsx3(
|
|
1688
|
+
"svg",
|
|
1689
|
+
{
|
|
1690
|
+
width: "16",
|
|
1691
|
+
height: "16",
|
|
1692
|
+
viewBox: "0 0 20 20",
|
|
1693
|
+
fill: "currentColor",
|
|
1694
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1695
|
+
"aria-hidden": "true",
|
|
1696
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z" })
|
|
1697
|
+
}
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// src/components/MixMatchBundle.tsx
|
|
1702
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1703
|
+
function ruleFor2(bundle, productId) {
|
|
1704
|
+
return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE2;
|
|
1705
|
+
}
|
|
1706
|
+
function canRemoveItem(bundle, selections, item) {
|
|
1707
|
+
const rule = ruleFor2(bundle, item.productId);
|
|
1708
|
+
if (!rule.required) return true;
|
|
1709
|
+
const unitsElsewhere = selections.filter((s) => s.productId === item.productId && s !== item).reduce((sum, s) => sum + s.quantity, 0);
|
|
1710
|
+
return unitsElsewhere >= rule.min;
|
|
1711
|
+
}
|
|
1712
|
+
function swapUnitsForItem(bundle, selections, productId, variantId) {
|
|
1713
|
+
const rule = ruleFor2(bundle, productId);
|
|
1714
|
+
if (!rule.required || rule.max !== rule.min) return 0;
|
|
1715
|
+
if (selections.some((s) => s.variantId === variantId)) return 0;
|
|
1716
|
+
return selections.filter((s) => s.productId === productId && s.variantId !== variantId).reduce((sum, s) => sum + s.quantity, 0);
|
|
1717
|
+
}
|
|
1718
|
+
function MixMatchBundle(props) {
|
|
1719
|
+
const {
|
|
1720
|
+
shopDomain,
|
|
1721
|
+
storefrontAccessToken,
|
|
1722
|
+
bundleGid,
|
|
1723
|
+
appUrl,
|
|
1724
|
+
analyticsEnabled,
|
|
1725
|
+
onAddToCart,
|
|
1726
|
+
onError,
|
|
1727
|
+
className
|
|
1728
|
+
} = props;
|
|
1729
|
+
const result = useBundleData({
|
|
1730
|
+
shopDomain,
|
|
1731
|
+
storefrontAccessToken,
|
|
1732
|
+
bundleGid
|
|
1733
|
+
});
|
|
1734
|
+
const { outOfStockBehavior } = useShopSettings({
|
|
1735
|
+
shopDomain,
|
|
1736
|
+
storefrontAccessToken
|
|
1737
|
+
});
|
|
1738
|
+
const { elementRef, trackAddToCart } = useAnalytics({
|
|
1739
|
+
shopDomain,
|
|
1740
|
+
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
1741
|
+
bundleGid,
|
|
1742
|
+
bundleType: "mix_match",
|
|
1743
|
+
enabled: analyticsEnabled !== false
|
|
1744
|
+
});
|
|
1745
|
+
const [selections, setSelections] = useState8([]);
|
|
1746
|
+
const [seededFor, setSeededFor] = useState8(null);
|
|
1747
|
+
const [pickerOpen, setPickerOpen] = useState8(false);
|
|
1748
|
+
const [addingToCart, setAddingToCart] = useState8(false);
|
|
1749
|
+
const [cartError, setCartError] = useState8(null);
|
|
1750
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
|
|
1751
|
+
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
1752
|
+
const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
1753
|
+
const slotsEdgeFadeRef = useEdgeFade();
|
|
1754
|
+
useEffect11(() => {
|
|
1755
|
+
if (result.status === "error") {
|
|
1756
|
+
onError?.(result.error);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
|
|
1760
|
+
onError?.(
|
|
1761
|
+
new Error(
|
|
1762
|
+
`MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
|
|
1763
|
+
)
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
}, [result, onError]);
|
|
1767
|
+
useEffect11(() => {
|
|
1768
|
+
if (!bundle || seededFor === bundle.id) return;
|
|
1769
|
+
setSeededFor(bundle.id);
|
|
1770
|
+
const seedCurrency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
1771
|
+
const seeds = [];
|
|
1772
|
+
for (const [pid, rule] of Object.entries(bundle.productRules)) {
|
|
1773
|
+
if (!rule.required) continue;
|
|
1774
|
+
const product = bundle.products.find((p) => p.id === pid);
|
|
1775
|
+
const variant = product?.variants.nodes.find(
|
|
1776
|
+
(v) => isVariantFulfillable3(v, rule.min)
|
|
1777
|
+
);
|
|
1778
|
+
if (!product || !variant) continue;
|
|
1779
|
+
seeds.push({
|
|
1780
|
+
productId: product.id,
|
|
1781
|
+
variantId: variant.id,
|
|
1782
|
+
title: product.title,
|
|
1783
|
+
url: `/products/${product.handle}`,
|
|
1784
|
+
variantTitle: variant.title,
|
|
1785
|
+
featuredImage: variant.image?.url ?? product.featuredImage?.url ?? null,
|
|
1786
|
+
price: parseFloat(variant.price.amount),
|
|
1787
|
+
compareAtPrice: variant.compareAtPrice ? parseFloat(variant.compareAtPrice.amount) : null,
|
|
1788
|
+
unitPrice: formatUnitPrice3(
|
|
1789
|
+
variant.unitPrice,
|
|
1790
|
+
variant.unitPriceMeasurement,
|
|
1791
|
+
seedCurrency
|
|
1792
|
+
),
|
|
1793
|
+
quantity: rule.min
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
if (seeds.length > 0) setSelections(seeds);
|
|
1797
|
+
}, [bundle, seededFor]);
|
|
1798
|
+
const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
|
|
1799
|
+
const requiredUnits = bundle?.minQuantity ?? 0;
|
|
1800
|
+
const totalQuantity = useMemo6(
|
|
1801
|
+
() => selections.reduce((sum, s) => sum + s.quantity, 0),
|
|
1802
|
+
[selections]
|
|
1803
|
+
);
|
|
1804
|
+
const meetsMinUnits = totalQuantity >= requiredUnits && requiredUnits > 0;
|
|
1805
|
+
const valid = !!bundle && meetsMinUnits;
|
|
1806
|
+
const shownUnits = Math.min(totalQuantity, requiredUnits);
|
|
1807
|
+
const remaining = Math.max(0, requiredUnits - shownUnits);
|
|
1808
|
+
const summary = useMemo6(() => {
|
|
1809
|
+
let comparePrice = 0;
|
|
1810
|
+
let salePrice = 0;
|
|
1811
|
+
for (const sel of selections) {
|
|
1812
|
+
if (sel.quantity <= 0) continue;
|
|
1813
|
+
comparePrice += sel.price * sel.quantity;
|
|
1814
|
+
if (bundle) {
|
|
1815
|
+
salePrice += calculateDiscount2(
|
|
1816
|
+
sel.price,
|
|
1817
|
+
bundle.discountConfig.discountType,
|
|
1818
|
+
bundle.discountConfig.discountValue
|
|
1819
|
+
) * sel.quantity;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const savings = Math.max(0, comparePrice - salePrice);
|
|
1823
|
+
const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
|
|
1824
|
+
return { comparePrice, salePrice, savings, savingsPercent };
|
|
1825
|
+
}, [selections, bundle]);
|
|
1826
|
+
const addSelection = useCallback6(
|
|
1827
|
+
(item) => {
|
|
1828
|
+
setSelections((prev) => {
|
|
1829
|
+
const units = prev.reduce((sum, s) => sum + s.quantity, 0);
|
|
1830
|
+
if (units + item.quantity > requiredUnits) return prev;
|
|
1831
|
+
if (prev.some((s) => s.variantId === item.variantId)) return prev;
|
|
1832
|
+
return [...prev, item];
|
|
1833
|
+
});
|
|
1834
|
+
},
|
|
1835
|
+
[requiredUnits]
|
|
1836
|
+
);
|
|
1837
|
+
const updateQuantity = useCallback6(
|
|
1838
|
+
(productId, variantId, quantity) => {
|
|
1839
|
+
setSelections((prev) => {
|
|
1840
|
+
const idx = prev.findIndex(
|
|
1841
|
+
(s) => s.productId === productId && s.variantId === variantId
|
|
1842
|
+
);
|
|
1843
|
+
if (idx === -1) return prev;
|
|
1844
|
+
const otherUnits = prev.reduce(
|
|
1845
|
+
(sum, s, i) => i === idx ? sum : sum + s.quantity,
|
|
1846
|
+
0
|
|
1847
|
+
);
|
|
1848
|
+
const clamped = Math.max(
|
|
1849
|
+
1,
|
|
1850
|
+
Math.min(quantity, requiredUnits - otherUnits)
|
|
1851
|
+
);
|
|
1852
|
+
if (clamped === prev[idx].quantity) return prev;
|
|
1853
|
+
const next = prev.slice();
|
|
1854
|
+
next[idx] = { ...next[idx], quantity: clamped };
|
|
1855
|
+
return next;
|
|
1856
|
+
});
|
|
1857
|
+
},
|
|
1858
|
+
[requiredUnits]
|
|
1859
|
+
);
|
|
1860
|
+
const swapSelection = useCallback6(
|
|
1861
|
+
(item) => {
|
|
1862
|
+
setSelections((prev) => {
|
|
1863
|
+
if (!bundle) return prev;
|
|
1864
|
+
const swapUnits = swapUnitsForItem(
|
|
1865
|
+
bundle,
|
|
1866
|
+
prev,
|
|
1867
|
+
item.productId,
|
|
1868
|
+
item.variantId
|
|
1869
|
+
);
|
|
1870
|
+
if (swapUnits <= 0) return prev;
|
|
1871
|
+
const qty = Math.min(Math.max(1, item.quantity), swapUnits);
|
|
1872
|
+
let freed = 0;
|
|
1873
|
+
const next = [];
|
|
1874
|
+
for (const s of prev) {
|
|
1875
|
+
if (freed < qty && s.productId === item.productId && s.variantId !== item.variantId) {
|
|
1876
|
+
const take = Math.min(s.quantity, qty - freed);
|
|
1877
|
+
freed += take;
|
|
1878
|
+
if (s.quantity > take) {
|
|
1879
|
+
next.push({ ...s, quantity: s.quantity - take });
|
|
1880
|
+
}
|
|
1881
|
+
} else {
|
|
1882
|
+
next.push(s);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (freed === 0) return prev;
|
|
1886
|
+
next.push({ ...item, quantity: freed });
|
|
1887
|
+
return next;
|
|
1888
|
+
});
|
|
1889
|
+
},
|
|
1890
|
+
[bundle]
|
|
1891
|
+
);
|
|
1892
|
+
const removeVariant = useCallback6(
|
|
1893
|
+
(variantId) => {
|
|
1894
|
+
setSelections((prev) => {
|
|
1895
|
+
const idx = prev.findIndex((s) => s.variantId === variantId);
|
|
1896
|
+
if (idx === -1) return prev;
|
|
1897
|
+
if (bundle && !canRemoveItem(bundle, prev, prev[idx])) return prev;
|
|
1898
|
+
const next = prev.slice();
|
|
1899
|
+
next.splice(idx, 1);
|
|
1900
|
+
return next;
|
|
1901
|
+
});
|
|
1902
|
+
},
|
|
1903
|
+
[bundle]
|
|
1904
|
+
);
|
|
1905
|
+
const removeSlot = useCallback6(
|
|
1906
|
+
(index) => {
|
|
1907
|
+
setSelections((prev) => {
|
|
1908
|
+
if (index < 0 || index >= prev.length) return prev;
|
|
1909
|
+
if (bundle && !canRemoveItem(bundle, prev, prev[index])) return prev;
|
|
1910
|
+
const next = prev.slice();
|
|
1911
|
+
next.splice(index, 1);
|
|
1912
|
+
return next;
|
|
1913
|
+
});
|
|
1914
|
+
},
|
|
1915
|
+
[bundle]
|
|
1916
|
+
);
|
|
1917
|
+
const handleAddToCart = useCallback6(async () => {
|
|
1918
|
+
if (!bundle || !valid) return;
|
|
1919
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1920
|
+
for (const sel of selections) {
|
|
1921
|
+
if (sel.quantity <= 0) continue;
|
|
1922
|
+
const key = `${sel.productId}::${sel.variantId}`;
|
|
1923
|
+
const existing = grouped.get(key);
|
|
1924
|
+
if (existing) existing.quantity += sel.quantity;
|
|
1925
|
+
else grouped.set(key, { variantId: sel.variantId, quantity: sel.quantity });
|
|
1926
|
+
}
|
|
1927
|
+
const lines = Array.from(grouped.values()).map((line) => ({
|
|
1928
|
+
merchandiseId: line.variantId,
|
|
1929
|
+
quantity: line.quantity,
|
|
1930
|
+
attributes: [
|
|
1931
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
1932
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
1933
|
+
]
|
|
1934
|
+
}));
|
|
1935
|
+
setAddingToCart(true);
|
|
1936
|
+
setCartError(null);
|
|
1937
|
+
try {
|
|
1938
|
+
await onAddToCart(lines);
|
|
1939
|
+
const totalPrice = selections.reduce(
|
|
1940
|
+
(sum, sel) => sum + sel.price * sel.quantity,
|
|
1941
|
+
0
|
|
1942
|
+
);
|
|
1943
|
+
trackAddToCart({
|
|
1944
|
+
quantity: totalQuantity,
|
|
1945
|
+
totalPrice: Math.round(totalPrice * 100) / 100
|
|
1946
|
+
});
|
|
1947
|
+
setSelections([]);
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1950
|
+
setCartError(error.message);
|
|
1951
|
+
onError?.(error);
|
|
1952
|
+
} finally {
|
|
1953
|
+
setAddingToCart(false);
|
|
1954
|
+
}
|
|
1955
|
+
}, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
|
|
1956
|
+
if (result.status === "loading") {
|
|
1957
|
+
return /* @__PURE__ */ jsxs4("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
1958
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
1959
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
1960
|
+
] });
|
|
1961
|
+
}
|
|
1962
|
+
if (result.status === "error") return null;
|
|
1963
|
+
if (!bundle) return null;
|
|
1964
|
+
const inStockCount = countInStockProducts(bundle);
|
|
1965
|
+
if (requiredUnits > 0 && inStockCount < requiredUnits) return null;
|
|
1966
|
+
const requiredBlocked = Object.entries(bundle.productRules).some(
|
|
1967
|
+
([pid, rule]) => {
|
|
1968
|
+
if (!rule.required) return false;
|
|
1969
|
+
const product = bundle.products.find((p) => p.id === pid);
|
|
1970
|
+
return !product?.variants.nodes.some(
|
|
1971
|
+
(v) => isVariantFulfillable3(v, rule.min)
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
);
|
|
1975
|
+
if (requiredBlocked) return null;
|
|
1976
|
+
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
1977
|
+
const ctaLabel = addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText || "Add to cart";
|
|
1978
|
+
return /* @__PURE__ */ jsxs4(
|
|
1979
|
+
"div",
|
|
1980
|
+
{
|
|
1981
|
+
ref: (el) => {
|
|
1982
|
+
elementRef(el);
|
|
1983
|
+
setConfigVarsRef(el);
|
|
1984
|
+
},
|
|
1985
|
+
className: `lb-bundle lb-bundle--mix-match lb-mix-match ${className ?? ""}`,
|
|
1986
|
+
role: "region",
|
|
1987
|
+
"aria-label": bundle.title,
|
|
1988
|
+
"data-required-quantity": requiredUnits,
|
|
1989
|
+
children: [
|
|
1990
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-bundle-header", children: /* @__PURE__ */ jsxs4("div", { className: "lb-bundle-header__content", children: [
|
|
1991
|
+
/* @__PURE__ */ jsx4("h3", { className: "lb-bundle-title", children: bundle.title }),
|
|
1992
|
+
bundle.description && /* @__PURE__ */ jsx4("p", { className: "lb-bundle-subtitle", children: bundle.description })
|
|
1993
|
+
] }) }),
|
|
1994
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-mix-match__progress", "data-progress": true, children: [
|
|
1995
|
+
/* @__PURE__ */ jsx4(
|
|
1996
|
+
"div",
|
|
1997
|
+
{
|
|
1998
|
+
className: "lb-mix-match__progress-segments",
|
|
1999
|
+
role: "progressbar",
|
|
2000
|
+
"aria-valuenow": shownUnits,
|
|
2001
|
+
"aria-valuemin": 0,
|
|
2002
|
+
"aria-valuemax": requiredUnits,
|
|
2003
|
+
children: Array.from({ length: requiredUnits }).map((_, i) => /* @__PURE__ */ jsx4(
|
|
2004
|
+
"span",
|
|
2005
|
+
{
|
|
2006
|
+
className: `lb-mix-match__progress-segment${i < shownUnits ? " lb-mix-match__progress-segment--filled" : ""}`,
|
|
2007
|
+
"data-progress-segment": true
|
|
2008
|
+
},
|
|
2009
|
+
i
|
|
2010
|
+
))
|
|
2011
|
+
}
|
|
2012
|
+
),
|
|
2013
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-mix-match__progress-labels", children: [
|
|
2014
|
+
/* @__PURE__ */ jsx4(
|
|
2015
|
+
"span",
|
|
2016
|
+
{
|
|
2017
|
+
className: "lb-mix-match__progress-count",
|
|
2018
|
+
"data-progress-count": true,
|
|
2019
|
+
"aria-live": "polite",
|
|
2020
|
+
children: `${shownUnits} of ${requiredUnits} added`
|
|
2021
|
+
}
|
|
2022
|
+
),
|
|
2023
|
+
/* @__PURE__ */ jsx4(
|
|
2024
|
+
"span",
|
|
2025
|
+
{
|
|
2026
|
+
className: "lb-mix-match__progress-remaining",
|
|
2027
|
+
"data-progress-remaining": true,
|
|
2028
|
+
"aria-live": "polite",
|
|
2029
|
+
children: remaining > 0 ? `${remaining} more to go` : "Complete"
|
|
2030
|
+
}
|
|
2031
|
+
)
|
|
2032
|
+
] })
|
|
2033
|
+
] }),
|
|
2034
|
+
/* @__PURE__ */ jsx4(
|
|
2035
|
+
"div",
|
|
2036
|
+
{
|
|
2037
|
+
className: "lb-mix-match__slots lb-edge-fade",
|
|
2038
|
+
"data-selection-slots": true,
|
|
2039
|
+
ref: slotsEdgeFadeRef,
|
|
2040
|
+
children: selections.map((item, index) => (
|
|
2041
|
+
/* Whole card reopens the picker to edit this pick. The inner title
|
|
2042
|
+
button provides the keyboard/AT path (its activation bubbles
|
|
2043
|
+
here); × stops propagation. */
|
|
2044
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- keyboard path is the inner "Edit selection" title button whose activation bubbles to this handler
|
|
2045
|
+
/* @__PURE__ */ jsxs4(
|
|
2046
|
+
"div",
|
|
2047
|
+
{
|
|
2048
|
+
className: "lb-mix-match__slot lb-mix-match__slot--filled",
|
|
2049
|
+
onClick: () => setPickerOpen(true),
|
|
2050
|
+
children: [
|
|
2051
|
+
item.featuredImage && /* @__PURE__ */ jsx4("div", { className: "lb-bundle-thumbnail", children: /* @__PURE__ */ jsx4("img", { src: item.featuredImage, alt: item.title, loading: "lazy" }) }),
|
|
2052
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-mix-match__filled-info", children: [
|
|
2053
|
+
/* @__PURE__ */ jsx4(
|
|
2054
|
+
"button",
|
|
2055
|
+
{
|
|
2056
|
+
type: "button",
|
|
2057
|
+
className: "lb-mix-match__filled-title",
|
|
2058
|
+
"aria-label": `Edit selection: ${item.title}`,
|
|
2059
|
+
children: item.title
|
|
2060
|
+
}
|
|
2061
|
+
),
|
|
2062
|
+
item.variantTitle && item.variantTitle !== "Default Title" && /* @__PURE__ */ jsx4("span", { className: "lb-mix-match__filled-variant", children: item.variantTitle }),
|
|
2063
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-mix-match__filled-price", children: [
|
|
2064
|
+
item.compareAtPrice !== null && item.compareAtPrice > item.price && /* @__PURE__ */ jsx4("s", { className: "lb-mix-match__filled-compare", children: formatMoney3(item.compareAtPrice, currency) }),
|
|
2065
|
+
/* @__PURE__ */ jsx4("span", { className: "lb-bundle-product-price", children: formatMoney3(item.price, currency) }),
|
|
2066
|
+
/* @__PURE__ */ jsx4("span", { className: "lb-bundle-qty-inline", children: `\xD7${item.quantity}` })
|
|
2067
|
+
] }),
|
|
2068
|
+
item.unitPrice && /* @__PURE__ */ jsx4("span", { className: "lb-bundle-product-unit-price", children: item.unitPrice })
|
|
2069
|
+
] }),
|
|
2070
|
+
canRemoveItem(bundle, selections, item) ? /* @__PURE__ */ jsx4(
|
|
2071
|
+
"button",
|
|
2072
|
+
{
|
|
2073
|
+
type: "button",
|
|
2074
|
+
className: "lb-mix-match__slot-remove",
|
|
2075
|
+
"aria-label": `Remove ${item.title}`,
|
|
2076
|
+
onClick: (e) => {
|
|
2077
|
+
e.stopPropagation();
|
|
2078
|
+
removeSlot(index);
|
|
2079
|
+
},
|
|
2080
|
+
children: /* @__PURE__ */ jsx4(CloseIcon2, {})
|
|
2081
|
+
}
|
|
2082
|
+
) : (
|
|
2083
|
+
/* Required pick at its floor — quiet state chip instead of
|
|
2084
|
+
the ×; removal returns once another slot covers the
|
|
2085
|
+
product's minimum (variant swap flow). */
|
|
2086
|
+
/* @__PURE__ */ jsx4("span", { className: "lb-mix-match__slot-required", children: "Required" })
|
|
2087
|
+
)
|
|
2088
|
+
]
|
|
2089
|
+
},
|
|
2090
|
+
`${item.productId}:${item.variantId}:${index}`
|
|
2091
|
+
)
|
|
2092
|
+
))
|
|
2093
|
+
}
|
|
2094
|
+
),
|
|
2095
|
+
totalQuantity < requiredUnits && /* @__PURE__ */ jsxs4(
|
|
2096
|
+
"button",
|
|
2097
|
+
{
|
|
2098
|
+
type: "button",
|
|
2099
|
+
className: "lb-mix-match__add-product",
|
|
2100
|
+
"data-add-product-trigger": true,
|
|
2101
|
+
onClick: () => setPickerOpen(true),
|
|
2102
|
+
children: [
|
|
2103
|
+
/* @__PURE__ */ jsx4("span", { className: "lb-mix-match__add-product-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx4(PlusIcon, {}) }),
|
|
2104
|
+
/* @__PURE__ */ jsx4("span", { className: "lb-mix-match__add-product-label", children: "Add a product" })
|
|
2105
|
+
]
|
|
2106
|
+
}
|
|
2107
|
+
),
|
|
2108
|
+
/* @__PURE__ */ jsx4("div", { className: "lb-bundle-divider" }),
|
|
2109
|
+
selections.length > 0 && /* @__PURE__ */ jsxs4("div", { className: "lb-bundle-summary", "data-pricing-section": true, children: [
|
|
2110
|
+
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle-summary__text", children: [
|
|
2111
|
+
/* @__PURE__ */ jsx4("span", { className: "lb-bundle-summary__label", children: "Bundle total" }),
|
|
2112
|
+
bundle.widgetConfig.savingsBar.visible && summary.savings > 0 && /* @__PURE__ */ jsxs4("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
|
|
2113
|
+
"You save",
|
|
2114
|
+
" ",
|
|
2115
|
+
/* @__PURE__ */ jsx4("span", { "data-savings-amount": true, children: formatMoney3(summary.savings, currency) }),
|
|
2116
|
+
" ",
|
|
2117
|
+
/* @__PURE__ */ jsxs4("span", { "data-savings-percent": true, children: [
|
|
2118
|
+
"(",
|
|
2119
|
+
summary.savingsPercent,
|
|
2120
|
+
"%)"
|
|
2121
|
+
] })
|
|
2122
|
+
] })
|
|
2123
|
+
] }),
|
|
2124
|
+
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle-summary__prices", children: [
|
|
2125
|
+
bundle.widgetConfig.pricing.showComparePrice && summary.savings > 0 && /* @__PURE__ */ jsx4("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: formatMoney3(summary.comparePrice, currency) }),
|
|
2126
|
+
/* @__PURE__ */ jsx4("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: formatMoney3(summary.salePrice, currency) })
|
|
2127
|
+
] })
|
|
2128
|
+
] }),
|
|
2129
|
+
cartError && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
2130
|
+
/* @__PURE__ */ jsx4(
|
|
2131
|
+
"button",
|
|
2132
|
+
{
|
|
2133
|
+
className: "lb-bundle__cta",
|
|
2134
|
+
onClick: handleAddToCart,
|
|
2135
|
+
disabled: addingToCart || !valid,
|
|
2136
|
+
"aria-busy": addingToCart,
|
|
2137
|
+
children: ctaLabel
|
|
2138
|
+
}
|
|
2139
|
+
),
|
|
2140
|
+
pickerOpen && /* @__PURE__ */ jsx4(
|
|
2141
|
+
MixMatchPicker,
|
|
2142
|
+
{
|
|
2143
|
+
outOfStockBehavior,
|
|
2144
|
+
bundle,
|
|
2145
|
+
currency,
|
|
2146
|
+
requiredQty: requiredUnits,
|
|
2147
|
+
showStepper,
|
|
2148
|
+
selections,
|
|
2149
|
+
onAdd: addSelection,
|
|
2150
|
+
onUpdateQuantity: updateQuantity,
|
|
2151
|
+
onRemoveVariant: removeVariant,
|
|
2152
|
+
canRemoveVariant: (variantId) => {
|
|
2153
|
+
const item = selections.find((s) => s.variantId === variantId);
|
|
2154
|
+
return item ? canRemoveItem(bundle, selections, item) : true;
|
|
2155
|
+
},
|
|
2156
|
+
swapUnitsFor: (productId, variantId) => swapUnitsForItem(bundle, selections, productId, variantId),
|
|
2157
|
+
onSwap: swapSelection,
|
|
2158
|
+
onClose: () => setPickerOpen(false),
|
|
2159
|
+
styleVarsRef: setModalConfigVarsRef
|
|
2160
|
+
}
|
|
2161
|
+
)
|
|
2162
|
+
]
|
|
2163
|
+
}
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
function countInStockProducts(bundle) {
|
|
2167
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2168
|
+
let count = 0;
|
|
2169
|
+
for (const product of bundle.products) {
|
|
2170
|
+
if (seen.has(product.id)) continue;
|
|
2171
|
+
seen.add(product.id);
|
|
2172
|
+
const rule = ruleFor2(bundle, product.id);
|
|
2173
|
+
const fulfillable = product.variants.nodes.some(
|
|
2174
|
+
(v) => isVariantFulfillable3(v, rule.min)
|
|
2175
|
+
);
|
|
2176
|
+
if (fulfillable) count += 1;
|
|
2177
|
+
}
|
|
2178
|
+
return count;
|
|
2179
|
+
}
|
|
2180
|
+
function PlusIcon() {
|
|
2181
|
+
return /* @__PURE__ */ jsxs4(
|
|
2182
|
+
"svg",
|
|
2183
|
+
{
|
|
2184
|
+
width: "18",
|
|
2185
|
+
height: "18",
|
|
2186
|
+
viewBox: "0 0 18 18",
|
|
2187
|
+
fill: "none",
|
|
2188
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2189
|
+
"aria-hidden": "true",
|
|
2190
|
+
children: [
|
|
2191
|
+
/* @__PURE__ */ jsx4("line", { x1: "9", y1: "3", x2: "9", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
2192
|
+
/* @__PURE__ */ jsx4("line", { x1: "3", y1: "9", x2: "15", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
|
|
2193
|
+
]
|
|
2194
|
+
}
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
function CloseIcon2() {
|
|
2198
|
+
return /* @__PURE__ */ jsxs4(
|
|
2199
|
+
"svg",
|
|
2200
|
+
{
|
|
2201
|
+
width: "16",
|
|
2202
|
+
height: "16",
|
|
2203
|
+
viewBox: "0 0 20 20",
|
|
2204
|
+
fill: "none",
|
|
2205
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2206
|
+
"aria-hidden": "true",
|
|
2207
|
+
children: [
|
|
2208
|
+
/* @__PURE__ */ jsx4("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
2209
|
+
/* @__PURE__ */ jsx4("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
|
|
2210
|
+
]
|
|
2211
|
+
}
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// src/components/VolumeBundle.tsx
|
|
2216
|
+
import { useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo7, useState as useState9 } from "react";
|
|
2217
|
+
import {
|
|
2218
|
+
formatMoney as formatMoney4,
|
|
2219
|
+
formatUnitPrice as formatUnitPrice4,
|
|
2220
|
+
calculateTierSavings,
|
|
2221
|
+
getActiveTier,
|
|
2222
|
+
getMinTierQuantity,
|
|
2223
|
+
isVariantFulfillable as isVariantFulfillable4,
|
|
2224
|
+
shouldShowLowStockBadge as shouldShowLowStockBadge2
|
|
2225
|
+
} from "@lime-bundles/core";
|
|
2226
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2227
|
+
function VolumeBundle(props) {
|
|
2228
|
+
const {
|
|
2229
|
+
shopDomain,
|
|
2230
|
+
storefrontAccessToken,
|
|
2231
|
+
bundleGid,
|
|
2232
|
+
appUrl,
|
|
2233
|
+
analyticsEnabled,
|
|
2234
|
+
onAddToCart,
|
|
2235
|
+
onError,
|
|
2236
|
+
className
|
|
2237
|
+
} = props;
|
|
2238
|
+
const result = useBundleData({
|
|
2239
|
+
shopDomain,
|
|
2240
|
+
storefrontAccessToken,
|
|
2241
|
+
bundleGid
|
|
2242
|
+
});
|
|
2243
|
+
const { elementRef, trackAddToCart } = useAnalytics({
|
|
2244
|
+
shopDomain,
|
|
2245
|
+
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
2246
|
+
bundleGid,
|
|
2247
|
+
bundleType: "volume",
|
|
2248
|
+
enabled: analyticsEnabled !== false
|
|
2249
|
+
});
|
|
2250
|
+
const [quantity, setQuantity] = useState9(1);
|
|
2251
|
+
const [addingToCart, setAddingToCart] = useState9(false);
|
|
2252
|
+
const [cartError, setCartError] = useState9(null);
|
|
2253
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
|
|
2254
|
+
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
2255
|
+
const tiersEdgeFadeRef = useEdgeFade();
|
|
2256
|
+
useEffect12(() => {
|
|
2257
|
+
if (result.status === "error") {
|
|
2258
|
+
onError?.(result.error);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
if (result.status === "success" && result.bundle.bundleType !== "volume") {
|
|
2262
|
+
onError?.(
|
|
2263
|
+
new Error(
|
|
2264
|
+
`VolumeBundle: expected bundleType="volume", got "${result.bundle.bundleType}"`
|
|
2265
|
+
)
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
}, [result, onError]);
|
|
2269
|
+
const product = bundle?.products[0];
|
|
2270
|
+
const variants = useMemo7(() => product?.variants.nodes ?? [], [product]);
|
|
2271
|
+
const optionNames = useMemo7(() => {
|
|
2272
|
+
const first = variants[0];
|
|
2273
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
2274
|
+
}, [variants]);
|
|
2275
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
2276
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
2277
|
+
const minTierQty = bundle ? getMinTierQuantity(bundle.volumeTiers) : 1;
|
|
2278
|
+
const isSoldOut = variants.length > 0 && !variants.some((v) => isVariantFulfillable4(v, minTierQty));
|
|
2279
|
+
const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable4(v, minTierQty)) ?? variants[0];
|
|
2280
|
+
const basePrice = displayVariant ? parseFloat(displayVariant.price.amount) : product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
|
|
2281
|
+
const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
2282
|
+
const thumbImage = displayVariant?.image ?? product?.featuredImage ?? null;
|
|
2283
|
+
const unitPriceText = displayVariant ? formatUnitPrice4(
|
|
2284
|
+
displayVariant.unitPrice,
|
|
2285
|
+
displayVariant.unitPriceMeasurement,
|
|
2286
|
+
currency
|
|
2287
|
+
) : null;
|
|
2288
|
+
const tierSavings = useMemo7(
|
|
2289
|
+
() => bundle ? calculateTierSavings(
|
|
2290
|
+
bundle.volumeTiers,
|
|
2291
|
+
basePrice,
|
|
2292
|
+
quantity,
|
|
2293
|
+
bundle.discountConfig.discountType
|
|
2294
|
+
) : [],
|
|
2295
|
+
[bundle, basePrice, quantity]
|
|
2296
|
+
);
|
|
2297
|
+
const activeTier = bundle ? getActiveTier(bundle.volumeTiers, quantity) : null;
|
|
2298
|
+
const popularTierIndex = useMemo7(() => {
|
|
2299
|
+
if (!bundle?.widgetConfig.popularBadge.visible || tierSavings.length === 0) {
|
|
2300
|
+
return -1;
|
|
2301
|
+
}
|
|
2302
|
+
const pinned = bundle.widgetConfig.popularBadge.tierIndex;
|
|
2303
|
+
if (pinned !== void 0) {
|
|
2304
|
+
return Math.min(Math.max(pinned, 0), tierSavings.length - 1);
|
|
2305
|
+
}
|
|
2306
|
+
let best = 0;
|
|
2307
|
+
for (let i = 1; i < tierSavings.length; i++) {
|
|
2308
|
+
if (tierSavings[i].savings > tierSavings[best].savings) best = i;
|
|
2309
|
+
}
|
|
2310
|
+
return best;
|
|
2311
|
+
}, [bundle, tierSavings]);
|
|
2312
|
+
const activeUnitPrice = tierSavings.find((ts) => ts.tier === activeTier)?.unitPrice ?? basePrice;
|
|
2313
|
+
const undiscountedTotal = basePrice * quantity;
|
|
2314
|
+
const volumeTotal = activeUnitPrice * quantity;
|
|
2315
|
+
const volumeSavings = Math.max(0, undiscountedTotal - volumeTotal);
|
|
2316
|
+
const volumeSavingsPercent = undiscountedTotal > 0 && volumeSavings > 0 ? Math.round(volumeSavings / undiscountedTotal * 100) : 0;
|
|
2317
|
+
const handleAddToCart = useCallback7(async () => {
|
|
2318
|
+
if (!bundle || !product) return;
|
|
2319
|
+
const variant = selectedVariant ?? product.variants.nodes.find(
|
|
2320
|
+
(v) => isVariantFulfillable4(v, quantity)
|
|
2321
|
+
);
|
|
2322
|
+
if (!variant) return;
|
|
2323
|
+
const lines = [
|
|
2324
|
+
{
|
|
2325
|
+
merchandiseId: variant.id,
|
|
2326
|
+
quantity,
|
|
2327
|
+
attributes: [
|
|
2328
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
2329
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
2330
|
+
]
|
|
2331
|
+
}
|
|
2332
|
+
];
|
|
2333
|
+
setAddingToCart(true);
|
|
2334
|
+
setCartError(null);
|
|
2335
|
+
try {
|
|
2336
|
+
await onAddToCart(lines);
|
|
2337
|
+
const unitPrice = tierSavings.find((ts) => ts.tier === activeTier)?.unitPrice ?? basePrice;
|
|
2338
|
+
trackAddToCart({
|
|
2339
|
+
productId: product.id,
|
|
2340
|
+
quantity,
|
|
2341
|
+
totalPrice: Math.round(unitPrice * quantity * 100) / 100
|
|
2342
|
+
});
|
|
2343
|
+
} catch (err) {
|
|
2344
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2345
|
+
setCartError(error.message);
|
|
2346
|
+
onError?.(error);
|
|
2347
|
+
} finally {
|
|
2348
|
+
setAddingToCart(false);
|
|
2349
|
+
}
|
|
2350
|
+
}, [
|
|
2351
|
+
bundle,
|
|
2352
|
+
product,
|
|
2353
|
+
quantity,
|
|
2354
|
+
activeTier,
|
|
2355
|
+
basePrice,
|
|
2356
|
+
onAddToCart,
|
|
2357
|
+
onError,
|
|
2358
|
+
trackAddToCart,
|
|
2359
|
+
selectedVariant,
|
|
2360
|
+
tierSavings
|
|
2361
|
+
]);
|
|
2362
|
+
if (result.status === "loading") {
|
|
2363
|
+
return /* @__PURE__ */ jsxs5("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
2364
|
+
/* @__PURE__ */ jsx5("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
2365
|
+
/* @__PURE__ */ jsx5("div", { className: "lb-skeleton lb-skeleton--tiers" })
|
|
2366
|
+
] });
|
|
2367
|
+
}
|
|
2368
|
+
if (result.status === "error") return null;
|
|
2369
|
+
if (!bundle) return null;
|
|
2370
|
+
if (!product) return null;
|
|
2371
|
+
return /* @__PURE__ */ jsxs5(
|
|
2372
|
+
"div",
|
|
2373
|
+
{
|
|
2374
|
+
ref: (el) => {
|
|
2375
|
+
elementRef(el);
|
|
2376
|
+
setConfigVarsRef(el);
|
|
2377
|
+
},
|
|
2378
|
+
className: `lb-bundle lb-bundle--volume ${className ?? ""}`,
|
|
2379
|
+
role: "region",
|
|
2380
|
+
"aria-label": bundle.title,
|
|
2381
|
+
children: [
|
|
2382
|
+
/* @__PURE__ */ jsx5("div", { className: "lb-bundle-header", children: /* @__PURE__ */ jsxs5("div", { className: "lb-bundle-header__content", children: [
|
|
2383
|
+
/* @__PURE__ */ jsx5("h3", { className: "lb-bundle-title", children: bundle.title }),
|
|
2384
|
+
bundle.description && /* @__PURE__ */ jsx5("p", { className: "lb-bundle-subtitle", children: bundle.description })
|
|
2385
|
+
] }) }),
|
|
2386
|
+
/* @__PURE__ */ jsxs5("div", { className: "lb-bundle-product-row lb-bundle-product-row--volume", children: [
|
|
2387
|
+
thumbImage && /* @__PURE__ */ jsx5(
|
|
2388
|
+
"img",
|
|
2389
|
+
{
|
|
2390
|
+
src: thumbImage.url,
|
|
2391
|
+
alt: thumbImage.altText ?? product.title,
|
|
2392
|
+
className: "lb-bundle__product-image",
|
|
2393
|
+
loading: "lazy"
|
|
2394
|
+
}
|
|
2395
|
+
),
|
|
2396
|
+
/* @__PURE__ */ jsxs5("div", { className: "lb-bundle__product-info", children: [
|
|
2397
|
+
/* @__PURE__ */ jsx5("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
2398
|
+
/* @__PURE__ */ jsxs5("p", { className: "lb-bundle__product-price", children: [
|
|
2399
|
+
formatMoney4(basePrice, currency),
|
|
2400
|
+
" each"
|
|
2401
|
+
] }),
|
|
2402
|
+
unitPriceText && /* @__PURE__ */ jsx5("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
2403
|
+
showPicker && /* @__PURE__ */ jsx5("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
2404
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
2405
|
+
value: o.value,
|
|
2406
|
+
label: o.value,
|
|
2407
|
+
disabled: o.disabled
|
|
2408
|
+
}));
|
|
2409
|
+
return /* @__PURE__ */ jsx5(
|
|
2410
|
+
VariantDropdown,
|
|
2411
|
+
{
|
|
2412
|
+
options: dropdownOptions,
|
|
2413
|
+
value: selectedValues[optionIndex] ?? null,
|
|
2414
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
2415
|
+
ariaLabel: optionName
|
|
2416
|
+
},
|
|
2417
|
+
optionName
|
|
2418
|
+
);
|
|
2419
|
+
}) })
|
|
2420
|
+
] })
|
|
2421
|
+
] }),
|
|
2422
|
+
/* @__PURE__ */ jsx5(
|
|
2423
|
+
"div",
|
|
2424
|
+
{
|
|
2425
|
+
className: "lb-bundle__tiers lb-edge-fade",
|
|
2426
|
+
role: "table",
|
|
2427
|
+
"aria-label": "Volume discounts",
|
|
2428
|
+
ref: tiersEdgeFadeRef,
|
|
2429
|
+
children: tierSavings.map((ts, tierIndex) => {
|
|
2430
|
+
const savingsPercent = Math.round(ts.savingsPercent);
|
|
2431
|
+
const showCompare = bundle.widgetConfig.pricing.showComparePrice && ts.savings > 0;
|
|
2432
|
+
return /* @__PURE__ */ jsxs5(
|
|
2433
|
+
"div",
|
|
2434
|
+
{
|
|
2435
|
+
className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
|
|
2436
|
+
role: "row",
|
|
2437
|
+
children: [
|
|
2438
|
+
/* @__PURE__ */ jsx5("span", { className: "lb-bundle__tier-radio", "aria-hidden": "true", children: /* @__PURE__ */ jsx5("span", { className: "lb-bundle__tier-radio-dot" }) }),
|
|
2439
|
+
/* @__PURE__ */ jsxs5("span", { className: "lb-bundle__tier-info", role: "cell", children: [
|
|
2440
|
+
/* @__PURE__ */ jsxs5("span", { className: "lb-bundle__tier-label", children: [
|
|
2441
|
+
"Buy ",
|
|
2442
|
+
ts.tier.minQuantity
|
|
2443
|
+
] }),
|
|
2444
|
+
/* @__PURE__ */ jsxs5("span", { className: "lb-bundle__tier-price", children: [
|
|
2445
|
+
showCompare && /* @__PURE__ */ jsx5("span", { className: "lb-bundle__tier-compare", children: formatMoney4(basePrice, currency) }),
|
|
2446
|
+
/* @__PURE__ */ jsx5("span", { children: formatMoney4(ts.unitPrice, currency) }),
|
|
2447
|
+
/* @__PURE__ */ jsx5("span", { className: "lb-bundle__tier-unit", children: "each" })
|
|
2448
|
+
] })
|
|
2449
|
+
] }),
|
|
2450
|
+
(tierIndex === popularTierIndex || savingsPercent > 0) && /* @__PURE__ */ jsxs5("span", { className: "lb-bundle__tier-right", role: "cell", children: [
|
|
2451
|
+
tierIndex === popularTierIndex && /* @__PURE__ */ jsx5("span", { className: "lb-bundle__tier-badge", children: bundle.widgetConfig.popularBadge.text }),
|
|
2452
|
+
savingsPercent > 0 && /* @__PURE__ */ jsxs5("span", { className: "lb-bundle__tier-savings", children: [
|
|
2453
|
+
"Save ",
|
|
2454
|
+
savingsPercent,
|
|
2455
|
+
"%"
|
|
2456
|
+
] })
|
|
2457
|
+
] })
|
|
2458
|
+
]
|
|
2459
|
+
},
|
|
2460
|
+
ts.tier.minQuantity
|
|
2461
|
+
);
|
|
2462
|
+
})
|
|
2463
|
+
}
|
|
2464
|
+
),
|
|
2465
|
+
/* @__PURE__ */ jsxs5("div", { className: "lb-bundle__quantity-selector", children: [
|
|
2466
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
|
|
2467
|
+
/* @__PURE__ */ jsxs5("div", { className: "lb-bundle__quantity-control", children: [
|
|
2468
|
+
/* @__PURE__ */ jsx5(
|
|
2469
|
+
"button",
|
|
2470
|
+
{
|
|
2471
|
+
"aria-label": "Decrease quantity",
|
|
2472
|
+
onClick: () => setQuantity((q) => Math.max(1, q - 1)),
|
|
2473
|
+
children: "\u2212"
|
|
2474
|
+
}
|
|
2475
|
+
),
|
|
2476
|
+
/* @__PURE__ */ jsx5(
|
|
2477
|
+
"input",
|
|
2478
|
+
{
|
|
2479
|
+
id: `lb-qty-${bundle.id}`,
|
|
2480
|
+
type: "number",
|
|
2481
|
+
min: 1,
|
|
2482
|
+
value: quantity,
|
|
2483
|
+
onChange: (e) => {
|
|
2484
|
+
const val = parseInt(e.target.value, 10);
|
|
2485
|
+
if (!isNaN(val) && val > 0) setQuantity(val);
|
|
2486
|
+
},
|
|
2487
|
+
className: "lb-bundle__quantity-input"
|
|
2488
|
+
}
|
|
2489
|
+
),
|
|
2490
|
+
/* @__PURE__ */ jsx5(
|
|
2491
|
+
"button",
|
|
2492
|
+
{
|
|
2493
|
+
"aria-label": "Increase quantity",
|
|
2494
|
+
onClick: () => setQuantity((q) => q + 1),
|
|
2495
|
+
children: "+"
|
|
2496
|
+
}
|
|
2497
|
+
)
|
|
2498
|
+
] })
|
|
2499
|
+
] }),
|
|
2500
|
+
cartError && /* @__PURE__ */ jsx5("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
2501
|
+
bundle && displayVariant && shouldShowLowStockBadge2(
|
|
2502
|
+
displayVariant,
|
|
2503
|
+
minTierQty,
|
|
2504
|
+
bundle.widgetConfig.lowStockThreshold,
|
|
2505
|
+
bundle.widgetConfig.showLowStockBadge
|
|
2506
|
+
) && /* @__PURE__ */ jsxs5("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
2507
|
+
"Only ",
|
|
2508
|
+
displayVariant.quantityAvailable,
|
|
2509
|
+
" left"
|
|
2510
|
+
] }),
|
|
2511
|
+
/* @__PURE__ */ jsx5("div", { className: "lb-bundle-divider" }),
|
|
2512
|
+
/* @__PURE__ */ jsxs5("div", { className: "lb-bundle-summary", children: [
|
|
2513
|
+
/* @__PURE__ */ jsxs5("div", { className: "lb-bundle-summary__text", children: [
|
|
2514
|
+
/* @__PURE__ */ jsxs5("span", { className: "lb-bundle-summary__label", "data-total-label": true, children: [
|
|
2515
|
+
"Total",
|
|
2516
|
+
bundle.widgetConfig.pricing.showItemCount && /* @__PURE__ */ jsxs5("span", { "data-item-count": true, children: [
|
|
2517
|
+
" ",
|
|
2518
|
+
"(",
|
|
2519
|
+
quantity,
|
|
2520
|
+
" item",
|
|
2521
|
+
quantity === 1 ? "" : "s",
|
|
2522
|
+
")"
|
|
2523
|
+
] })
|
|
2524
|
+
] }),
|
|
2525
|
+
bundle.widgetConfig.savingsBar.visible && volumeSavings > 0 && /* @__PURE__ */ jsxs5("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
|
|
2526
|
+
"You save",
|
|
2527
|
+
" ",
|
|
2528
|
+
/* @__PURE__ */ jsx5("span", { "data-savings-amount": true, children: formatMoney4(volumeSavings, currency) }),
|
|
2529
|
+
" ",
|
|
2530
|
+
/* @__PURE__ */ jsxs5("span", { "data-savings-percent": true, children: [
|
|
2531
|
+
"(",
|
|
2532
|
+
volumeSavingsPercent,
|
|
2533
|
+
"%)"
|
|
2534
|
+
] })
|
|
2535
|
+
] })
|
|
2536
|
+
] }),
|
|
2537
|
+
/* @__PURE__ */ jsxs5("span", { className: "lb-bundle-summary__prices", children: [
|
|
2538
|
+
bundle.widgetConfig.pricing.showComparePrice && volumeSavings > 0 && /* @__PURE__ */ jsx5("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: formatMoney4(undiscountedTotal, currency) }),
|
|
2539
|
+
/* @__PURE__ */ jsx5("span", { className: "lb-bundle-sale-price", "data-total-price": true, children: formatMoney4(volumeTotal, currency) })
|
|
2540
|
+
] })
|
|
2541
|
+
] }),
|
|
2542
|
+
/* @__PURE__ */ jsx5(
|
|
2543
|
+
"button",
|
|
2544
|
+
{
|
|
2545
|
+
className: "lb-bundle__cta",
|
|
2546
|
+
onClick: handleAddToCart,
|
|
2547
|
+
disabled: addingToCart || isSoldOut,
|
|
2548
|
+
"aria-busy": addingToCart,
|
|
2549
|
+
children: addingToCart ? "Adding..." : isSoldOut ? "Sold out" : bundle.widgetConfig.cta.ctaText ?? `Add ${quantity} to Cart`
|
|
2550
|
+
}
|
|
2551
|
+
)
|
|
2552
|
+
]
|
|
2553
|
+
}
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// src/components/BogoBundle.tsx
|
|
2558
|
+
import { useCallback as useCallback8, useEffect as useEffect13, useMemo as useMemo8, useState as useState10 } from "react";
|
|
2559
|
+
import {
|
|
2560
|
+
formatMoney as formatMoney5,
|
|
2561
|
+
formatUnitPrice as formatUnitPrice5,
|
|
2562
|
+
isVariantFulfillable as isVariantFulfillable5
|
|
2563
|
+
} from "@lime-bundles/core";
|
|
2564
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2565
|
+
function discountedUnit(price, percent) {
|
|
2566
|
+
const cents = Math.round(price * 100);
|
|
2567
|
+
const discounted = cents - Math.floor(cents * percent / 100);
|
|
2568
|
+
return discounted / 100;
|
|
2569
|
+
}
|
|
2570
|
+
function BogoBundle(props) {
|
|
2571
|
+
const {
|
|
2572
|
+
shopDomain,
|
|
2573
|
+
storefrontAccessToken,
|
|
2574
|
+
bundleGid,
|
|
2575
|
+
appUrl,
|
|
2576
|
+
analyticsEnabled,
|
|
2577
|
+
onAddToCart,
|
|
2578
|
+
onError,
|
|
2579
|
+
className
|
|
2580
|
+
} = props;
|
|
2581
|
+
const result = useBundleData({
|
|
2582
|
+
shopDomain,
|
|
2583
|
+
storefrontAccessToken,
|
|
2584
|
+
bundleGid
|
|
2585
|
+
});
|
|
2586
|
+
const { elementRef, trackAddToCart } = useAnalytics({
|
|
2587
|
+
shopDomain,
|
|
2588
|
+
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
2589
|
+
bundleGid,
|
|
2590
|
+
bundleType: "bogo",
|
|
2591
|
+
enabled: analyticsEnabled !== false
|
|
2592
|
+
});
|
|
2593
|
+
const [addingToCart, setAddingToCart] = useState10(false);
|
|
2594
|
+
const [cartError, setCartError] = useState10(null);
|
|
2595
|
+
const [selectedVariants, setSelectedVariants] = useState10({ buy: null, get: null });
|
|
2596
|
+
const handleVariantChange = useCallback8(
|
|
2597
|
+
(role, variant) => {
|
|
2598
|
+
setSelectedVariants((prev) => {
|
|
2599
|
+
if (prev[role] === variant) return prev;
|
|
2600
|
+
return { ...prev, [role]: variant };
|
|
2601
|
+
});
|
|
2602
|
+
},
|
|
2603
|
+
[]
|
|
2604
|
+
);
|
|
2605
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "bogo" ? result.bundle : null;
|
|
2606
|
+
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
2607
|
+
useEffect13(() => {
|
|
2608
|
+
if (result.status === "error") {
|
|
2609
|
+
onError?.(result.error);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (result.status === "success" && result.bundle.bundleType !== "bogo") {
|
|
2613
|
+
onError?.(
|
|
2614
|
+
new Error(
|
|
2615
|
+
`BogoBundle: expected bundleType="bogo", got "${result.bundle.bundleType}"`
|
|
2616
|
+
)
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
}, [result, onError]);
|
|
2620
|
+
const buyProduct = useMemo8(
|
|
2621
|
+
() => bundle?.products.find((p) => p.id === bundle.buyProductId) ?? null,
|
|
2622
|
+
[bundle]
|
|
2623
|
+
);
|
|
2624
|
+
const getProduct = useMemo8(
|
|
2625
|
+
() => bundle?.products.find((p) => p.id === bundle.getProductId) ?? null,
|
|
2626
|
+
[bundle]
|
|
2627
|
+
);
|
|
2628
|
+
const percent = bundle ? Math.min(100, Math.max(0, bundle.discountConfig.discountValue)) : 0;
|
|
2629
|
+
const resolveVariant = useCallback8(
|
|
2630
|
+
(product, selected, qty) => {
|
|
2631
|
+
if (!product) return null;
|
|
2632
|
+
return selected ?? product.variants.nodes.find((v) => isVariantFulfillable5(v, qty)) ?? product.variants.nodes[0] ?? null;
|
|
2633
|
+
},
|
|
2634
|
+
[]
|
|
2635
|
+
);
|
|
2636
|
+
const oosSides = useMemo8(() => {
|
|
2637
|
+
if (!bundle) return 0;
|
|
2638
|
+
const short = (product, qty) => !!product && !product.variants.nodes.some((v) => isVariantFulfillable5(v, qty));
|
|
2639
|
+
return (short(buyProduct, bundle.buyQuantity) ? 1 : 0) + (short(getProduct, bundle.getQuantity) ? 1 : 0);
|
|
2640
|
+
}, [bundle, buyProduct, getProduct]);
|
|
2641
|
+
const handleAddToCart = useCallback8(async () => {
|
|
2642
|
+
if (!bundle || !buyProduct || !getProduct) return;
|
|
2643
|
+
const buyVariant2 = resolveVariant(buyProduct, selectedVariants.buy, bundle.buyQuantity);
|
|
2644
|
+
const getVariant2 = resolveVariant(getProduct, selectedVariants.get, bundle.getQuantity);
|
|
2645
|
+
if (!buyVariant2 || !getVariant2) return;
|
|
2646
|
+
const attributes = [
|
|
2647
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
2648
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
2649
|
+
];
|
|
2650
|
+
const lines = buyVariant2.id === getVariant2.id ? [
|
|
2651
|
+
{
|
|
2652
|
+
merchandiseId: buyVariant2.id,
|
|
2653
|
+
quantity: bundle.buyQuantity + bundle.getQuantity,
|
|
2654
|
+
attributes
|
|
2655
|
+
}
|
|
2656
|
+
] : [
|
|
2657
|
+
{
|
|
2658
|
+
merchandiseId: buyVariant2.id,
|
|
2659
|
+
quantity: bundle.buyQuantity,
|
|
2660
|
+
attributes
|
|
2661
|
+
},
|
|
2662
|
+
{
|
|
2663
|
+
merchandiseId: getVariant2.id,
|
|
2664
|
+
quantity: bundle.getQuantity,
|
|
2665
|
+
attributes
|
|
2666
|
+
}
|
|
2667
|
+
];
|
|
2668
|
+
setAddingToCart(true);
|
|
2669
|
+
setCartError(null);
|
|
2670
|
+
try {
|
|
2671
|
+
await onAddToCart(lines);
|
|
2672
|
+
const buyUnit2 = parseFloat(buyVariant2.price.amount);
|
|
2673
|
+
const getUnit2 = parseFloat(getVariant2.price.amount);
|
|
2674
|
+
const totalPrice = buyUnit2 * bundle.buyQuantity + discountedUnit(getUnit2, percent) * bundle.getQuantity;
|
|
2675
|
+
trackAddToCart({
|
|
2676
|
+
quantity: bundle.buyQuantity + bundle.getQuantity,
|
|
2677
|
+
totalPrice: Math.round(totalPrice * 100) / 100
|
|
2678
|
+
});
|
|
2679
|
+
} catch (err) {
|
|
2680
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2681
|
+
setCartError(error.message);
|
|
2682
|
+
onError?.(error);
|
|
2683
|
+
} finally {
|
|
2684
|
+
setAddingToCart(false);
|
|
2685
|
+
}
|
|
2686
|
+
}, [bundle, buyProduct, getProduct, selectedVariants, resolveVariant, onAddToCart, onError, trackAddToCart, percent]);
|
|
2687
|
+
if (result.status === "loading") {
|
|
2688
|
+
return /* @__PURE__ */ jsxs6("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
2689
|
+
/* @__PURE__ */ jsx6("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
2690
|
+
/* @__PURE__ */ jsx6("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
2691
|
+
] });
|
|
2692
|
+
}
|
|
2693
|
+
if (result.status === "error") return null;
|
|
2694
|
+
if (!bundle || !buyProduct || !getProduct) return null;
|
|
2695
|
+
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
2696
|
+
const buyVariant = resolveVariant(buyProduct, selectedVariants.buy, bundle.buyQuantity);
|
|
2697
|
+
const getVariant = resolveVariant(getProduct, selectedVariants.get, bundle.getQuantity);
|
|
2698
|
+
const buyUnit = buyVariant ? parseFloat(buyVariant.price.amount) : 0;
|
|
2699
|
+
const getUnit = getVariant ? parseFloat(getVariant.price.amount) : 0;
|
|
2700
|
+
const comparePrice = buyUnit * bundle.buyQuantity + getUnit * bundle.getQuantity;
|
|
2701
|
+
const salePrice = buyUnit * bundle.buyQuantity + discountedUnit(getUnit, percent) * bundle.getQuantity;
|
|
2702
|
+
const savings = Math.max(0, comparePrice - salePrice);
|
|
2703
|
+
const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
|
|
2704
|
+
const showCompare = bundle.widgetConfig.pricing.showComparePrice && savings > 0;
|
|
2705
|
+
const showSavings = bundle.widgetConfig.savingsBar.visible && savings > 0;
|
|
2706
|
+
const badge = percent === 100 ? "Free" : `${percent}% off`;
|
|
2707
|
+
return /* @__PURE__ */ jsxs6(
|
|
2708
|
+
"div",
|
|
2709
|
+
{
|
|
2710
|
+
ref: (el) => {
|
|
2711
|
+
elementRef(el);
|
|
2712
|
+
setConfigVarsRef(el);
|
|
2713
|
+
},
|
|
2714
|
+
className: `lb-bundle lb-bundle--bogo ${className ?? ""}`,
|
|
2715
|
+
role: "region",
|
|
2716
|
+
"aria-label": bundle.title,
|
|
2717
|
+
children: [
|
|
2718
|
+
/* @__PURE__ */ jsx6("div", { className: "lb-bundle-header", children: /* @__PURE__ */ jsxs6("div", { className: "lb-bundle-header__content", children: [
|
|
2719
|
+
/* @__PURE__ */ jsx6("h3", { className: "lb-bundle-title", children: bundle.title }),
|
|
2720
|
+
bundle.description && /* @__PURE__ */ jsx6("p", { className: "lb-bundle-subtitle", children: bundle.description })
|
|
2721
|
+
] }) }),
|
|
2722
|
+
/* @__PURE__ */ jsxs6("div", { className: "lb-bundle__products", children: [
|
|
2723
|
+
/* @__PURE__ */ jsx6(
|
|
2724
|
+
BogoProductRow,
|
|
2725
|
+
{
|
|
2726
|
+
side: "buy",
|
|
2727
|
+
product: buyProduct,
|
|
2728
|
+
quantity: bundle.buyQuantity,
|
|
2729
|
+
percent,
|
|
2730
|
+
badge: null,
|
|
2731
|
+
currency,
|
|
2732
|
+
onVariantChange: handleVariantChange
|
|
2733
|
+
}
|
|
2734
|
+
),
|
|
2735
|
+
/* @__PURE__ */ jsx6("div", { className: "lb-bogo__plus", "aria-hidden": "true", children: /* @__PURE__ */ jsxs6("svg", { width: "12", height: "12", viewBox: "0 0 18 18", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
2736
|
+
/* @__PURE__ */ jsx6("line", { x1: "9", y1: "3", x2: "9", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
2737
|
+
/* @__PURE__ */ jsx6("line", { x1: "3", y1: "9", x2: "15", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
|
|
2738
|
+
] }) }),
|
|
2739
|
+
/* @__PURE__ */ jsx6(
|
|
2740
|
+
BogoProductRow,
|
|
2741
|
+
{
|
|
2742
|
+
side: "get",
|
|
2743
|
+
product: getProduct,
|
|
2744
|
+
quantity: bundle.getQuantity,
|
|
2745
|
+
percent,
|
|
2746
|
+
badge,
|
|
2747
|
+
currency,
|
|
2748
|
+
onVariantChange: handleVariantChange
|
|
2749
|
+
}
|
|
2750
|
+
)
|
|
2751
|
+
] }),
|
|
2752
|
+
/* @__PURE__ */ jsx6("div", { className: "lb-bundle-divider" }),
|
|
2753
|
+
/* @__PURE__ */ jsxs6("div", { className: "lb-bundle-summary", children: [
|
|
2754
|
+
/* @__PURE__ */ jsxs6("div", { className: "lb-bundle-summary__text", children: [
|
|
2755
|
+
/* @__PURE__ */ jsx6("span", { className: "lb-bundle-summary__label", children: "Bundle price" }),
|
|
2756
|
+
showSavings && /* @__PURE__ */ jsxs6("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
|
|
2757
|
+
"You save",
|
|
2758
|
+
" ",
|
|
2759
|
+
/* @__PURE__ */ jsx6("span", { "data-savings-amount": true, children: formatMoney5(savings, currency) }),
|
|
2760
|
+
" ",
|
|
2761
|
+
/* @__PURE__ */ jsxs6("span", { "data-savings-percent": true, children: [
|
|
2762
|
+
"(",
|
|
2763
|
+
savingsPercent,
|
|
2764
|
+
"%)"
|
|
2765
|
+
] })
|
|
2766
|
+
] })
|
|
2767
|
+
] }),
|
|
2768
|
+
/* @__PURE__ */ jsxs6("span", { className: "lb-bundle-summary__prices", children: [
|
|
2769
|
+
showCompare && /* @__PURE__ */ jsx6("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: formatMoney5(comparePrice, currency) }),
|
|
2770
|
+
/* @__PURE__ */ jsx6("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: formatMoney5(salePrice, currency) })
|
|
2771
|
+
] })
|
|
2772
|
+
] }),
|
|
2773
|
+
cartError && /* @__PURE__ */ jsx6("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
2774
|
+
/* @__PURE__ */ jsx6(
|
|
2775
|
+
"button",
|
|
2776
|
+
{
|
|
2777
|
+
className: "lb-bundle__cta",
|
|
2778
|
+
onClick: handleAddToCart,
|
|
2779
|
+
disabled: addingToCart || oosSides > 0,
|
|
2780
|
+
"aria-busy": addingToCart,
|
|
2781
|
+
children: addingToCart ? "Adding..." : oosSides > 0 ? `${oosSides} item${oosSides === 1 ? "" : "s"} unavailable` : bundle.widgetConfig.cta.ctaText ?? "Add Bundle to Cart"
|
|
2782
|
+
}
|
|
2783
|
+
)
|
|
2784
|
+
]
|
|
2785
|
+
}
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
function BogoProductRow({ side, product, quantity, percent, badge, currency, onVariantChange }) {
|
|
2789
|
+
const variants = product.variants.nodes;
|
|
2790
|
+
const optionNames = useMemo8(() => {
|
|
2791
|
+
const first = variants[0];
|
|
2792
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
2793
|
+
}, [variants]);
|
|
2794
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
2795
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({
|
|
2796
|
+
variants,
|
|
2797
|
+
optionNames
|
|
2798
|
+
});
|
|
2799
|
+
useEffect13(() => {
|
|
2800
|
+
onVariantChange(side, selectedVariant ?? null);
|
|
2801
|
+
}, [selectedVariant, side, onVariantChange]);
|
|
2802
|
+
const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable5(v, quantity)) ?? variants[0];
|
|
2803
|
+
const unit = displayVariant ? parseFloat(displayVariant.price.amount) : 0;
|
|
2804
|
+
const isGet = side === "get" && percent > 0;
|
|
2805
|
+
const priceText = isGet ? formatMoney5(discountedUnit(unit, percent), currency) : formatMoney5(unit, currency);
|
|
2806
|
+
const compareAt = isGet ? formatMoney5(unit, currency) : displayVariant?.compareAtPrice && parseFloat(displayVariant.compareAtPrice.amount) > unit ? formatMoney5(displayVariant.compareAtPrice.amount, currency) : null;
|
|
2807
|
+
const unitPriceText = displayVariant ? formatUnitPrice5(
|
|
2808
|
+
displayVariant.unitPrice,
|
|
2809
|
+
displayVariant.unitPriceMeasurement,
|
|
2810
|
+
currency
|
|
2811
|
+
) : null;
|
|
2812
|
+
const thumbImage = displayVariant?.image ?? product.featuredImage;
|
|
2813
|
+
return /* @__PURE__ */ jsxs6("div", { className: "lb-bundle-product-row", part: "product", "data-bogo-role": side, children: [
|
|
2814
|
+
thumbImage && /* @__PURE__ */ jsx6(
|
|
2815
|
+
"img",
|
|
2816
|
+
{
|
|
2817
|
+
src: thumbImage.url,
|
|
2818
|
+
alt: thumbImage.altText ?? product.title,
|
|
2819
|
+
className: "lb-bundle__product-image",
|
|
2820
|
+
loading: "lazy"
|
|
2821
|
+
}
|
|
2822
|
+
),
|
|
2823
|
+
/* @__PURE__ */ jsxs6("div", { className: "lb-bundle__product-info", children: [
|
|
2824
|
+
/* @__PURE__ */ jsx6("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
2825
|
+
!showPicker && displayVariant && variants.length > 1 && /* @__PURE__ */ jsx6("span", { className: "lb-bundle-variant-badge", children: displayVariant.title }),
|
|
2826
|
+
/* @__PURE__ */ jsxs6("span", { className: "lb-bundle-product-prices", children: [
|
|
2827
|
+
compareAt && /* @__PURE__ */ jsx6(
|
|
2828
|
+
"span",
|
|
2829
|
+
{
|
|
2830
|
+
className: "lb-bundle-product-compare-price",
|
|
2831
|
+
"data-product-compare-price": true,
|
|
2832
|
+
children: compareAt
|
|
2833
|
+
}
|
|
2834
|
+
),
|
|
2835
|
+
/* @__PURE__ */ jsx6("span", { className: "lb-bundle__product-price", "data-product-price": true, children: priceText }),
|
|
2836
|
+
/* @__PURE__ */ jsxs6("span", { className: "lb-bundle-qty-inline", "data-qty-inline": true, children: [
|
|
2837
|
+
"\xD7",
|
|
2838
|
+
quantity
|
|
2839
|
+
] })
|
|
2840
|
+
] }),
|
|
2841
|
+
unitPriceText && /* @__PURE__ */ jsx6(
|
|
2842
|
+
"span",
|
|
2843
|
+
{
|
|
2844
|
+
className: "lb-bundle__product-unit-price",
|
|
2845
|
+
"data-product-unit-price": true,
|
|
2846
|
+
children: unitPriceText
|
|
2847
|
+
}
|
|
2848
|
+
),
|
|
2849
|
+
showPicker && /* @__PURE__ */ jsx6("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
2850
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
2851
|
+
value: o.value,
|
|
2852
|
+
label: o.value,
|
|
2853
|
+
disabled: o.disabled
|
|
2854
|
+
}));
|
|
2855
|
+
return /* @__PURE__ */ jsx6(
|
|
2856
|
+
VariantDropdown,
|
|
2857
|
+
{
|
|
2858
|
+
options: dropdownOptions,
|
|
2859
|
+
value: selectedValues[optionIndex] ?? null,
|
|
2860
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
2861
|
+
ariaLabel: optionName
|
|
2862
|
+
},
|
|
2863
|
+
optionName
|
|
2864
|
+
);
|
|
2865
|
+
}) })
|
|
2866
|
+
] }),
|
|
2867
|
+
badge && /* @__PURE__ */ jsx6("span", { className: "lb-bogo__badge", children: badge })
|
|
2868
|
+
] });
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/components/MultiStepBundle.tsx
|
|
2872
|
+
import { useCallback as useCallback9, useEffect as useEffect15, useMemo as useMemo10, useState as useState12 } from "react";
|
|
2873
|
+
import {
|
|
2874
|
+
formatMoney as formatMoney7,
|
|
2875
|
+
formatUnitPrice as formatUnitPrice7,
|
|
2876
|
+
calculateDiscount as calculateDiscount3,
|
|
2877
|
+
isVariantFulfillable as isVariantFulfillable7,
|
|
2878
|
+
productsForStep as productsForStep2,
|
|
2879
|
+
stepHeadroom as stepHeadroom2,
|
|
2880
|
+
DEFAULT_PRODUCT_RULE as DEFAULT_PRODUCT_RULE4
|
|
2881
|
+
} from "@lime-bundles/core";
|
|
2882
|
+
|
|
2883
|
+
// src/components/MultiStepPicker.tsx
|
|
2884
|
+
import {
|
|
2885
|
+
useEffect as useEffect14,
|
|
2886
|
+
useMemo as useMemo9,
|
|
2887
|
+
useRef as useRef7,
|
|
2888
|
+
useState as useState11
|
|
2889
|
+
} from "react";
|
|
2890
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
2891
|
+
import {
|
|
2892
|
+
formatMoney as formatMoney6,
|
|
2893
|
+
formatUnitPrice as formatUnitPrice6,
|
|
2894
|
+
isVariantFulfillable as isVariantFulfillable6,
|
|
2895
|
+
maxAddableQuantity as maxAddableQuantity2,
|
|
2896
|
+
productsForStep,
|
|
2897
|
+
stepHeadroom,
|
|
2898
|
+
DEFAULT_PRODUCT_RULE as DEFAULT_PRODUCT_RULE3
|
|
2899
|
+
} from "@lime-bundles/core";
|
|
2900
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2901
|
+
function ruleFor3(bundle, productId) {
|
|
2902
|
+
return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE3;
|
|
2903
|
+
}
|
|
2904
|
+
function sanitizeId2(gid) {
|
|
2905
|
+
return gid.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
2906
|
+
}
|
|
2907
|
+
function buildEligibleProducts2(bundle, stepIndex, oosBehavior) {
|
|
2908
|
+
const result = [];
|
|
2909
|
+
for (const product of productsForStep(bundle, stepIndex)) {
|
|
2910
|
+
const rule = ruleFor3(bundle, product.id);
|
|
2911
|
+
const available = product.variants.nodes.filter(
|
|
2912
|
+
(v) => isVariantFulfillable6(v, rule.min)
|
|
2913
|
+
);
|
|
2914
|
+
const isOos = available.length === 0;
|
|
2915
|
+
if (isOos && oosBehavior === "hide") continue;
|
|
2916
|
+
result.push({
|
|
2917
|
+
product,
|
|
2918
|
+
variants: product.variants.nodes,
|
|
2919
|
+
firstAvailableVariant: available[0] ?? null,
|
|
2920
|
+
isOos
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
return result;
|
|
2924
|
+
}
|
|
2925
|
+
function normalizeText2(str) {
|
|
2926
|
+
return str.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
2927
|
+
}
|
|
2928
|
+
var FILTERS_ALL2 = "__all__";
|
|
2929
|
+
function MultiStepPicker({
|
|
2930
|
+
outOfStockBehavior,
|
|
2931
|
+
bundle,
|
|
2932
|
+
currency,
|
|
2933
|
+
initialStep,
|
|
2934
|
+
showStepper,
|
|
2935
|
+
selections,
|
|
2936
|
+
onAdd,
|
|
2937
|
+
onUpdateQuantity,
|
|
2938
|
+
onRemoveVariant,
|
|
2939
|
+
canRemoveVariant,
|
|
2940
|
+
swapUnitsFor,
|
|
2941
|
+
onSwap,
|
|
2942
|
+
onClose,
|
|
2943
|
+
styleVarsRef
|
|
2944
|
+
}) {
|
|
2945
|
+
const wc = bundle.widgetConfig;
|
|
2946
|
+
const steps = bundle.steps;
|
|
2947
|
+
const overlayRef = useRef7(null);
|
|
2948
|
+
const dialogRef = useRef7(null);
|
|
2949
|
+
const titleRef = useRef7(null);
|
|
2950
|
+
const mouseDownTarget = useRef7(null);
|
|
2951
|
+
const [open, setOpen] = useState11(false);
|
|
2952
|
+
const [stepIndex, setStepIndex] = useState11(
|
|
2953
|
+
() => Math.max(0, Math.min(steps.length - 1, initialStep))
|
|
2954
|
+
);
|
|
2955
|
+
const [query, setQuery] = useState11("");
|
|
2956
|
+
const [activeType, setActiveType] = useState11(FILTERS_ALL2);
|
|
2957
|
+
const [announcement, setAnnouncement] = useState11("");
|
|
2958
|
+
const step = steps[stepIndex];
|
|
2959
|
+
const stepMin = step?.minQuantity ?? 1;
|
|
2960
|
+
const stepPicks = selections[stepIndex] ?? [];
|
|
2961
|
+
const unitsInStep = stepPicks.reduce((sum, s) => sum + s.quantity, 0);
|
|
2962
|
+
const stepIsSatisfied = unitsInStep >= stepMin;
|
|
2963
|
+
const isLastStep = stepIndex === steps.length - 1;
|
|
2964
|
+
const shownUnits = Math.min(unitsInStep, stepMin);
|
|
2965
|
+
const eligible = useMemo9(
|
|
2966
|
+
() => buildEligibleProducts2(bundle, stepIndex, outOfStockBehavior),
|
|
2967
|
+
[bundle, stepIndex, outOfStockBehavior]
|
|
2968
|
+
);
|
|
2969
|
+
const productTypes = useMemo9(() => {
|
|
2970
|
+
const types = [];
|
|
2971
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2972
|
+
for (const ep of eligible) {
|
|
2973
|
+
const ty = (ep.product.productType ?? "").trim();
|
|
2974
|
+
if (ty && !seen.has(ty)) {
|
|
2975
|
+
seen.add(ty);
|
|
2976
|
+
types.push(ty);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
return types;
|
|
2980
|
+
}, [eligible]);
|
|
2981
|
+
const showFilters = wc.mixMatchShowTypeFilters && productTypes.length >= 2;
|
|
2982
|
+
useEffect14(() => {
|
|
2983
|
+
const id = requestAnimationFrame(() => setOpen(true));
|
|
2984
|
+
return () => cancelAnimationFrame(id);
|
|
2985
|
+
}, []);
|
|
2986
|
+
useScrollLock(true);
|
|
2987
|
+
useFocusTrap({
|
|
2988
|
+
active: true,
|
|
2989
|
+
containerRef: dialogRef,
|
|
2990
|
+
onEscape: onClose
|
|
2991
|
+
});
|
|
2992
|
+
function goToStep(i) {
|
|
2993
|
+
const clamped = Math.max(0, Math.min(steps.length - 1, i));
|
|
2994
|
+
setStepIndex(clamped);
|
|
2995
|
+
setQuery("");
|
|
2996
|
+
setActiveType(FILTERS_ALL2);
|
|
2997
|
+
const target = steps[clamped];
|
|
2998
|
+
const targetPicks = selections[clamped] ?? [];
|
|
2999
|
+
const targetUnits = targetPicks.reduce((sum, s) => sum + s.quantity, 0);
|
|
3000
|
+
setAnnouncement(
|
|
3001
|
+
`Step ${clamped + 1} of ${steps.length}. ${target.name}. ${Math.min(targetUnits, target.minQuantity)} of ${target.minQuantity} added`
|
|
3002
|
+
);
|
|
3003
|
+
titleRef.current?.focus();
|
|
3004
|
+
}
|
|
3005
|
+
const subtitle = `Step ${stepIndex + 1} of ${steps.length} \xB7 ${step?.name ?? ""}`;
|
|
3006
|
+
const filtered = useMemo9(() => {
|
|
3007
|
+
const nq = normalizeText2(query.trim());
|
|
3008
|
+
return eligible.filter((ep) => {
|
|
3009
|
+
const matchesQuery = !nq || normalizeText2(ep.product.title).includes(nq);
|
|
3010
|
+
const matchesType = activeType === FILTERS_ALL2 || (ep.product.productType ?? "") === activeType;
|
|
3011
|
+
return matchesQuery && matchesType;
|
|
3012
|
+
});
|
|
3013
|
+
}, [eligible, query, activeType]);
|
|
3014
|
+
const showEmpty = filtered.length === 0 && query.trim().length > 0;
|
|
3015
|
+
function onOverlayMouseDown(e) {
|
|
3016
|
+
mouseDownTarget.current = e.target;
|
|
3017
|
+
}
|
|
3018
|
+
function onOverlayMouseUp(e) {
|
|
3019
|
+
if (e.target === overlayRef.current && mouseDownTarget.current === overlayRef.current) {
|
|
3020
|
+
onClose();
|
|
3021
|
+
}
|
|
3022
|
+
mouseDownTarget.current = null;
|
|
3023
|
+
}
|
|
3024
|
+
const titleId = `lb-modal-title-${sanitizeId2(bundle.id)}`;
|
|
3025
|
+
const modal = (
|
|
3026
|
+
// The overlay is a backdrop: click-outside-to-close is a mouse convenience;
|
|
3027
|
+
// keyboard users close via Escape (handled by useFocusTrap), and the dialog
|
|
3028
|
+
// inside carries the interactive role + focus trap. So the static-element
|
|
3029
|
+
// interaction lint does not apply to this backdrop.
|
|
3030
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
3031
|
+
/* @__PURE__ */ jsx7(
|
|
3032
|
+
"div",
|
|
3033
|
+
{
|
|
3034
|
+
ref: (el) => {
|
|
3035
|
+
overlayRef.current = el;
|
|
3036
|
+
styleVarsRef(el);
|
|
3037
|
+
},
|
|
3038
|
+
className: `lb-mix-match__modal-overlay${open ? " lb-mix-match__modal-overlay--open" : ""}`,
|
|
3039
|
+
"data-modal-overlay": true,
|
|
3040
|
+
"data-bundle-gid": bundle.id,
|
|
3041
|
+
onMouseDown: onOverlayMouseDown,
|
|
3042
|
+
onMouseUp: onOverlayMouseUp,
|
|
3043
|
+
children: /* @__PURE__ */ jsxs7(
|
|
3044
|
+
"div",
|
|
3045
|
+
{
|
|
3046
|
+
ref: dialogRef,
|
|
3047
|
+
className: `lb-mix-match__modal${showFilters ? "" : " lb-mix-match__modal--filters-hidden"}`,
|
|
3048
|
+
role: "dialog",
|
|
3049
|
+
"aria-modal": "true",
|
|
3050
|
+
"aria-labelledby": titleId,
|
|
3051
|
+
tabIndex: -1,
|
|
3052
|
+
children: [
|
|
3053
|
+
/* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-header", children: [
|
|
3054
|
+
/* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-header-top", children: [
|
|
3055
|
+
/* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-heading", children: [
|
|
3056
|
+
/* @__PURE__ */ jsx7(
|
|
3057
|
+
"h4",
|
|
3058
|
+
{
|
|
3059
|
+
className: "lb-mix-match__modal-title",
|
|
3060
|
+
id: titleId,
|
|
3061
|
+
tabIndex: -1,
|
|
3062
|
+
ref: titleRef,
|
|
3063
|
+
children: "Add to your bundle"
|
|
3064
|
+
}
|
|
3065
|
+
),
|
|
3066
|
+
/* @__PURE__ */ jsx7("p", { className: "lb-mix-match__modal-subtitle", "data-modal-subtitle": true, children: subtitle })
|
|
3067
|
+
] }),
|
|
3068
|
+
/* @__PURE__ */ jsx7(
|
|
3069
|
+
"button",
|
|
3070
|
+
{
|
|
3071
|
+
type: "button",
|
|
3072
|
+
className: "lb-mix-match__modal-close",
|
|
3073
|
+
"data-modal-close": true,
|
|
3074
|
+
"aria-label": "Close",
|
|
3075
|
+
onClick: onClose,
|
|
3076
|
+
children: /* @__PURE__ */ jsx7(CloseIcon3, {})
|
|
3077
|
+
}
|
|
3078
|
+
)
|
|
3079
|
+
] }),
|
|
3080
|
+
/* @__PURE__ */ jsx7(
|
|
3081
|
+
"div",
|
|
3082
|
+
{
|
|
3083
|
+
className: "lb-mix-match__progress lb-mix-match__modal-progress",
|
|
3084
|
+
"data-progress": true,
|
|
3085
|
+
children: /* @__PURE__ */ jsx7(
|
|
3086
|
+
"div",
|
|
3087
|
+
{
|
|
3088
|
+
className: "lb-mix-match__progress-segments",
|
|
3089
|
+
"data-modal-step-segments": true,
|
|
3090
|
+
role: "progressbar",
|
|
3091
|
+
"aria-valuenow": shownUnits,
|
|
3092
|
+
"aria-valuemin": 0,
|
|
3093
|
+
"aria-valuemax": stepMin,
|
|
3094
|
+
children: Array.from({ length: stepMin }).map((_, i) => /* @__PURE__ */ jsx7(
|
|
3095
|
+
"span",
|
|
3096
|
+
{
|
|
3097
|
+
className: `lb-mix-match__progress-segment${i < shownUnits ? " lb-mix-match__progress-segment--filled" : ""}`,
|
|
3098
|
+
"data-progress-segment": true
|
|
3099
|
+
},
|
|
3100
|
+
i
|
|
3101
|
+
))
|
|
3102
|
+
}
|
|
3103
|
+
)
|
|
3104
|
+
}
|
|
3105
|
+
)
|
|
3106
|
+
] }),
|
|
3107
|
+
wc.showSearch && /* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-search", children: [
|
|
3108
|
+
/* @__PURE__ */ jsx7(
|
|
3109
|
+
"input",
|
|
3110
|
+
{
|
|
3111
|
+
type: "text",
|
|
3112
|
+
className: "lb-mix-match__modal-search-input",
|
|
3113
|
+
"data-modal-search": true,
|
|
3114
|
+
role: "searchbox",
|
|
3115
|
+
"aria-label": "Search products",
|
|
3116
|
+
placeholder: "Search products",
|
|
3117
|
+
autoComplete: "off",
|
|
3118
|
+
value: query,
|
|
3119
|
+
onChange: (e) => setQuery(e.target.value)
|
|
3120
|
+
}
|
|
3121
|
+
),
|
|
3122
|
+
query.length > 0 && /* @__PURE__ */ jsx7(
|
|
3123
|
+
"button",
|
|
3124
|
+
{
|
|
3125
|
+
type: "button",
|
|
3126
|
+
className: "lb-mix-match__modal-search-clear",
|
|
3127
|
+
"data-modal-search-clear": true,
|
|
3128
|
+
"aria-label": "Clear search",
|
|
3129
|
+
onClick: () => setQuery(""),
|
|
3130
|
+
children: /* @__PURE__ */ jsx7(SearchClearIcon2, {})
|
|
3131
|
+
}
|
|
3132
|
+
)
|
|
3133
|
+
] }),
|
|
3134
|
+
showFilters && /* @__PURE__ */ jsx7(
|
|
3135
|
+
"div",
|
|
3136
|
+
{
|
|
3137
|
+
className: "lb-mix-match__filters",
|
|
3138
|
+
"data-modal-filters": true,
|
|
3139
|
+
role: "group",
|
|
3140
|
+
"aria-label": "Filter by product type",
|
|
3141
|
+
children: [FILTERS_ALL2, ...productTypes].map((value) => {
|
|
3142
|
+
const isActive = value === activeType;
|
|
3143
|
+
return /* @__PURE__ */ jsx7(
|
|
3144
|
+
"button",
|
|
3145
|
+
{
|
|
3146
|
+
type: "button",
|
|
3147
|
+
className: `lb-mix-match__filter${isActive ? " lb-mix-match__filter--active" : ""}`,
|
|
3148
|
+
"data-filter": value,
|
|
3149
|
+
"aria-pressed": isActive,
|
|
3150
|
+
onClick: () => setActiveType(value),
|
|
3151
|
+
children: value === FILTERS_ALL2 ? "All" : value
|
|
3152
|
+
},
|
|
3153
|
+
value
|
|
3154
|
+
);
|
|
3155
|
+
})
|
|
3156
|
+
}
|
|
3157
|
+
),
|
|
3158
|
+
/* @__PURE__ */ jsx7("div", { className: "lb-mix-match__modal-list", "data-modal-list": true, children: filtered.map((ep) => /* @__PURE__ */ jsx7(
|
|
3159
|
+
MultiStepPickerRow,
|
|
3160
|
+
{
|
|
3161
|
+
bundle,
|
|
3162
|
+
eligible: ep,
|
|
3163
|
+
currency,
|
|
3164
|
+
showStepper,
|
|
3165
|
+
stepIndex,
|
|
3166
|
+
selections,
|
|
3167
|
+
onAdd,
|
|
3168
|
+
swapUnitsFor,
|
|
3169
|
+
onSwap,
|
|
3170
|
+
onUpdateQuantity,
|
|
3171
|
+
onRemoveVariant,
|
|
3172
|
+
canRemoveVariant
|
|
3173
|
+
},
|
|
3174
|
+
`${stepIndex}:${ep.product.id}`
|
|
3175
|
+
)) }),
|
|
3176
|
+
showEmpty && /* @__PURE__ */ jsx7("div", { className: "lb-mix-match__modal-empty", "data-modal-empty": true, children: /* @__PURE__ */ jsx7("p", { children: "No products match your search" }) }),
|
|
3177
|
+
/* @__PURE__ */ jsx7("span", { "data-modal-live": true, "aria-live": "polite", className: "lb-visually-hidden", children: announcement || (query.trim() ? `${filtered.length} products shown` : "") }),
|
|
3178
|
+
/* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-footer lb-multi-step__modal-footer", children: [
|
|
3179
|
+
stepIndex > 0 && /* @__PURE__ */ jsx7(
|
|
3180
|
+
"button",
|
|
3181
|
+
{
|
|
3182
|
+
type: "button",
|
|
3183
|
+
className: "lb-multi-step__modal-back",
|
|
3184
|
+
"data-modal-back": true,
|
|
3185
|
+
onClick: () => goToStep(stepIndex - 1),
|
|
3186
|
+
children: "Back"
|
|
3187
|
+
}
|
|
3188
|
+
),
|
|
3189
|
+
/* @__PURE__ */ jsx7(
|
|
3190
|
+
"span",
|
|
3191
|
+
{
|
|
3192
|
+
className: "lb-mix-match__modal-footer-count",
|
|
3193
|
+
"data-modal-footer-count": true,
|
|
3194
|
+
"aria-live": "polite",
|
|
3195
|
+
children: `${shownUnits} of ${stepMin} added`
|
|
3196
|
+
}
|
|
3197
|
+
),
|
|
3198
|
+
!isLastStep && /* @__PURE__ */ jsx7(
|
|
3199
|
+
"button",
|
|
3200
|
+
{
|
|
3201
|
+
type: "button",
|
|
3202
|
+
className: "lb-mix-match__modal-done",
|
|
3203
|
+
"data-modal-next": true,
|
|
3204
|
+
disabled: !stepIsSatisfied,
|
|
3205
|
+
onClick: () => {
|
|
3206
|
+
if (stepIsSatisfied) goToStep(stepIndex + 1);
|
|
3207
|
+
},
|
|
3208
|
+
children: "Next"
|
|
3209
|
+
}
|
|
3210
|
+
),
|
|
3211
|
+
isLastStep && /* @__PURE__ */ jsx7(
|
|
3212
|
+
"button",
|
|
3213
|
+
{
|
|
3214
|
+
type: "button",
|
|
3215
|
+
className: "lb-mix-match__modal-done",
|
|
3216
|
+
"data-modal-done": true,
|
|
3217
|
+
disabled: !stepIsSatisfied,
|
|
3218
|
+
onClick: onClose,
|
|
3219
|
+
children: "Done"
|
|
3220
|
+
}
|
|
3221
|
+
)
|
|
3222
|
+
] })
|
|
3223
|
+
]
|
|
3224
|
+
}
|
|
3225
|
+
)
|
|
3226
|
+
}
|
|
3227
|
+
)
|
|
3228
|
+
);
|
|
3229
|
+
if (typeof document === "undefined") return null;
|
|
3230
|
+
return createPortal2(modal, document.body);
|
|
3231
|
+
}
|
|
3232
|
+
function MultiStepPickerRow({
|
|
3233
|
+
bundle,
|
|
3234
|
+
eligible,
|
|
3235
|
+
currency,
|
|
3236
|
+
showStepper,
|
|
3237
|
+
stepIndex,
|
|
3238
|
+
selections,
|
|
3239
|
+
onAdd,
|
|
3240
|
+
onUpdateQuantity,
|
|
3241
|
+
onRemoveVariant,
|
|
3242
|
+
canRemoveVariant,
|
|
3243
|
+
swapUnitsFor,
|
|
3244
|
+
onSwap
|
|
3245
|
+
}) {
|
|
3246
|
+
const { product, variants, isOos } = eligible;
|
|
3247
|
+
const rule = ruleFor3(bundle, product.id);
|
|
3248
|
+
const step = bundle.steps[stepIndex];
|
|
3249
|
+
const optionNames = useMemo9(() => {
|
|
3250
|
+
const first = variants[0];
|
|
3251
|
+
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
3252
|
+
}, [variants]);
|
|
3253
|
+
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
3254
|
+
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
3255
|
+
const currentVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable6(v, rule.min)) ?? eligible.firstAvailableVariant ?? variants[0] ?? null;
|
|
3256
|
+
const stepPicks = useMemo9(
|
|
3257
|
+
() => selections[stepIndex] ?? [],
|
|
3258
|
+
[selections, stepIndex]
|
|
3259
|
+
);
|
|
3260
|
+
const unitsInStep = stepPicks.reduce((sum, s) => sum + s.quantity, 0);
|
|
3261
|
+
const committedSelection = useMemo9(
|
|
3262
|
+
() => currentVariant ? stepPicks.find((s) => s.variantId === currentVariant.id) ?? null : null,
|
|
3263
|
+
[stepPicks, currentVariant]
|
|
3264
|
+
);
|
|
3265
|
+
const variantInStep = committedSelection !== null;
|
|
3266
|
+
const committedQty = committedSelection?.quantity ?? 0;
|
|
3267
|
+
const productOtherUnits = useMemo9(() => {
|
|
3268
|
+
if (!currentVariant) return 0;
|
|
3269
|
+
let sum = 0;
|
|
3270
|
+
for (let i = 0; i < selections.length; i++) {
|
|
3271
|
+
for (const s of selections[i]) {
|
|
3272
|
+
if (s.productId !== product.id) continue;
|
|
3273
|
+
if (i === stepIndex && s.variantId === currentVariant.id) continue;
|
|
3274
|
+
sum += s.quantity;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
return sum;
|
|
3278
|
+
}, [selections, product.id, currentVariant, stepIndex]);
|
|
3279
|
+
const variantOtherUnits = useMemo9(() => {
|
|
3280
|
+
if (!currentVariant) return 0;
|
|
3281
|
+
let sum = 0;
|
|
3282
|
+
for (const picks of selections) {
|
|
3283
|
+
for (const s of picks) {
|
|
3284
|
+
if (s.variantId === currentVariant.id) sum += s.quantity;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return Math.max(0, sum - committedQty);
|
|
3288
|
+
}, [selections, currentVariant, committedQty]);
|
|
3289
|
+
const swapUnits = currentVariant ? swapUnitsFor?.(stepIndex, product.id, currentVariant.id) ?? 0 : 0;
|
|
3290
|
+
const inSwapState = swapUnits >= rule.min;
|
|
3291
|
+
const room = stepHeadroom(step?.maxQuantity ?? null, unitsInStep);
|
|
3292
|
+
const productHeadroom = rule.max - productOtherUnits;
|
|
3293
|
+
const spotCeiling = inSwapState ? swapUnits : Math.min(
|
|
3294
|
+
productHeadroom,
|
|
3295
|
+
room === Number.POSITIVE_INFINITY ? productHeadroom : committedQty + room
|
|
3296
|
+
);
|
|
3297
|
+
const stockRemaining = currentVariant ? maxAddableQuantity2(currentVariant, Number.MAX_SAFE_INTEGER, variantOtherUnits) : 0;
|
|
3298
|
+
const cap = Math.max(0, Math.min(spotCeiling, stockRemaining));
|
|
3299
|
+
const stepperMax = Math.max(rule.min, cap);
|
|
3300
|
+
const [qty, setQty] = useState11(rule.min);
|
|
3301
|
+
useEffect14(() => {
|
|
3302
|
+
setQty((prev) => Math.max(rule.min, Math.min(prev, stepperMax)));
|
|
3303
|
+
}, [stepperMax, rule.min]);
|
|
3304
|
+
const currentVariantId = currentVariant?.id ?? null;
|
|
3305
|
+
useEffect14(() => {
|
|
3306
|
+
if (!variantInStep) setQty(rule.min);
|
|
3307
|
+
}, [variantInStep, currentVariantId, rule.min]);
|
|
3308
|
+
if (!currentVariant || !step) return null;
|
|
3309
|
+
const thumbImage = currentVariant.image ?? product.featuredImage ?? null;
|
|
3310
|
+
const unitPriceText = formatUnitPrice6(
|
|
3311
|
+
currentVariant.unitPrice,
|
|
3312
|
+
currentVariant.unitPriceMeasurement,
|
|
3313
|
+
currency
|
|
3314
|
+
);
|
|
3315
|
+
const noStock = cap < rule.min;
|
|
3316
|
+
const needsMoreSpots = !variantInStep && !inSwapState && room !== Number.POSITIVE_INFINITY && rule.min > room;
|
|
3317
|
+
const addDisabled = !variantInStep && (inSwapState ? noStock : noStock || needsMoreSpots);
|
|
3318
|
+
const lockedRequired = variantInStep && currentVariant != null && !(canRemoveVariant?.(currentVariant.id) ?? true);
|
|
3319
|
+
const stepperDisabled = isOos || !variantInStep && addDisabled;
|
|
3320
|
+
const shownQty = variantInStep ? committedQty : qty;
|
|
3321
|
+
function handleAddOrRemove() {
|
|
3322
|
+
if (variantInStep && currentVariant) {
|
|
3323
|
+
onRemoveVariant(stepIndex, currentVariant.id);
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
if (!currentVariant || noStock) return;
|
|
3327
|
+
const pickedQty = showStepper ? qty : rule.min;
|
|
3328
|
+
const item = {
|
|
3329
|
+
productId: product.id,
|
|
3330
|
+
variantId: currentVariant.id,
|
|
3331
|
+
title: product.title,
|
|
3332
|
+
url: `/products/${product.handle}`,
|
|
3333
|
+
variantTitle: currentVariant.title,
|
|
3334
|
+
featuredImage: thumbImage?.url ?? null,
|
|
3335
|
+
price: parseFloat(currentVariant.price.amount),
|
|
3336
|
+
compareAtPrice: currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null,
|
|
3337
|
+
unitPrice: unitPriceText,
|
|
3338
|
+
quantity: pickedQty
|
|
3339
|
+
};
|
|
3340
|
+
if (inSwapState && onSwap) {
|
|
3341
|
+
onSwap(stepIndex, { ...item, quantity: Math.min(pickedQty, swapUnits) });
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
if (needsMoreSpots) return;
|
|
3345
|
+
onAdd(stepIndex, item);
|
|
3346
|
+
}
|
|
3347
|
+
const priceCents = parseFloat(currentVariant.price.amount);
|
|
3348
|
+
const compareCents = currentVariant.compareAtPrice ? parseFloat(currentVariant.compareAtPrice.amount) : null;
|
|
3349
|
+
return /* @__PURE__ */ jsxs7(
|
|
3350
|
+
"div",
|
|
3351
|
+
{
|
|
3352
|
+
className: `lb-mix-match__modal-product${isOos ? " lb-mix-match__modal-product--sold-out" : ""}${variantInStep ? " lb-mix-match__modal-product--in-bundle" : ""}`,
|
|
3353
|
+
"data-product-item": true,
|
|
3354
|
+
"data-title": normalizeText2(product.title),
|
|
3355
|
+
"data-type": product.productType ?? "",
|
|
3356
|
+
"aria-disabled": isOos || void 0,
|
|
3357
|
+
children: [
|
|
3358
|
+
/* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-product-thumb", children: [
|
|
3359
|
+
thumbImage && /* @__PURE__ */ jsx7(
|
|
3360
|
+
"img",
|
|
3361
|
+
{
|
|
3362
|
+
src: thumbImage.url,
|
|
3363
|
+
alt: thumbImage.altText ?? product.title,
|
|
3364
|
+
loading: "lazy"
|
|
3365
|
+
}
|
|
3366
|
+
),
|
|
3367
|
+
variantInStep && /* @__PURE__ */ jsx7("span", { className: "lb-mix-match__modal-added-badge", children: "Added" })
|
|
3368
|
+
] }),
|
|
3369
|
+
/* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-product-info", children: [
|
|
3370
|
+
/* @__PURE__ */ jsx7("p", { className: "lb-mix-match__modal-product-title", children: /* @__PURE__ */ jsx7(
|
|
3371
|
+
"a",
|
|
3372
|
+
{
|
|
3373
|
+
href: `/products/${product.handle}`,
|
|
3374
|
+
target: "_blank",
|
|
3375
|
+
rel: "noopener noreferrer",
|
|
3376
|
+
children: product.title
|
|
3377
|
+
}
|
|
3378
|
+
) }),
|
|
3379
|
+
/* @__PURE__ */ jsxs7("p", { className: "lb-mix-match__modal-product-price", children: [
|
|
3380
|
+
/* @__PURE__ */ jsx7("span", { "data-row-price-sale": true, children: formatMoney6(priceCents, currency) }),
|
|
3381
|
+
compareCents !== null && compareCents > priceCents && /* @__PURE__ */ jsx7("s", { className: "lb-mix-match__modal-product-compare", "data-row-compare": true, children: formatMoney6(compareCents, currency) })
|
|
3382
|
+
] }),
|
|
3383
|
+
unitPriceText && /* @__PURE__ */ jsx7("p", { className: "lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price", children: unitPriceText }),
|
|
3384
|
+
showPicker && /* @__PURE__ */ jsx7("div", { className: "lb-bundle-variant-option-groups", children: optionNames.map((optionName, optionIndex) => {
|
|
3385
|
+
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
3386
|
+
value: o.value,
|
|
3387
|
+
label: o.value,
|
|
3388
|
+
disabled: o.disabled
|
|
3389
|
+
}));
|
|
3390
|
+
return /* @__PURE__ */ jsxs7(
|
|
3391
|
+
"div",
|
|
3392
|
+
{
|
|
3393
|
+
className: "lb-bundle-variant-option-group",
|
|
3394
|
+
children: [
|
|
3395
|
+
/* @__PURE__ */ jsx7("span", { className: "lb-bundle-variant-option-label", children: optionName }),
|
|
3396
|
+
/* @__PURE__ */ jsx7(
|
|
3397
|
+
VariantDropdown,
|
|
3398
|
+
{
|
|
3399
|
+
className: "lb-mix-match__variant-select-dropdown",
|
|
3400
|
+
options: dropdownOptions,
|
|
3401
|
+
value: selectedValues[optionIndex] ?? null,
|
|
3402
|
+
onChange: (v) => setOptionValue(optionIndex, v),
|
|
3403
|
+
ariaLabel: optionName
|
|
3404
|
+
}
|
|
3405
|
+
)
|
|
3406
|
+
]
|
|
3407
|
+
},
|
|
3408
|
+
optionName
|
|
3409
|
+
);
|
|
3410
|
+
}) }),
|
|
3411
|
+
!showPicker && variants.length === 1 && currentVariant.title !== "Default Title" && /* @__PURE__ */ jsx7("span", { className: "lb-mix-match__filled-variant", children: currentVariant.title }),
|
|
3412
|
+
!isOos && needsMoreSpots && room > 0 && /* @__PURE__ */ jsx7("span", { className: "lb-mix-match__modal-needs-spots", children: rule.min === 1 ? "Needs 1 spot" : `Needs ${rule.min} spots` }),
|
|
3413
|
+
isOos ? /* @__PURE__ */ jsx7("span", { className: "lb-mix-match__modal-sold-out-label", children: "Sold out" }) : /* @__PURE__ */ jsxs7("div", { className: "lb-mix-match__modal-product-actions", children: [
|
|
3414
|
+
showStepper && /* @__PURE__ */ jsx7("div", { className: "lb-bundle-variant-option-group lb-mix-match__qty-stepper-group", children: /* @__PURE__ */ jsxs7(
|
|
3415
|
+
"div",
|
|
3416
|
+
{
|
|
3417
|
+
className: "lb-mix-match__qty-stepper",
|
|
3418
|
+
role: "group",
|
|
3419
|
+
"aria-label": "Quantity",
|
|
3420
|
+
children: [
|
|
3421
|
+
/* @__PURE__ */ jsx7(
|
|
3422
|
+
"button",
|
|
3423
|
+
{
|
|
3424
|
+
type: "button",
|
|
3425
|
+
className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus",
|
|
3426
|
+
"aria-label": "Decrease quantity",
|
|
3427
|
+
disabled: stepperDisabled || shownQty <= rule.min,
|
|
3428
|
+
onClick: () => {
|
|
3429
|
+
if (variantInStep && currentVariant) {
|
|
3430
|
+
onUpdateQuantity(
|
|
3431
|
+
stepIndex,
|
|
3432
|
+
product.id,
|
|
3433
|
+
currentVariant.id,
|
|
3434
|
+
Math.max(rule.min, committedQty - 1)
|
|
3435
|
+
);
|
|
3436
|
+
} else {
|
|
3437
|
+
setQty((q) => Math.max(rule.min, q - 1));
|
|
3438
|
+
}
|
|
3439
|
+
},
|
|
3440
|
+
children: "\u2212"
|
|
3441
|
+
}
|
|
3442
|
+
),
|
|
3443
|
+
/* @__PURE__ */ jsx7(
|
|
3444
|
+
"span",
|
|
3445
|
+
{
|
|
3446
|
+
className: "lb-mix-match__qty-stepper-value",
|
|
3447
|
+
"data-row-qty": true,
|
|
3448
|
+
"aria-live": "polite",
|
|
3449
|
+
children: shownQty
|
|
3450
|
+
}
|
|
3451
|
+
),
|
|
3452
|
+
/* @__PURE__ */ jsx7(
|
|
3453
|
+
"button",
|
|
3454
|
+
{
|
|
3455
|
+
type: "button",
|
|
3456
|
+
className: "lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus",
|
|
3457
|
+
"aria-label": "Increase quantity",
|
|
3458
|
+
disabled: stepperDisabled || shownQty >= stepperMax,
|
|
3459
|
+
onClick: () => {
|
|
3460
|
+
if (variantInStep && currentVariant) {
|
|
3461
|
+
onUpdateQuantity(
|
|
3462
|
+
stepIndex,
|
|
3463
|
+
product.id,
|
|
3464
|
+
currentVariant.id,
|
|
3465
|
+
Math.min(stepperMax, committedQty + 1)
|
|
3466
|
+
);
|
|
3467
|
+
} else {
|
|
3468
|
+
setQty((q) => Math.min(stepperMax, q + 1));
|
|
3469
|
+
}
|
|
3470
|
+
},
|
|
3471
|
+
children: "+"
|
|
3472
|
+
}
|
|
3473
|
+
)
|
|
3474
|
+
]
|
|
3475
|
+
}
|
|
3476
|
+
) }),
|
|
3477
|
+
/* @__PURE__ */ jsx7(
|
|
3478
|
+
"button",
|
|
3479
|
+
{
|
|
3480
|
+
type: "button",
|
|
3481
|
+
className: `lb-mix-match__modal-add${variantInStep ? " lb-mix-match__modal-add--added" : ""}${lockedRequired ? " lb-mix-match__modal-add--required" : ""}`,
|
|
3482
|
+
"data-add-product": true,
|
|
3483
|
+
"aria-label": variantInStep ? lockedRequired ? `${product.title} is required and cannot be removed` : `Remove ${product.title}` : `${inSwapState ? "Swap" : "Add"} ${product.title}`,
|
|
3484
|
+
disabled: addDisabled || lockedRequired,
|
|
3485
|
+
onClick: handleAddOrRemove,
|
|
3486
|
+
children: variantInStep ? lockedRequired ? "Required" : "Remove" : inSwapState ? "Swap" : "Add"
|
|
3487
|
+
}
|
|
3488
|
+
)
|
|
3489
|
+
] })
|
|
3490
|
+
] })
|
|
3491
|
+
]
|
|
3492
|
+
}
|
|
3493
|
+
);
|
|
3494
|
+
}
|
|
3495
|
+
function CloseIcon3() {
|
|
3496
|
+
return /* @__PURE__ */ jsxs7(
|
|
3497
|
+
"svg",
|
|
3498
|
+
{
|
|
3499
|
+
width: "20",
|
|
3500
|
+
height: "20",
|
|
3501
|
+
viewBox: "0 0 20 20",
|
|
3502
|
+
fill: "none",
|
|
3503
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
3504
|
+
"aria-hidden": "true",
|
|
3505
|
+
children: [
|
|
3506
|
+
/* @__PURE__ */ jsx7("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
3507
|
+
/* @__PURE__ */ jsx7("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
|
|
3508
|
+
]
|
|
3509
|
+
}
|
|
3510
|
+
);
|
|
3511
|
+
}
|
|
3512
|
+
function SearchClearIcon2() {
|
|
3513
|
+
return /* @__PURE__ */ jsx7(
|
|
3514
|
+
"svg",
|
|
3515
|
+
{
|
|
3516
|
+
width: "16",
|
|
3517
|
+
height: "16",
|
|
3518
|
+
viewBox: "0 0 20 20",
|
|
3519
|
+
fill: "currentColor",
|
|
3520
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
3521
|
+
"aria-hidden": "true",
|
|
3522
|
+
children: /* @__PURE__ */ jsx7("path", { d: "M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z" })
|
|
3523
|
+
}
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
// src/components/MultiStepBundle.tsx
|
|
3528
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3529
|
+
function ruleFor4(bundle, productId) {
|
|
3530
|
+
return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE4;
|
|
3531
|
+
}
|
|
3532
|
+
function canRemoveItem2(bundle, selections, item) {
|
|
3533
|
+
const rule = ruleFor4(bundle, item.productId);
|
|
3534
|
+
if (!rule.required) return true;
|
|
3535
|
+
let unitsElsewhere = 0;
|
|
3536
|
+
for (const stepPicks of selections) {
|
|
3537
|
+
for (const s of stepPicks) {
|
|
3538
|
+
if (s.productId === item.productId && s !== item) {
|
|
3539
|
+
unitsElsewhere += s.quantity;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
return unitsElsewhere >= rule.min;
|
|
3544
|
+
}
|
|
3545
|
+
function swapUnitsForItem2(bundle, selections, step, productId, variantId) {
|
|
3546
|
+
const rule = ruleFor4(bundle, productId);
|
|
3547
|
+
if (!rule.required || rule.max !== rule.min) return 0;
|
|
3548
|
+
const stepPicks = selections[step] ?? [];
|
|
3549
|
+
if (stepPicks.some((s) => s.variantId === variantId)) return 0;
|
|
3550
|
+
return stepPicks.filter((s) => s.productId === productId && s.variantId !== variantId).reduce((sum, s) => sum + s.quantity, 0);
|
|
3551
|
+
}
|
|
3552
|
+
function MultiStepBundle(props) {
|
|
3553
|
+
const {
|
|
3554
|
+
shopDomain,
|
|
3555
|
+
storefrontAccessToken,
|
|
3556
|
+
bundleGid,
|
|
3557
|
+
appUrl,
|
|
3558
|
+
analyticsEnabled,
|
|
3559
|
+
onAddToCart,
|
|
3560
|
+
onError,
|
|
3561
|
+
className
|
|
3562
|
+
} = props;
|
|
1204
3563
|
const result = useBundleData({
|
|
1205
3564
|
shopDomain,
|
|
1206
3565
|
storefrontAccessToken,
|
|
1207
3566
|
bundleGid
|
|
1208
3567
|
});
|
|
3568
|
+
const { outOfStockBehavior } = useShopSettings({
|
|
3569
|
+
shopDomain,
|
|
3570
|
+
storefrontAccessToken
|
|
3571
|
+
});
|
|
1209
3572
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
1210
3573
|
shopDomain,
|
|
1211
3574
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
1212
3575
|
bundleGid,
|
|
1213
|
-
bundleType: "
|
|
3576
|
+
bundleType: "multi_step",
|
|
1214
3577
|
enabled: analyticsEnabled !== false
|
|
1215
3578
|
});
|
|
1216
|
-
const [
|
|
1217
|
-
const [
|
|
1218
|
-
const [
|
|
1219
|
-
const
|
|
3579
|
+
const [selections, setSelections] = useState12([]);
|
|
3580
|
+
const [pickerOpenAt, setPickerOpenAt] = useState12(null);
|
|
3581
|
+
const [addingToCart, setAddingToCart] = useState12(false);
|
|
3582
|
+
const [cartError, setCartError] = useState12(null);
|
|
3583
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "multi_step" ? result.bundle : null;
|
|
1220
3584
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
1221
|
-
|
|
3585
|
+
const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
3586
|
+
useEffect15(() => {
|
|
1222
3587
|
if (result.status === "error") {
|
|
1223
3588
|
onError?.(result.error);
|
|
1224
3589
|
return;
|
|
1225
3590
|
}
|
|
1226
|
-
if (result.status === "success" && result.bundle.bundleType !== "
|
|
3591
|
+
if (result.status === "success" && result.bundle.bundleType !== "multi_step") {
|
|
1227
3592
|
onError?.(
|
|
1228
3593
|
new Error(
|
|
1229
|
-
`
|
|
3594
|
+
`MultiStepBundle: expected bundleType="multi_step", got "${result.bundle.bundleType}"`
|
|
1230
3595
|
)
|
|
1231
3596
|
);
|
|
1232
3597
|
}
|
|
1233
3598
|
}, [result, onError]);
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
3599
|
+
const steps = useMemo10(() => bundle?.steps ?? [], [bundle]);
|
|
3600
|
+
useEffect15(() => {
|
|
3601
|
+
const seeded = steps.map(() => []);
|
|
3602
|
+
if (bundle) {
|
|
3603
|
+
const seedCurrency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
3604
|
+
for (const [pid, rule] of Object.entries(bundle.productRules)) {
|
|
3605
|
+
if (!rule.required) continue;
|
|
3606
|
+
for (let i = 0; i < steps.length; i++) {
|
|
3607
|
+
const product = productsForStep2(bundle, i).find((p) => p.id === pid);
|
|
3608
|
+
const variant = product?.variants.nodes.find(
|
|
3609
|
+
(v) => isVariantFulfillable7(v, rule.min)
|
|
3610
|
+
);
|
|
3611
|
+
if (!product || !variant) continue;
|
|
3612
|
+
seeded[i].push({
|
|
3613
|
+
productId: product.id,
|
|
3614
|
+
variantId: variant.id,
|
|
3615
|
+
title: product.title,
|
|
3616
|
+
url: `/products/${product.handle}`,
|
|
3617
|
+
variantTitle: variant.title,
|
|
3618
|
+
featuredImage: variant.image?.url ?? product.featuredImage?.url ?? null,
|
|
3619
|
+
price: parseFloat(variant.price.amount),
|
|
3620
|
+
compareAtPrice: variant.compareAtPrice ? parseFloat(variant.compareAtPrice.amount) : null,
|
|
3621
|
+
unitPrice: formatUnitPrice7(
|
|
3622
|
+
variant.unitPrice,
|
|
3623
|
+
variant.unitPriceMeasurement,
|
|
3624
|
+
seedCurrency
|
|
3625
|
+
),
|
|
3626
|
+
quantity: rule.min
|
|
3627
|
+
});
|
|
3628
|
+
break;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
setSelections(seeded);
|
|
3633
|
+
}, [bundle, steps]);
|
|
3634
|
+
const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
|
|
3635
|
+
const totalMin = useMemo10(
|
|
3636
|
+
() => steps.reduce((sum, s) => sum + s.minQuantity, 0),
|
|
3637
|
+
[steps]
|
|
1260
3638
|
);
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
)
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
3639
|
+
const unitsByStep = useMemo10(
|
|
3640
|
+
() => selections.map((picks) => picks.reduce((sum, s) => sum + s.quantity, 0)),
|
|
3641
|
+
[selections]
|
|
3642
|
+
);
|
|
3643
|
+
const totalQuantity = useMemo10(
|
|
3644
|
+
() => unitsByStep.reduce((sum, u) => sum + u, 0),
|
|
3645
|
+
[unitsByStep]
|
|
3646
|
+
);
|
|
3647
|
+
const stepSatisfied = useCallback9(
|
|
3648
|
+
(i) => (unitsByStep[i] ?? 0) >= (steps[i]?.minQuantity ?? 1),
|
|
3649
|
+
[unitsByStep, steps]
|
|
3650
|
+
);
|
|
3651
|
+
const allSatisfied = steps.length > 0 && steps.every((_, i) => stepSatisfied(i));
|
|
3652
|
+
const valid = !!bundle && allSatisfied;
|
|
3653
|
+
const completedSteps = steps.filter((_, i) => stepSatisfied(i)).length;
|
|
3654
|
+
const summary = useMemo10(() => {
|
|
3655
|
+
let comparePrice = 0;
|
|
3656
|
+
let salePrice = 0;
|
|
3657
|
+
for (const stepPicks of selections) {
|
|
3658
|
+
for (const sel of stepPicks) {
|
|
3659
|
+
if (sel.quantity <= 0) continue;
|
|
3660
|
+
comparePrice += sel.price * sel.quantity;
|
|
3661
|
+
if (bundle) {
|
|
3662
|
+
salePrice += calculateDiscount3(
|
|
3663
|
+
sel.price,
|
|
3664
|
+
bundle.discountConfig.discountType,
|
|
3665
|
+
bundle.discountConfig.discountValue
|
|
3666
|
+
) * sel.quantity;
|
|
3667
|
+
}
|
|
1276
3668
|
}
|
|
1277
|
-
|
|
3669
|
+
}
|
|
3670
|
+
const savings = Math.max(0, comparePrice - salePrice);
|
|
3671
|
+
const savingsPercent = comparePrice > 0 && savings > 0 ? Math.round(savings / comparePrice * 100) : 0;
|
|
3672
|
+
return { comparePrice, salePrice, savings, savingsPercent };
|
|
3673
|
+
}, [selections, bundle]);
|
|
3674
|
+
const addSelection = useCallback9((step, item) => {
|
|
3675
|
+
setSelections((prev) => {
|
|
3676
|
+
if (step < 0 || step >= prev.length) return prev;
|
|
3677
|
+
if (prev[step].some((s) => s.variantId === item.variantId)) return prev;
|
|
3678
|
+
const next = prev.slice();
|
|
3679
|
+
next[step] = [...next[step], item];
|
|
3680
|
+
return next;
|
|
3681
|
+
});
|
|
3682
|
+
}, []);
|
|
3683
|
+
const swapSelection = useCallback9(
|
|
3684
|
+
(step, item) => {
|
|
3685
|
+
setSelections((prev) => {
|
|
3686
|
+
if (!bundle || step < 0 || step >= prev.length) return prev;
|
|
3687
|
+
const swapUnits = swapUnitsForItem2(
|
|
3688
|
+
bundle,
|
|
3689
|
+
prev,
|
|
3690
|
+
step,
|
|
3691
|
+
item.productId,
|
|
3692
|
+
item.variantId
|
|
3693
|
+
);
|
|
3694
|
+
if (swapUnits <= 0) return prev;
|
|
3695
|
+
const qty = Math.min(Math.max(1, item.quantity), swapUnits);
|
|
3696
|
+
let freed = 0;
|
|
3697
|
+
const stepPicks = [];
|
|
3698
|
+
for (const s of prev[step]) {
|
|
3699
|
+
if (freed < qty && s.productId === item.productId && s.variantId !== item.variantId) {
|
|
3700
|
+
const take = Math.min(s.quantity, qty - freed);
|
|
3701
|
+
freed += take;
|
|
3702
|
+
if (s.quantity > take) {
|
|
3703
|
+
stepPicks.push({ ...s, quantity: s.quantity - take });
|
|
3704
|
+
}
|
|
3705
|
+
} else {
|
|
3706
|
+
stepPicks.push(s);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
if (freed === 0) return prev;
|
|
3710
|
+
stepPicks.push({ ...item, quantity: freed });
|
|
3711
|
+
const next = prev.slice();
|
|
3712
|
+
next[step] = stepPicks;
|
|
3713
|
+
return next;
|
|
3714
|
+
});
|
|
3715
|
+
},
|
|
3716
|
+
[bundle]
|
|
3717
|
+
);
|
|
3718
|
+
const updateQuantity = useCallback9(
|
|
3719
|
+
(step, productId, variantId, quantity) => {
|
|
3720
|
+
setSelections((prev) => {
|
|
3721
|
+
if (step < 0 || step >= prev.length) return prev;
|
|
3722
|
+
const idx = prev[step].findIndex(
|
|
3723
|
+
(s) => s.productId === productId && s.variantId === variantId
|
|
3724
|
+
);
|
|
3725
|
+
if (idx === -1) return prev;
|
|
3726
|
+
const clamped = Math.max(1, quantity);
|
|
3727
|
+
if (clamped === prev[step][idx].quantity) return prev;
|
|
3728
|
+
const next = prev.slice();
|
|
3729
|
+
const stepPicks = next[step].slice();
|
|
3730
|
+
stepPicks[idx] = { ...stepPicks[idx], quantity: clamped };
|
|
3731
|
+
next[step] = stepPicks;
|
|
3732
|
+
return next;
|
|
3733
|
+
});
|
|
3734
|
+
},
|
|
3735
|
+
[]
|
|
3736
|
+
);
|
|
3737
|
+
const removeVariant = useCallback9(
|
|
3738
|
+
(step, variantId) => {
|
|
3739
|
+
setSelections((prev) => {
|
|
3740
|
+
if (step < 0 || step >= prev.length) return prev;
|
|
3741
|
+
const idx = prev[step].findIndex((s) => s.variantId === variantId);
|
|
3742
|
+
if (idx === -1) return prev;
|
|
3743
|
+
if (bundle && !canRemoveItem2(bundle, prev, prev[step][idx])) return prev;
|
|
3744
|
+
const next = prev.slice();
|
|
3745
|
+
const stepPicks = next[step].slice();
|
|
3746
|
+
stepPicks.splice(idx, 1);
|
|
3747
|
+
next[step] = stepPicks;
|
|
3748
|
+
return next;
|
|
3749
|
+
});
|
|
3750
|
+
},
|
|
3751
|
+
[bundle]
|
|
3752
|
+
);
|
|
3753
|
+
const removeSlot = useCallback9(
|
|
3754
|
+
(step, index) => {
|
|
3755
|
+
setSelections((prev) => {
|
|
3756
|
+
if (step < 0 || step >= prev.length) return prev;
|
|
3757
|
+
if (index < 0 || index >= prev[step].length) return prev;
|
|
3758
|
+
if (bundle && !canRemoveItem2(bundle, prev, prev[step][index]))
|
|
3759
|
+
return prev;
|
|
3760
|
+
const next = prev.slice();
|
|
3761
|
+
const stepPicks = next[step].slice();
|
|
3762
|
+
stepPicks.splice(index, 1);
|
|
3763
|
+
next[step] = stepPicks;
|
|
3764
|
+
return next;
|
|
3765
|
+
});
|
|
3766
|
+
},
|
|
3767
|
+
[bundle]
|
|
3768
|
+
);
|
|
3769
|
+
const handleAddToCart = useCallback9(async () => {
|
|
3770
|
+
if (!bundle || !valid) return;
|
|
3771
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3772
|
+
for (const stepPicks of selections) {
|
|
3773
|
+
for (const sel of stepPicks) {
|
|
3774
|
+
if (sel.quantity <= 0) continue;
|
|
3775
|
+
const existing = grouped.get(sel.variantId);
|
|
3776
|
+
if (existing) existing.quantity += sel.quantity;
|
|
3777
|
+
else grouped.set(sel.variantId, { variantId: sel.variantId, quantity: sel.quantity });
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
const lines = Array.from(grouped.values()).map((line) => ({
|
|
3781
|
+
merchandiseId: line.variantId,
|
|
3782
|
+
quantity: line.quantity,
|
|
3783
|
+
attributes: [
|
|
3784
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
3785
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
3786
|
+
]
|
|
3787
|
+
}));
|
|
1278
3788
|
setAddingToCart(true);
|
|
1279
3789
|
setCartError(null);
|
|
1280
3790
|
try {
|
|
1281
3791
|
await onAddToCart(lines);
|
|
1282
|
-
const
|
|
3792
|
+
const totalPrice = selections.reduce(
|
|
3793
|
+
(sum, stepPicks) => sum + stepPicks.reduce((s, sel) => s + sel.price * sel.quantity, 0),
|
|
3794
|
+
0
|
|
3795
|
+
);
|
|
1283
3796
|
trackAddToCart({
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
totalPrice: Math.round(unitPrice * quantity * 100) / 100
|
|
3797
|
+
quantity: totalQuantity,
|
|
3798
|
+
totalPrice: Math.round(totalPrice * 100) / 100
|
|
1287
3799
|
});
|
|
3800
|
+
setSelections(steps.map(() => []));
|
|
1288
3801
|
} catch (err) {
|
|
1289
3802
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1290
3803
|
setCartError(error.message);
|
|
@@ -1292,169 +3805,306 @@ function VolumeBundle(props) {
|
|
|
1292
3805
|
} finally {
|
|
1293
3806
|
setAddingToCart(false);
|
|
1294
3807
|
}
|
|
1295
|
-
}, [
|
|
1296
|
-
bundle,
|
|
1297
|
-
product,
|
|
1298
|
-
quantity,
|
|
1299
|
-
activeTier,
|
|
1300
|
-
basePrice,
|
|
1301
|
-
onAddToCart,
|
|
1302
|
-
onError,
|
|
1303
|
-
trackAddToCart,
|
|
1304
|
-
selectedVariant,
|
|
1305
|
-
tierSavings
|
|
1306
|
-
]);
|
|
3808
|
+
}, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity, steps]);
|
|
1307
3809
|
if (result.status === "loading") {
|
|
1308
|
-
return /* @__PURE__ */
|
|
1309
|
-
/* @__PURE__ */
|
|
1310
|
-
/* @__PURE__ */
|
|
3810
|
+
return /* @__PURE__ */ jsxs8("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
3811
|
+
/* @__PURE__ */ jsx8("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
3812
|
+
/* @__PURE__ */ jsx8("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
1311
3813
|
] });
|
|
1312
3814
|
}
|
|
1313
3815
|
if (result.status === "error") return null;
|
|
1314
|
-
if (!bundle) return null;
|
|
1315
|
-
|
|
1316
|
-
|
|
3816
|
+
if (!bundle || steps.length === 0) return null;
|
|
3817
|
+
for (let i = 0; i < steps.length; i++) {
|
|
3818
|
+
if (!stepHasInStockProduct(bundle, i)) return null;
|
|
3819
|
+
}
|
|
3820
|
+
const requiredBlocked = Object.entries(bundle.productRules).some(
|
|
3821
|
+
([pid, rule]) => {
|
|
3822
|
+
if (!rule.required) return false;
|
|
3823
|
+
return !steps.some(
|
|
3824
|
+
(_, i) => productsForStep2(bundle, i).some(
|
|
3825
|
+
(p) => p.id === pid && p.variants.nodes.some((v) => isVariantFulfillable7(v, rule.min))
|
|
3826
|
+
)
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3829
|
+
);
|
|
3830
|
+
if (requiredBlocked) return null;
|
|
3831
|
+
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
3832
|
+
const ctaLabel = addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText || "Add to cart";
|
|
3833
|
+
return /* @__PURE__ */ jsxs8(
|
|
1317
3834
|
"div",
|
|
1318
3835
|
{
|
|
1319
3836
|
ref: (el) => {
|
|
1320
3837
|
elementRef(el);
|
|
1321
3838
|
setConfigVarsRef(el);
|
|
1322
3839
|
},
|
|
1323
|
-
className: `lb-bundle lb-bundle--
|
|
3840
|
+
className: `lb-bundle lb-bundle--multi-step lb-mix-match lb-multi-step ${className ?? ""}`,
|
|
1324
3841
|
role: "region",
|
|
1325
3842
|
"aria-label": bundle.title,
|
|
3843
|
+
"data-required-quantity": totalMin,
|
|
1326
3844
|
children: [
|
|
1327
|
-
/* @__PURE__ */
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
3845
|
+
/* @__PURE__ */ jsx8("div", { className: "lb-bundle-header", children: /* @__PURE__ */ jsxs8("div", { className: "lb-bundle-header__content", children: [
|
|
3846
|
+
/* @__PURE__ */ jsx8("h3", { className: "lb-bundle-title", children: bundle.title }),
|
|
3847
|
+
bundle.description && /* @__PURE__ */ jsx8("p", { className: "lb-bundle-subtitle", children: bundle.description })
|
|
3848
|
+
] }) }),
|
|
3849
|
+
/* @__PURE__ */ jsxs8("div", { className: "lb-mix-match__progress", "data-progress": true, children: [
|
|
3850
|
+
/* @__PURE__ */ jsx8(
|
|
3851
|
+
"div",
|
|
1331
3852
|
{
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
/* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
1340
|
-
/* @__PURE__ */ jsxs4("p", { className: "lb-bundle__product-price", children: [
|
|
1341
|
-
formatMoney3(basePrice, currency),
|
|
1342
|
-
" each"
|
|
1343
|
-
] }),
|
|
1344
|
-
unitPriceText && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
1345
|
-
showPicker && /* @__PURE__ */ jsx4("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
1346
|
-
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1347
|
-
value: o.value,
|
|
1348
|
-
label: o.value,
|
|
1349
|
-
disabled: o.disabled
|
|
1350
|
-
}));
|
|
1351
|
-
return /* @__PURE__ */ jsx4(
|
|
1352
|
-
VariantDropdown,
|
|
3853
|
+
className: "lb-mix-match__progress-segments",
|
|
3854
|
+
role: "progressbar",
|
|
3855
|
+
"aria-valuenow": completedSteps,
|
|
3856
|
+
"aria-valuemin": 0,
|
|
3857
|
+
"aria-valuemax": steps.length,
|
|
3858
|
+
children: Array.from({ length: steps.length }).map((_, i) => /* @__PURE__ */ jsx8(
|
|
3859
|
+
"span",
|
|
1353
3860
|
{
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
onChange: (v) => setOptionValue(optionIndex, v),
|
|
1357
|
-
ariaLabel: optionName
|
|
3861
|
+
className: `lb-mix-match__progress-segment${i < completedSteps ? " lb-mix-match__progress-segment--filled" : ""}`,
|
|
3862
|
+
"data-progress-segment": true
|
|
1358
3863
|
},
|
|
1359
|
-
|
|
1360
|
-
)
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
{
|
|
1367
|
-
className: "lb-bundle__tiers",
|
|
1368
|
-
role: "table",
|
|
1369
|
-
"aria-label": "Volume discounts",
|
|
1370
|
-
children: tierSavings.map((ts) => /* @__PURE__ */ jsxs4(
|
|
1371
|
-
"div",
|
|
1372
|
-
{
|
|
1373
|
-
className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
|
|
1374
|
-
role: "row",
|
|
1375
|
-
children: [
|
|
1376
|
-
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
|
|
1377
|
-
ts.tier.minQuantity,
|
|
1378
|
-
"+ items"
|
|
1379
|
-
] }),
|
|
1380
|
-
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-price", role: "cell", children: [
|
|
1381
|
-
formatMoney3(ts.unitPrice, currency),
|
|
1382
|
-
" each"
|
|
1383
|
-
] }),
|
|
1384
|
-
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
|
|
1385
|
-
"Save ",
|
|
1386
|
-
ts.savingsPercent.toFixed(0),
|
|
1387
|
-
"%"
|
|
1388
|
-
] })
|
|
1389
|
-
]
|
|
1390
|
-
},
|
|
1391
|
-
ts.tier.minQuantity
|
|
1392
|
-
))
|
|
1393
|
-
}
|
|
1394
|
-
),
|
|
1395
|
-
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-selector", children: [
|
|
1396
|
-
/* @__PURE__ */ jsx4("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
|
|
1397
|
-
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-control", children: [
|
|
1398
|
-
/* @__PURE__ */ jsx4(
|
|
1399
|
-
"button",
|
|
1400
|
-
{
|
|
1401
|
-
"aria-label": "Decrease quantity",
|
|
1402
|
-
onClick: () => setQuantity((q) => Math.max(1, q - 1)),
|
|
1403
|
-
children: "\u2212"
|
|
1404
|
-
}
|
|
1405
|
-
),
|
|
1406
|
-
/* @__PURE__ */ jsx4(
|
|
1407
|
-
"input",
|
|
3864
|
+
i
|
|
3865
|
+
))
|
|
3866
|
+
}
|
|
3867
|
+
),
|
|
3868
|
+
/* @__PURE__ */ jsxs8("div", { className: "lb-mix-match__progress-labels", children: [
|
|
3869
|
+
/* @__PURE__ */ jsx8(
|
|
3870
|
+
"span",
|
|
1408
3871
|
{
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
onChange: (e) => {
|
|
1414
|
-
const val = parseInt(e.target.value, 10);
|
|
1415
|
-
if (!isNaN(val) && val > 0) setQuantity(val);
|
|
1416
|
-
},
|
|
1417
|
-
className: "lb-bundle__quantity-input"
|
|
3872
|
+
className: "lb-mix-match__progress-count",
|
|
3873
|
+
"data-progress-count": true,
|
|
3874
|
+
"aria-live": "polite",
|
|
3875
|
+
children: `${completedSteps} of ${steps.length} steps completed`
|
|
1418
3876
|
}
|
|
1419
3877
|
),
|
|
1420
|
-
/* @__PURE__ */
|
|
1421
|
-
"
|
|
3878
|
+
/* @__PURE__ */ jsx8(
|
|
3879
|
+
"span",
|
|
1422
3880
|
{
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
3881
|
+
className: "lb-mix-match__progress-remaining",
|
|
3882
|
+
"data-progress-remaining": true,
|
|
3883
|
+
"aria-live": "polite",
|
|
3884
|
+
children: completedSteps < steps.length ? `${steps.length - completedSteps} more to go` : "Complete"
|
|
1426
3885
|
}
|
|
1427
3886
|
)
|
|
1428
3887
|
] })
|
|
1429
3888
|
] }),
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
3889
|
+
/* @__PURE__ */ jsx8("div", { className: "lb-multi-step__groups", "data-step-groups": true, children: steps.map((step, i) => /* @__PURE__ */ jsxs8(
|
|
3890
|
+
"section",
|
|
3891
|
+
{
|
|
3892
|
+
className: "lb-multi-step__group",
|
|
3893
|
+
"data-step-group": i,
|
|
3894
|
+
children: [
|
|
3895
|
+
/* @__PURE__ */ jsxs8("div", { className: "lb-multi-step__group-heading", children: [
|
|
3896
|
+
/* @__PURE__ */ jsx8("span", { className: "lb-multi-step__group-name", children: step.name }),
|
|
3897
|
+
/* @__PURE__ */ jsx8(
|
|
3898
|
+
"span",
|
|
3899
|
+
{
|
|
3900
|
+
className: `lb-multi-step__group-count${stepSatisfied(i) ? " lb-multi-step__group-count--met" : ""}`,
|
|
3901
|
+
"data-step-count": i,
|
|
3902
|
+
children: `${Math.min(unitsByStep[i] ?? 0, step.minQuantity)} of ${step.minQuantity} added`
|
|
3903
|
+
}
|
|
3904
|
+
)
|
|
3905
|
+
] }),
|
|
3906
|
+
/* @__PURE__ */ jsx8(
|
|
3907
|
+
"div",
|
|
3908
|
+
{
|
|
3909
|
+
className: "lb-mix-match__slots lb-edge-fade lb-multi-step__group-slots",
|
|
3910
|
+
"data-step-slots": i,
|
|
3911
|
+
children: (selections[i] ?? []).map((item, index) => (
|
|
3912
|
+
/* Whole card reopens the wizard AT this step. The inner
|
|
3913
|
+
title button provides the keyboard/AT path (its
|
|
3914
|
+
activation bubbles here); × stops propagation. */
|
|
3915
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- keyboard path is the inner "Edit selection" title button whose activation bubbles to this handler
|
|
3916
|
+
/* @__PURE__ */ jsxs8(
|
|
3917
|
+
"div",
|
|
3918
|
+
{
|
|
3919
|
+
className: "lb-mix-match__slot lb-mix-match__slot--filled",
|
|
3920
|
+
onClick: () => setPickerOpenAt(i),
|
|
3921
|
+
children: [
|
|
3922
|
+
item.featuredImage && /* @__PURE__ */ jsx8("div", { className: "lb-bundle-thumbnail", children: /* @__PURE__ */ jsx8(
|
|
3923
|
+
"img",
|
|
3924
|
+
{
|
|
3925
|
+
src: item.featuredImage,
|
|
3926
|
+
alt: item.title,
|
|
3927
|
+
loading: "lazy"
|
|
3928
|
+
}
|
|
3929
|
+
) }),
|
|
3930
|
+
/* @__PURE__ */ jsxs8("div", { className: "lb-mix-match__filled-info", children: [
|
|
3931
|
+
/* @__PURE__ */ jsx8(
|
|
3932
|
+
"button",
|
|
3933
|
+
{
|
|
3934
|
+
type: "button",
|
|
3935
|
+
className: "lb-mix-match__filled-title",
|
|
3936
|
+
"aria-label": `Edit selection: ${item.title}`,
|
|
3937
|
+
children: item.title
|
|
3938
|
+
}
|
|
3939
|
+
),
|
|
3940
|
+
item.variantTitle && item.variantTitle !== "Default Title" && /* @__PURE__ */ jsx8("span", { className: "lb-mix-match__filled-variant", children: item.variantTitle }),
|
|
3941
|
+
/* @__PURE__ */ jsxs8("span", { className: "lb-mix-match__filled-price", children: [
|
|
3942
|
+
item.compareAtPrice !== null && item.compareAtPrice > item.price && /* @__PURE__ */ jsx8("s", { className: "lb-mix-match__filled-compare", children: formatMoney7(item.compareAtPrice, currency) }),
|
|
3943
|
+
/* @__PURE__ */ jsx8("span", { className: "lb-bundle-product-price", children: formatMoney7(item.price, currency) }),
|
|
3944
|
+
/* @__PURE__ */ jsx8("span", { className: "lb-bundle-qty-inline", children: `\xD7${item.quantity}` })
|
|
3945
|
+
] }),
|
|
3946
|
+
item.unitPrice && /* @__PURE__ */ jsx8("span", { className: "lb-bundle-product-unit-price", children: item.unitPrice })
|
|
3947
|
+
] }),
|
|
3948
|
+
canRemoveItem2(bundle, selections, item) ? /* @__PURE__ */ jsx8(
|
|
3949
|
+
"button",
|
|
3950
|
+
{
|
|
3951
|
+
type: "button",
|
|
3952
|
+
className: "lb-mix-match__slot-remove",
|
|
3953
|
+
"aria-label": `Remove ${item.title}`,
|
|
3954
|
+
onClick: (e) => {
|
|
3955
|
+
e.stopPropagation();
|
|
3956
|
+
removeSlot(i, index);
|
|
3957
|
+
},
|
|
3958
|
+
children: /* @__PURE__ */ jsx8(CloseIcon4, {})
|
|
3959
|
+
}
|
|
3960
|
+
) : (
|
|
3961
|
+
/* Required pick at its floor — quiet state chip instead
|
|
3962
|
+
of the ×; removal returns once another slot covers
|
|
3963
|
+
the product's minimum (variant swap flow). */
|
|
3964
|
+
/* @__PURE__ */ jsx8("span", { className: "lb-mix-match__slot-required", children: "Required" })
|
|
3965
|
+
)
|
|
3966
|
+
]
|
|
3967
|
+
},
|
|
3968
|
+
`${item.productId}:${item.variantId}:${index}`
|
|
3969
|
+
)
|
|
3970
|
+
))
|
|
3971
|
+
}
|
|
3972
|
+
),
|
|
3973
|
+
stepHeadroom2(step.maxQuantity, unitsByStep[i] ?? 0) !== 0 && /* @__PURE__ */ jsxs8(
|
|
3974
|
+
"button",
|
|
3975
|
+
{
|
|
3976
|
+
type: "button",
|
|
3977
|
+
className: "lb-mix-match__add-product lb-multi-step__step-trigger",
|
|
3978
|
+
"data-step-trigger": i,
|
|
3979
|
+
"aria-label": `${step.minQuantity === 1 && step.maxQuantity === 1 ? "Choose a product" : "Choose products"}: ${step.name}`,
|
|
3980
|
+
onClick: () => setPickerOpenAt(i),
|
|
3981
|
+
children: [
|
|
3982
|
+
/* @__PURE__ */ jsx8(
|
|
3983
|
+
"span",
|
|
3984
|
+
{
|
|
3985
|
+
className: "lb-mix-match__add-product-icon",
|
|
3986
|
+
"aria-hidden": "true",
|
|
3987
|
+
children: /* @__PURE__ */ jsx8(PlusIcon2, {})
|
|
3988
|
+
}
|
|
3989
|
+
),
|
|
3990
|
+
/* @__PURE__ */ jsx8("span", { className: "lb-mix-match__add-product-label", children: step.minQuantity === 1 && step.maxQuantity === 1 ? "Choose a product" : "Choose products" })
|
|
3991
|
+
]
|
|
3992
|
+
}
|
|
3993
|
+
)
|
|
3994
|
+
]
|
|
3995
|
+
},
|
|
3996
|
+
i
|
|
3997
|
+
)) }),
|
|
3998
|
+
/* @__PURE__ */ jsx8("div", { className: "lb-bundle-divider" }),
|
|
3999
|
+
allSatisfied && /* @__PURE__ */ jsxs8("div", { className: "lb-bundle-summary", "data-pricing-section": true, children: [
|
|
4000
|
+
/* @__PURE__ */ jsxs8("div", { className: "lb-bundle-summary__text", children: [
|
|
4001
|
+
/* @__PURE__ */ jsx8("span", { className: "lb-bundle-summary__label", children: "Bundle total" }),
|
|
4002
|
+
bundle.widgetConfig.savingsBar.visible && summary.savings > 0 && /* @__PURE__ */ jsxs8("p", { className: "lb-bundle-savings-line", "data-savings-bar": true, children: [
|
|
4003
|
+
"You save",
|
|
4004
|
+
" ",
|
|
4005
|
+
/* @__PURE__ */ jsx8("span", { "data-savings-amount": true, children: formatMoney7(summary.savings, currency) }),
|
|
4006
|
+
" ",
|
|
4007
|
+
/* @__PURE__ */ jsxs8("span", { "data-savings-percent": true, children: [
|
|
4008
|
+
"(",
|
|
4009
|
+
summary.savingsPercent,
|
|
4010
|
+
"%)"
|
|
4011
|
+
] })
|
|
4012
|
+
] })
|
|
4013
|
+
] }),
|
|
4014
|
+
/* @__PURE__ */ jsxs8("span", { className: "lb-bundle-summary__prices", children: [
|
|
4015
|
+
bundle.widgetConfig.pricing.showComparePrice && summary.savings > 0 && /* @__PURE__ */ jsx8("span", { className: "lb-bundle-compare-price", "data-compare-price": true, children: formatMoney7(summary.comparePrice, currency) }),
|
|
4016
|
+
/* @__PURE__ */ jsx8("span", { className: "lb-bundle-sale-price", "data-sale-price": true, children: formatMoney7(summary.salePrice, currency) })
|
|
4017
|
+
] })
|
|
1440
4018
|
] }),
|
|
1441
|
-
/* @__PURE__ */
|
|
4019
|
+
cartError && /* @__PURE__ */ jsx8("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
4020
|
+
/* @__PURE__ */ jsx8(
|
|
1442
4021
|
"button",
|
|
1443
4022
|
{
|
|
1444
4023
|
className: "lb-bundle__cta",
|
|
1445
4024
|
onClick: handleAddToCart,
|
|
1446
|
-
disabled: addingToCart,
|
|
4025
|
+
disabled: addingToCart || !valid,
|
|
1447
4026
|
"aria-busy": addingToCart,
|
|
1448
|
-
children:
|
|
4027
|
+
children: ctaLabel
|
|
4028
|
+
}
|
|
4029
|
+
),
|
|
4030
|
+
pickerOpenAt !== null && /* @__PURE__ */ jsx8(
|
|
4031
|
+
MultiStepPicker,
|
|
4032
|
+
{
|
|
4033
|
+
outOfStockBehavior,
|
|
4034
|
+
bundle,
|
|
4035
|
+
currency,
|
|
4036
|
+
initialStep: pickerOpenAt,
|
|
4037
|
+
showStepper,
|
|
4038
|
+
selections,
|
|
4039
|
+
onAdd: addSelection,
|
|
4040
|
+
onUpdateQuantity: updateQuantity,
|
|
4041
|
+
onRemoveVariant: removeVariant,
|
|
4042
|
+
canRemoveVariant: (variantId) => {
|
|
4043
|
+
for (const stepPicks of selections) {
|
|
4044
|
+
const item = stepPicks.find((p) => p.variantId === variantId);
|
|
4045
|
+
if (item) return canRemoveItem2(bundle, selections, item);
|
|
4046
|
+
}
|
|
4047
|
+
return true;
|
|
4048
|
+
},
|
|
4049
|
+
swapUnitsFor: (step, productId, variantId) => swapUnitsForItem2(bundle, selections, step, productId, variantId),
|
|
4050
|
+
onSwap: swapSelection,
|
|
4051
|
+
onClose: () => setPickerOpenAt(null),
|
|
4052
|
+
styleVarsRef: setModalConfigVarsRef
|
|
1449
4053
|
}
|
|
1450
4054
|
)
|
|
1451
4055
|
]
|
|
1452
4056
|
}
|
|
1453
4057
|
);
|
|
1454
4058
|
}
|
|
4059
|
+
function stepHasInStockProduct(bundle, stepIndex) {
|
|
4060
|
+
return productsForStep2(bundle, stepIndex).some((product) => {
|
|
4061
|
+
const rule = ruleFor4(bundle, product.id);
|
|
4062
|
+
return product.variants.nodes.some((v) => isVariantFulfillable7(v, rule.min));
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
function PlusIcon2() {
|
|
4066
|
+
return /* @__PURE__ */ jsxs8(
|
|
4067
|
+
"svg",
|
|
4068
|
+
{
|
|
4069
|
+
width: "18",
|
|
4070
|
+
height: "18",
|
|
4071
|
+
viewBox: "0 0 18 18",
|
|
4072
|
+
fill: "none",
|
|
4073
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
4074
|
+
"aria-hidden": "true",
|
|
4075
|
+
children: [
|
|
4076
|
+
/* @__PURE__ */ jsx8("line", { x1: "9", y1: "3", x2: "9", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
4077
|
+
/* @__PURE__ */ jsx8("line", { x1: "3", y1: "9", x2: "15", y2: "9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
|
|
4078
|
+
]
|
|
4079
|
+
}
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
4082
|
+
function CloseIcon4() {
|
|
4083
|
+
return /* @__PURE__ */ jsxs8(
|
|
4084
|
+
"svg",
|
|
4085
|
+
{
|
|
4086
|
+
width: "16",
|
|
4087
|
+
height: "16",
|
|
4088
|
+
viewBox: "0 0 20 20",
|
|
4089
|
+
fill: "none",
|
|
4090
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
4091
|
+
"aria-hidden": "true",
|
|
4092
|
+
children: [
|
|
4093
|
+
/* @__PURE__ */ jsx8("line", { x1: "5", y1: "5", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
|
|
4094
|
+
/* @__PURE__ */ jsx8("line", { x1: "15", y1: "5", x2: "5", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
|
|
4095
|
+
]
|
|
4096
|
+
}
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
// src/fetchShopCustomCss.ts
|
|
4101
|
+
async function fetchShopCustomCss(options) {
|
|
4102
|
+
const settings = await fetchShopSettings(options);
|
|
4103
|
+
return settings.customCss;
|
|
4104
|
+
}
|
|
1455
4105
|
|
|
1456
4106
|
// src/hooks/useBundlesForProduct.ts
|
|
1457
|
-
import { useState as
|
|
4107
|
+
import { useState as useState13, useEffect as useEffect16 } from "react";
|
|
1458
4108
|
import {
|
|
1459
4109
|
fetchBundlesForProduct,
|
|
1460
4110
|
injectCustomCss as injectCustomCss2
|
|
@@ -1465,10 +4115,10 @@ var INITIAL_STATE2 = {
|
|
|
1465
4115
|
error: null
|
|
1466
4116
|
};
|
|
1467
4117
|
function useBundlesForProduct(options) {
|
|
1468
|
-
const [state, setState] =
|
|
4118
|
+
const [state, setState] = useState13(
|
|
1469
4119
|
INITIAL_STATE2
|
|
1470
4120
|
);
|
|
1471
|
-
|
|
4121
|
+
useEffect16(() => {
|
|
1472
4122
|
const controller = new AbortController();
|
|
1473
4123
|
setState(INITIAL_STATE2);
|
|
1474
4124
|
fetchBundlesForProduct({
|
|
@@ -1488,13 +4138,13 @@ function useBundlesForProduct(options) {
|
|
|
1488
4138
|
error: err instanceof Error ? err : new Error(String(err))
|
|
1489
4139
|
});
|
|
1490
4140
|
});
|
|
1491
|
-
|
|
4141
|
+
fetchShopSettings({
|
|
1492
4142
|
shopDomain: options.shopDomain,
|
|
1493
4143
|
storefrontAccessToken: options.storefrontAccessToken,
|
|
1494
4144
|
signal: controller.signal
|
|
1495
|
-
}).then((
|
|
4145
|
+
}).then((settings) => {
|
|
1496
4146
|
if (controller.signal.aborted) return;
|
|
1497
|
-
injectCustomCss2(options.shopDomain,
|
|
4147
|
+
injectCustomCss2(options.shopDomain, settings.customCss);
|
|
1498
4148
|
}).catch(() => {
|
|
1499
4149
|
});
|
|
1500
4150
|
return () => {
|
|
@@ -1523,20 +4173,17 @@ import {
|
|
|
1523
4173
|
computeBundleSaleCents,
|
|
1524
4174
|
calculateTierSavings as calculateTierSavings2,
|
|
1525
4175
|
getActiveTier as getActiveTier2,
|
|
4176
|
+
productsForStep as productsForStep3,
|
|
4177
|
+
stepHeadroom as stepHeadroom3,
|
|
1526
4178
|
validateQuantity,
|
|
1527
4179
|
transformImageUrl,
|
|
1528
4180
|
formatCountdown,
|
|
1529
|
-
formatMoney as
|
|
1530
|
-
formatUnitPrice as
|
|
1531
|
-
calculateDiscount as
|
|
4181
|
+
formatMoney as formatMoney8,
|
|
4182
|
+
formatUnitPrice as formatUnitPrice8,
|
|
4183
|
+
calculateDiscount as calculateDiscount4,
|
|
1532
4184
|
reportImpression as reportImpression2,
|
|
1533
4185
|
reportAddToCart as reportAddToCart2,
|
|
1534
4186
|
observeImpression as observeImpression2,
|
|
1535
|
-
getLinkGroupAssignment,
|
|
1536
|
-
pickVariantFromWeights,
|
|
1537
|
-
fnv1a,
|
|
1538
|
-
setConsent,
|
|
1539
|
-
hasConsent,
|
|
1540
4187
|
injectCustomCss as injectCustomCss3,
|
|
1541
4188
|
sanitizeCustomCss,
|
|
1542
4189
|
createStorefrontClient as createStorefrontClient3,
|
|
@@ -1545,21 +4192,26 @@ import {
|
|
|
1545
4192
|
parseMetaobjectBundleStrict as parseMetaobjectBundleStrict2,
|
|
1546
4193
|
BUNDLE_METAOBJECT_QUERY as BUNDLE_METAOBJECT_QUERY2,
|
|
1547
4194
|
BUNDLES_FOR_PRODUCT_QUERY,
|
|
1548
|
-
|
|
4195
|
+
SHOP_SETTINGS_QUERY as SHOP_SETTINGS_QUERY2,
|
|
4196
|
+
DEFAULT_OUT_OF_STOCK_BEHAVIOR as DEFAULT_OUT_OF_STOCK_BEHAVIOR3,
|
|
4197
|
+
parseOutOfStockBehavior as parseOutOfStockBehavior2
|
|
1549
4198
|
} from "@lime-bundles/core";
|
|
1550
4199
|
export {
|
|
1551
4200
|
BUNDLES_FOR_PRODUCT_QUERY,
|
|
1552
4201
|
BUNDLE_METAOBJECT_QUERY2 as BUNDLE_METAOBJECT_QUERY,
|
|
4202
|
+
BogoBundle,
|
|
1553
4203
|
BundleParseError2 as BundleParseError,
|
|
4204
|
+
DEFAULT_OUT_OF_STOCK_BEHAVIOR3 as DEFAULT_OUT_OF_STOCK_BEHAVIOR,
|
|
1554
4205
|
FixedBundle,
|
|
1555
4206
|
MixMatchBundle,
|
|
1556
|
-
|
|
4207
|
+
MultiStepBundle,
|
|
4208
|
+
SHOP_SETTINGS_QUERY2 as SHOP_SETTINGS_QUERY,
|
|
1557
4209
|
StorefrontApiError,
|
|
1558
4210
|
VariantDropdown,
|
|
1559
4211
|
VolumeBundle,
|
|
1560
4212
|
WIDGET_CONFIG_DEFAULTS,
|
|
1561
4213
|
applyWidgetConfigVars2 as applyWidgetConfigVars,
|
|
1562
|
-
|
|
4214
|
+
calculateDiscount4 as calculateDiscount,
|
|
1563
4215
|
calculateTierSavings2 as calculateTierSavings,
|
|
1564
4216
|
computeBundleSaleCents,
|
|
1565
4217
|
computeFixedPricing,
|
|
@@ -1567,31 +4219,32 @@ export {
|
|
|
1567
4219
|
fetchBundleData,
|
|
1568
4220
|
fetchBundlesForProduct2 as fetchBundlesForProduct,
|
|
1569
4221
|
fetchShopCustomCss,
|
|
1570
|
-
|
|
4222
|
+
fetchShopSettings,
|
|
4223
|
+
fetchShopSettingsOrDefault,
|
|
1571
4224
|
formatCents,
|
|
1572
4225
|
formatCountdown,
|
|
1573
|
-
|
|
1574
|
-
|
|
4226
|
+
formatMoney8 as formatMoney,
|
|
4227
|
+
formatUnitPrice8 as formatUnitPrice,
|
|
1575
4228
|
getActiveTier2 as getActiveTier,
|
|
1576
|
-
getLinkGroupAssignment,
|
|
1577
|
-
hasConsent,
|
|
1578
4229
|
injectCustomCss3 as injectCustomCss,
|
|
1579
4230
|
mergeWidgetConfig,
|
|
1580
4231
|
observeImpression2 as observeImpression,
|
|
1581
4232
|
parseCents,
|
|
1582
4233
|
parseMetaobjectBundle,
|
|
1583
4234
|
parseMetaobjectBundleStrict2 as parseMetaobjectBundleStrict,
|
|
4235
|
+
parseOutOfStockBehavior2 as parseOutOfStockBehavior,
|
|
1584
4236
|
percentageDiscountUnit,
|
|
1585
|
-
|
|
4237
|
+
productsForStep3 as productsForStep,
|
|
1586
4238
|
reportAddToCart2 as reportAddToCart,
|
|
1587
4239
|
reportImpression2 as reportImpression,
|
|
1588
4240
|
sanitizeCustomCss,
|
|
1589
|
-
|
|
4241
|
+
stepHeadroom3 as stepHeadroom,
|
|
1590
4242
|
thumbnailRatioVars,
|
|
1591
4243
|
transformImageUrl,
|
|
1592
4244
|
useAnalytics,
|
|
1593
4245
|
useBundleData,
|
|
1594
4246
|
useBundlesForProduct,
|
|
4247
|
+
useShopSettings,
|
|
1595
4248
|
useVariantSelection,
|
|
1596
4249
|
useWidgetConfigVars,
|
|
1597
4250
|
validateQuantity
|