@lime-bundles/react 4.1.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 +3251 -636
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -6
- package/dist/index.d.ts +54 -6
- package/dist/index.js +3264 -613
- package/dist/index.js.map +1 -1
- package/docs/css-variables.md +44 -70
- package/docs/react-nextjs.md +17 -6
- package/docs/web-component.md +2 -2
- 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 () => {
|
|
@@ -182,7 +201,7 @@ function useAnalytics(options) {
|
|
|
182
201
|
);
|
|
183
202
|
useEffect2(() => {
|
|
184
203
|
impressionFiredRef.current = false;
|
|
185
|
-
}, [options.bundleGid
|
|
204
|
+
}, [options.bundleGid]);
|
|
186
205
|
useEffect2(() => {
|
|
187
206
|
if (options.enabled === false) return;
|
|
188
207
|
if (impressionFiredRef.current) return;
|
|
@@ -192,9 +211,7 @@ function useAnalytics(options) {
|
|
|
192
211
|
impressionFiredRef.current = true;
|
|
193
212
|
reportImpression(config, {
|
|
194
213
|
bundleGid: options.bundleGid,
|
|
195
|
-
bundleType: options.bundleType
|
|
196
|
-
abTestId: options.abTestId,
|
|
197
|
-
abVariant: options.abVariant
|
|
214
|
+
bundleType: options.bundleType
|
|
198
215
|
});
|
|
199
216
|
});
|
|
200
217
|
return cleanup;
|
|
@@ -203,9 +220,7 @@ function useAnalytics(options) {
|
|
|
203
220
|
config,
|
|
204
221
|
options.enabled,
|
|
205
222
|
options.bundleGid,
|
|
206
|
-
options.bundleType
|
|
207
|
-
options.abTestId,
|
|
208
|
-
options.abVariant
|
|
223
|
+
options.bundleType
|
|
209
224
|
]);
|
|
210
225
|
const trackAddToCart = useCallback(
|
|
211
226
|
(event) => {
|
|
@@ -215,18 +230,14 @@ function useAnalytics(options) {
|
|
|
215
230
|
bundleType: options.bundleType,
|
|
216
231
|
productId: event.productId,
|
|
217
232
|
quantity: event.quantity,
|
|
218
|
-
totalPrice: event.totalPrice
|
|
219
|
-
abTestId: options.abTestId,
|
|
220
|
-
abVariant: options.abVariant
|
|
233
|
+
totalPrice: event.totalPrice
|
|
221
234
|
});
|
|
222
235
|
},
|
|
223
236
|
[
|
|
224
237
|
config,
|
|
225
238
|
options.enabled,
|
|
226
239
|
options.bundleGid,
|
|
227
|
-
options.bundleType
|
|
228
|
-
options.abTestId,
|
|
229
|
-
options.abVariant
|
|
240
|
+
options.bundleType
|
|
230
241
|
]
|
|
231
242
|
);
|
|
232
243
|
return { elementRef, trackAddToCart };
|
|
@@ -330,13 +341,58 @@ function useVariantSelection({
|
|
|
330
341
|
};
|
|
331
342
|
}
|
|
332
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
|
+
|
|
333
389
|
// src/components/VariantDropdown.tsx
|
|
334
390
|
import {
|
|
335
391
|
useEffect as useEffect5,
|
|
336
392
|
useId,
|
|
337
393
|
useLayoutEffect,
|
|
338
394
|
useMemo as useMemo3,
|
|
339
|
-
useRef as
|
|
395
|
+
useRef as useRef4,
|
|
340
396
|
useState as useState4
|
|
341
397
|
} from "react";
|
|
342
398
|
import { dropdown } from "@lime-bundles/core";
|
|
@@ -367,9 +423,9 @@ function VariantDropdown({
|
|
|
367
423
|
className
|
|
368
424
|
}) {
|
|
369
425
|
const idBase = useId();
|
|
370
|
-
const triggerRef =
|
|
371
|
-
const listboxRef =
|
|
372
|
-
const typeAheadRef =
|
|
426
|
+
const triggerRef = useRef4(null);
|
|
427
|
+
const listboxRef = useRef4(null);
|
|
428
|
+
const typeAheadRef = useRef4(emptyTypeAheadState());
|
|
373
429
|
const [isOpen, setIsOpen] = useState4(false);
|
|
374
430
|
const [activeIndex, setActiveIndex] = useState4(-1);
|
|
375
431
|
const [position, setPosition] = useState4(null);
|
|
@@ -631,7 +687,7 @@ function FixedBundle(props) {
|
|
|
631
687
|
const [addingToCart, setAddingToCart] = useState5(false);
|
|
632
688
|
const [cartError, setCartError] = useState5(null);
|
|
633
689
|
const [selectedVariants, setSelectedVariants] = useState5({});
|
|
634
|
-
const handleVariantChange =
|
|
690
|
+
const handleVariantChange = useCallback5(
|
|
635
691
|
(productId, variant) => {
|
|
636
692
|
setSelectedVariants((prev) => {
|
|
637
693
|
if (prev[productId] === variant) return prev;
|
|
@@ -642,6 +698,7 @@ function FixedBundle(props) {
|
|
|
642
698
|
);
|
|
643
699
|
const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
|
|
644
700
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
701
|
+
const productsEdgeFadeRef = useEdgeFade();
|
|
645
702
|
useEffect6(() => {
|
|
646
703
|
if (result.status === "error") {
|
|
647
704
|
onError?.(result.error);
|
|
@@ -655,7 +712,17 @@ function FixedBundle(props) {
|
|
|
655
712
|
);
|
|
656
713
|
}
|
|
657
714
|
}, [result, onError]);
|
|
658
|
-
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 () => {
|
|
659
726
|
if (!bundle) return;
|
|
660
727
|
const bundleLines = bundle.products.filter(
|
|
661
728
|
(p) => p.variants.nodes.some(
|
|
@@ -718,6 +785,27 @@ function FixedBundle(props) {
|
|
|
718
785
|
if (result.status === "error") return null;
|
|
719
786
|
if (!bundle) return null;
|
|
720
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;
|
|
721
809
|
return /* @__PURE__ */ jsxs2(
|
|
722
810
|
"div",
|
|
723
811
|
{
|
|
@@ -729,34 +817,58 @@ function FixedBundle(props) {
|
|
|
729
817
|
role: "region",
|
|
730
818
|
"aria-label": bundle.title,
|
|
731
819
|
children: [
|
|
732
|
-
/* @__PURE__ */ jsx2("
|
|
733
|
-
|
|
734
|
-
|
|
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(
|
|
735
825
|
FixedProductRow,
|
|
736
826
|
{
|
|
737
827
|
bundle,
|
|
738
828
|
product,
|
|
739
829
|
currency,
|
|
830
|
+
isOos: oosProductIds.has(product.id),
|
|
740
831
|
onVariantChange: handleVariantChange
|
|
741
832
|
},
|
|
742
833
|
product.id
|
|
743
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
|
+
] }),
|
|
744
856
|
cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
745
857
|
/* @__PURE__ */ jsx2(
|
|
746
858
|
"button",
|
|
747
859
|
{
|
|
748
860
|
className: "lb-bundle__cta",
|
|
749
861
|
onClick: handleAddToCart,
|
|
750
|
-
disabled: addingToCart,
|
|
862
|
+
disabled: addingToCart || oosProductIds.size > 0,
|
|
751
863
|
"aria-busy": addingToCart,
|
|
752
|
-
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"
|
|
753
865
|
}
|
|
754
866
|
)
|
|
755
867
|
]
|
|
756
868
|
}
|
|
757
869
|
);
|
|
758
870
|
}
|
|
759
|
-
function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
871
|
+
function FixedProductRow({ bundle, product, currency, isOos, onVariantChange }) {
|
|
760
872
|
const variants = product.variants.nodes;
|
|
761
873
|
const optionNames = useMemo4(() => {
|
|
762
874
|
const first = variants[0];
|
|
@@ -774,11 +886,13 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
|
774
886
|
(v) => isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id))
|
|
775
887
|
) ?? variants[0];
|
|
776
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;
|
|
777
890
|
const unitPriceText = displayVariant ? formatUnitPrice(
|
|
778
891
|
displayVariant.unitPrice,
|
|
779
892
|
displayVariant.unitPriceMeasurement,
|
|
780
893
|
currency
|
|
781
894
|
) : null;
|
|
895
|
+
const displayQty = displayVariant ? resolveBundleQty(bundle, product.id, displayVariant.id) : 1;
|
|
782
896
|
const thumbImage = displayVariant?.image ?? product.featuredImage;
|
|
783
897
|
const showLowStock = !!displayVariant && shouldShowLowStockBadge(
|
|
784
898
|
displayVariant,
|
|
@@ -786,304 +900,15 @@ function FixedProductRow({ bundle, product, currency, onVariantChange }) {
|
|
|
786
900
|
bundle.widgetConfig.lowStockThreshold,
|
|
787
901
|
bundle.widgetConfig.showLowStockBadge
|
|
788
902
|
);
|
|
789
|
-
return /* @__PURE__ */ jsxs2(
|
|
790
|
-
thumbImage && /* @__PURE__ */ jsx2(
|
|
791
|
-
"img",
|
|
792
|
-
{
|
|
793
|
-
src: thumbImage.url,
|
|
794
|
-
alt: thumbImage.altText ?? product.title,
|
|
795
|
-
className: "lb-bundle__product-image",
|
|
796
|
-
loading: "lazy"
|
|
797
|
-
}
|
|
798
|
-
),
|
|
799
|
-
/* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
|
|
800
|
-
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
801
|
-
/* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: priceText }),
|
|
802
|
-
unitPriceText && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
803
|
-
showLowStock && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-low-stock-badge", children: [
|
|
804
|
-
"Only ",
|
|
805
|
-
displayVariant.quantityAvailable,
|
|
806
|
-
" left"
|
|
807
|
-
] }),
|
|
808
|
-
showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
809
|
-
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
810
|
-
value: o.value,
|
|
811
|
-
label: o.value,
|
|
812
|
-
disabled: o.disabled
|
|
813
|
-
}));
|
|
814
|
-
return /* @__PURE__ */ jsx2(
|
|
815
|
-
VariantDropdown,
|
|
816
|
-
{
|
|
817
|
-
options: dropdownOptions,
|
|
818
|
-
value: selectedValues[optionIndex] ?? null,
|
|
819
|
-
onChange: (v) => setOptionValue(optionIndex, v),
|
|
820
|
-
ariaLabel: optionName
|
|
821
|
-
},
|
|
822
|
-
optionName
|
|
823
|
-
);
|
|
824
|
-
}) })
|
|
825
|
-
] })
|
|
826
|
-
] });
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// src/components/MixMatchBundle.tsx
|
|
830
|
-
import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo5, useState as useState6 } from "react";
|
|
831
|
-
import {
|
|
832
|
-
formatMoney as formatMoney2,
|
|
833
|
-
formatUnitPrice as formatUnitPrice2,
|
|
834
|
-
isVariantFulfillable as isVariantFulfillable2,
|
|
835
|
-
shouldShowLowStockBadge as shouldShowLowStockBadge2,
|
|
836
|
-
maxAddableQuantity,
|
|
837
|
-
DEFAULT_PRODUCT_RULE
|
|
838
|
-
} from "@lime-bundles/core";
|
|
839
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
840
|
-
function ruleFor(bundle, productId) {
|
|
841
|
-
return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;
|
|
842
|
-
}
|
|
843
|
-
function MixMatchBundle(props) {
|
|
844
|
-
const {
|
|
845
|
-
shopDomain,
|
|
846
|
-
storefrontAccessToken,
|
|
847
|
-
bundleGid,
|
|
848
|
-
appUrl,
|
|
849
|
-
analyticsEnabled,
|
|
850
|
-
onAddToCart,
|
|
851
|
-
onError,
|
|
852
|
-
className
|
|
853
|
-
} = props;
|
|
854
|
-
const result = useBundleData({
|
|
855
|
-
shopDomain,
|
|
856
|
-
storefrontAccessToken,
|
|
857
|
-
bundleGid
|
|
858
|
-
});
|
|
859
|
-
const { elementRef, trackAddToCart } = useAnalytics({
|
|
860
|
-
shopDomain,
|
|
861
|
-
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
862
|
-
bundleGid,
|
|
863
|
-
bundleType: "mix_match",
|
|
864
|
-
enabled: analyticsEnabled !== false
|
|
865
|
-
});
|
|
866
|
-
const [selections, setSelections] = useState6(
|
|
867
|
-
/* @__PURE__ */ new Map()
|
|
868
|
-
);
|
|
869
|
-
const [addingToCart, setAddingToCart] = useState6(false);
|
|
870
|
-
const [cartError, setCartError] = useState6(null);
|
|
871
|
-
const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
|
|
872
|
-
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
873
|
-
useEffect7(() => {
|
|
874
|
-
if (result.status === "error") {
|
|
875
|
-
onError?.(result.error);
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
|
|
879
|
-
onError?.(
|
|
880
|
-
new Error(
|
|
881
|
-
`MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
|
|
882
|
-
)
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
}, [result, onError]);
|
|
886
|
-
const showStepper = bundle?.widgetConfig.mixMatchShowQuantitySelector !== false;
|
|
887
|
-
const distinctSelectedProducts = useMemo5(() => {
|
|
888
|
-
const ids = /* @__PURE__ */ new Set();
|
|
889
|
-
for (const sel of selections.values()) {
|
|
890
|
-
if (sel.quantity > 0) ids.add(sel.productId);
|
|
891
|
-
}
|
|
892
|
-
return ids;
|
|
893
|
-
}, [selections]);
|
|
894
|
-
const totalQuantity = useMemo5(
|
|
895
|
-
() => Array.from(selections.values()).reduce((sum, s) => sum + s.quantity, 0),
|
|
896
|
-
[selections]
|
|
897
|
-
);
|
|
898
|
-
const requiredPicks = bundle?.minQuantity ?? 0;
|
|
899
|
-
const meetsMinPicks = distinctSelectedProducts.size >= requiredPicks;
|
|
900
|
-
const valid = !!bundle && meetsMinPicks;
|
|
901
|
-
const remaining = Math.max(0, requiredPicks - distinctSelectedProducts.size);
|
|
902
|
-
const validationMessage = valid ? null : remaining > 0 ? `Pick ${remaining} more product${remaining === 1 ? "" : "s"}` : null;
|
|
903
|
-
const selectVariant = useCallback5(
|
|
904
|
-
(productId, variant) => {
|
|
905
|
-
if (!bundle) return;
|
|
906
|
-
const rule = ruleFor(bundle, productId);
|
|
907
|
-
const maxAddable = maxAddableQuantity(variant, rule.max, 0);
|
|
908
|
-
if (maxAddable < rule.min) return;
|
|
909
|
-
setSelections((prev) => {
|
|
910
|
-
const next = new Map(prev);
|
|
911
|
-
next.set(`${productId}:${variant.id}`, {
|
|
912
|
-
productId,
|
|
913
|
-
variantId: variant.id,
|
|
914
|
-
quantity: rule.min
|
|
915
|
-
});
|
|
916
|
-
return next;
|
|
917
|
-
});
|
|
918
|
-
},
|
|
919
|
-
[bundle]
|
|
920
|
-
);
|
|
921
|
-
const deselect = useCallback5((productId, variantId) => {
|
|
922
|
-
setSelections((prev) => {
|
|
923
|
-
if (!prev.has(`${productId}:${variantId}`)) return prev;
|
|
924
|
-
const next = new Map(prev);
|
|
925
|
-
next.delete(`${productId}:${variantId}`);
|
|
926
|
-
return next;
|
|
927
|
-
});
|
|
928
|
-
}, []);
|
|
929
|
-
const setQuantity = useCallback5(
|
|
930
|
-
(productId, variant, quantity) => {
|
|
931
|
-
if (!bundle) return;
|
|
932
|
-
const rule = ruleFor(bundle, productId);
|
|
933
|
-
const maxAddable = maxAddableQuantity(variant, rule.max, 0);
|
|
934
|
-
const key = `${productId}:${variant.id}`;
|
|
935
|
-
setSelections((prev) => {
|
|
936
|
-
const next = new Map(prev);
|
|
937
|
-
if (quantity < rule.min || maxAddable < rule.min) {
|
|
938
|
-
next.delete(key);
|
|
939
|
-
return next;
|
|
940
|
-
}
|
|
941
|
-
const clamped = Math.min(quantity, maxAddable);
|
|
942
|
-
next.set(key, {
|
|
943
|
-
productId,
|
|
944
|
-
variantId: variant.id,
|
|
945
|
-
quantity: clamped
|
|
946
|
-
});
|
|
947
|
-
return next;
|
|
948
|
-
});
|
|
949
|
-
},
|
|
950
|
-
[bundle]
|
|
951
|
-
);
|
|
952
|
-
const handleAddToCart = useCallback5(async () => {
|
|
953
|
-
if (!bundle || !valid) return;
|
|
954
|
-
const byVariant = /* @__PURE__ */ new Map();
|
|
955
|
-
for (const sel of selections.values()) {
|
|
956
|
-
if (sel.quantity <= 0) continue;
|
|
957
|
-
byVariant.set(
|
|
958
|
-
sel.variantId,
|
|
959
|
-
(byVariant.get(sel.variantId) ?? 0) + sel.quantity
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
const lines = Array.from(byVariant.entries()).map(
|
|
963
|
-
([variantId, quantity]) => ({
|
|
964
|
-
merchandiseId: variantId,
|
|
965
|
-
quantity,
|
|
966
|
-
attributes: [
|
|
967
|
-
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
968
|
-
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
969
|
-
]
|
|
970
|
-
})
|
|
971
|
-
);
|
|
972
|
-
setAddingToCart(true);
|
|
973
|
-
setCartError(null);
|
|
974
|
-
try {
|
|
975
|
-
await onAddToCart(lines);
|
|
976
|
-
const totalPrice = lines.reduce((sum, line) => {
|
|
977
|
-
const product = bundle.products.find(
|
|
978
|
-
(p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
|
|
979
|
-
);
|
|
980
|
-
const variant = product?.variants.nodes.find(
|
|
981
|
-
(v) => v.id === line.merchandiseId
|
|
982
|
-
);
|
|
983
|
-
return sum + parseFloat(variant?.price.amount ?? "0") * line.quantity;
|
|
984
|
-
}, 0);
|
|
985
|
-
trackAddToCart({
|
|
986
|
-
quantity: totalQuantity,
|
|
987
|
-
totalPrice: Math.round(totalPrice * 100) / 100
|
|
988
|
-
});
|
|
989
|
-
setSelections(/* @__PURE__ */ new Map());
|
|
990
|
-
} catch (err) {
|
|
991
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
992
|
-
setCartError(error.message);
|
|
993
|
-
onError?.(error);
|
|
994
|
-
} finally {
|
|
995
|
-
setAddingToCart(false);
|
|
996
|
-
}
|
|
997
|
-
}, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity]);
|
|
998
|
-
if (result.status === "loading") {
|
|
999
|
-
return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
1000
|
-
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
1001
|
-
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
1002
|
-
] });
|
|
1003
|
-
}
|
|
1004
|
-
if (result.status === "error") return null;
|
|
1005
|
-
if (!bundle) return null;
|
|
1006
|
-
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
1007
|
-
return /* @__PURE__ */ jsxs3(
|
|
1008
|
-
"div",
|
|
1009
|
-
{
|
|
1010
|
-
ref: (el) => {
|
|
1011
|
-
elementRef(el);
|
|
1012
|
-
setConfigVarsRef(el);
|
|
1013
|
-
},
|
|
1014
|
-
className: `lb-bundle lb-bundle--mix-match ${className ?? ""}`,
|
|
1015
|
-
role: "region",
|
|
1016
|
-
"aria-label": bundle.title,
|
|
1017
|
-
children: [
|
|
1018
|
-
/* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
|
|
1019
|
-
bundle.discountLabel && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
|
|
1020
|
-
/* @__PURE__ */ jsx3("p", { className: "lb-bundle__instructions", children: requiredPicks > 0 ? `Pick ${requiredPicks} product${requiredPicks === 1 ? "" : "s"}` : "Pick your products" }),
|
|
1021
|
-
/* @__PURE__ */ jsx3("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => /* @__PURE__ */ jsx3(
|
|
1022
|
-
MixMatchProductRow,
|
|
1023
|
-
{
|
|
1024
|
-
bundle,
|
|
1025
|
-
product,
|
|
1026
|
-
currency,
|
|
1027
|
-
showStepper,
|
|
1028
|
-
selections,
|
|
1029
|
-
onSelect: selectVariant,
|
|
1030
|
-
onDeselect: deselect,
|
|
1031
|
-
onSetQuantity: setQuantity
|
|
1032
|
-
},
|
|
1033
|
-
product.id
|
|
1034
|
-
)) }),
|
|
1035
|
-
validationMessage && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__validation", role: "status", children: validationMessage }),
|
|
1036
|
-
cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
1037
|
-
/* @__PURE__ */ jsx3(
|
|
1038
|
-
"button",
|
|
1039
|
-
{
|
|
1040
|
-
className: "lb-bundle__cta",
|
|
1041
|
-
onClick: handleAddToCart,
|
|
1042
|
-
disabled: addingToCart || !valid,
|
|
1043
|
-
"aria-busy": addingToCart,
|
|
1044
|
-
children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
|
|
1045
|
-
}
|
|
1046
|
-
)
|
|
1047
|
-
]
|
|
1048
|
-
}
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
function MixMatchProductRow({
|
|
1052
|
-
bundle,
|
|
1053
|
-
product,
|
|
1054
|
-
currency,
|
|
1055
|
-
showStepper,
|
|
1056
|
-
selections,
|
|
1057
|
-
onSelect,
|
|
1058
|
-
onDeselect,
|
|
1059
|
-
onSetQuantity
|
|
1060
|
-
}) {
|
|
1061
|
-
const variants = product.variants.nodes;
|
|
1062
|
-
const optionNames = useMemo5(() => {
|
|
1063
|
-
const first = variants[0];
|
|
1064
|
-
return first ? first.selectedOptions.map((o) => o.name) : [];
|
|
1065
|
-
}, [variants]);
|
|
1066
|
-
const showPicker = variants.length > 1 && optionNames.length > 0;
|
|
1067
|
-
const rule = ruleFor(bundle, product.id);
|
|
1068
|
-
const { selectedValues, selectedVariant, setOptionValue, optionsFor } = useVariantSelection({ variants, optionNames });
|
|
1069
|
-
const displayVariant = selectedVariant ?? variants.find((v) => isVariantFulfillable2(v, rule.min)) ?? variants[0] ?? null;
|
|
1070
|
-
if (!displayVariant) return null;
|
|
1071
|
-
const fulfillable = isVariantFulfillable2(displayVariant, rule.min);
|
|
1072
|
-
const maxAddable = maxAddableQuantity(displayVariant, rule.max, 0);
|
|
1073
|
-
const key = `${product.id}:${displayVariant.id}`;
|
|
1074
|
-
const selected = selections.get(key);
|
|
1075
|
-
const thumbImage = displayVariant.image ?? product.featuredImage;
|
|
1076
|
-
const unitPriceText = formatUnitPrice2(
|
|
1077
|
-
displayVariant.unitPrice,
|
|
1078
|
-
displayVariant.unitPriceMeasurement,
|
|
1079
|
-
currency
|
|
1080
|
-
);
|
|
1081
|
-
return /* @__PURE__ */ jsxs3(
|
|
903
|
+
return /* @__PURE__ */ jsxs2(
|
|
1082
904
|
"div",
|
|
1083
905
|
{
|
|
1084
|
-
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",
|
|
1085
909
|
children: [
|
|
1086
|
-
|
|
910
|
+
isOos && /* @__PURE__ */ jsx2("span", { className: "lb-bundle-product-row__oos", children: "Out of stock" }),
|
|
911
|
+
thumbImage && /* @__PURE__ */ jsx2(
|
|
1087
912
|
"img",
|
|
1088
913
|
{
|
|
1089
914
|
src: thumbImage.url,
|
|
@@ -1092,27 +917,40 @@ function MixMatchProductRow({
|
|
|
1092
917
|
loading: "lazy"
|
|
1093
918
|
}
|
|
1094
919
|
),
|
|
1095
|
-
/* @__PURE__ */
|
|
1096
|
-
/* @__PURE__ */
|
|
1097
|
-
/* @__PURE__ */
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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: [
|
|
1105
943
|
"Only ",
|
|
1106
944
|
displayVariant.quantityAvailable,
|
|
1107
945
|
" left"
|
|
1108
946
|
] }),
|
|
1109
|
-
showPicker &&
|
|
947
|
+
showPicker && /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
1110
948
|
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1111
949
|
value: o.value,
|
|
1112
950
|
label: o.value,
|
|
1113
951
|
disabled: o.disabled
|
|
1114
952
|
}));
|
|
1115
|
-
return /* @__PURE__ */
|
|
953
|
+
return /* @__PURE__ */ jsx2(
|
|
1116
954
|
VariantDropdown,
|
|
1117
955
|
{
|
|
1118
956
|
options: dropdownOptions,
|
|
@@ -1124,171 +962,2842 @@ function MixMatchProductRow({
|
|
|
1124
962
|
);
|
|
1125
963
|
}) })
|
|
1126
964
|
] }),
|
|
1127
|
-
/* @__PURE__ */
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
"aria-label": `Decrease ${product.title}`,
|
|
1132
|
-
onClick: () => onSetQuantity(
|
|
1133
|
-
product.id,
|
|
1134
|
-
displayVariant,
|
|
1135
|
-
selected.quantity - 1
|
|
1136
|
-
),
|
|
1137
|
-
disabled: selected.quantity <= rule.min,
|
|
1138
|
-
children: "\u2212"
|
|
1139
|
-
}
|
|
1140
|
-
),
|
|
1141
|
-
/* @__PURE__ */ jsx3("span", { "aria-live": "polite", children: selected.quantity }),
|
|
1142
|
-
/* @__PURE__ */ jsx3(
|
|
1143
|
-
"button",
|
|
1144
|
-
{
|
|
1145
|
-
"aria-label": `Increase ${product.title}`,
|
|
1146
|
-
onClick: () => onSetQuantity(
|
|
1147
|
-
product.id,
|
|
1148
|
-
displayVariant,
|
|
1149
|
-
selected.quantity + 1
|
|
1150
|
-
),
|
|
1151
|
-
disabled: selected.quantity >= maxAddable,
|
|
1152
|
-
children: "+"
|
|
1153
|
-
}
|
|
1154
|
-
),
|
|
1155
|
-
/* @__PURE__ */ jsx3(
|
|
1156
|
-
"button",
|
|
1157
|
-
{
|
|
1158
|
-
className: "lb-bundle__remove-btn",
|
|
1159
|
-
"aria-label": `Remove ${product.title}`,
|
|
1160
|
-
onClick: () => onDeselect(product.id, displayVariant.id),
|
|
1161
|
-
children: "Remove"
|
|
1162
|
-
}
|
|
1163
|
-
)
|
|
1164
|
-
] }) : /* @__PURE__ */ jsx3(
|
|
1165
|
-
"button",
|
|
1166
|
-
{
|
|
1167
|
-
className: "lb-bundle__select-btn lb-bundle__select-btn--selected",
|
|
1168
|
-
"aria-label": `Remove ${product.title}`,
|
|
1169
|
-
onClick: () => onDeselect(product.id, displayVariant.id),
|
|
1170
|
-
children: "Selected"
|
|
1171
|
-
}
|
|
1172
|
-
) : /* @__PURE__ */ jsx3(
|
|
1173
|
-
"button",
|
|
1174
|
-
{
|
|
1175
|
-
className: "lb-bundle__select-btn",
|
|
1176
|
-
onClick: () => onSelect(product.id, displayVariant),
|
|
1177
|
-
disabled: !fulfillable || maxAddable <= 0,
|
|
1178
|
-
children: fulfillable && maxAddable > 0 ? "Select" : "Sold out"
|
|
1179
|
-
}
|
|
1180
|
-
) })
|
|
965
|
+
displayVariant && /* @__PURE__ */ jsxs2("span", { className: "lb-bundle-qty-chip", "data-qty-inline": true, children: [
|
|
966
|
+
"\xD7",
|
|
967
|
+
displayQty
|
|
968
|
+
] })
|
|
1181
969
|
]
|
|
1182
970
|
}
|
|
1183
971
|
);
|
|
1184
972
|
}
|
|
1185
973
|
|
|
1186
|
-
// src/components/
|
|
1187
|
-
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";
|
|
1188
976
|
import {
|
|
1189
977
|
formatMoney as formatMoney3,
|
|
1190
978
|
formatUnitPrice as formatUnitPrice3,
|
|
1191
|
-
|
|
1192
|
-
getActiveTier,
|
|
979
|
+
calculateDiscount as calculateDiscount2,
|
|
1193
980
|
isVariantFulfillable as isVariantFulfillable3,
|
|
1194
|
-
|
|
981
|
+
DEFAULT_PRODUCT_RULE as DEFAULT_PRODUCT_RULE2
|
|
1195
982
|
} from "@lime-bundles/core";
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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;
|
|
1208
3563
|
const result = useBundleData({
|
|
1209
3564
|
shopDomain,
|
|
1210
3565
|
storefrontAccessToken,
|
|
1211
3566
|
bundleGid
|
|
1212
3567
|
});
|
|
3568
|
+
const { outOfStockBehavior } = useShopSettings({
|
|
3569
|
+
shopDomain,
|
|
3570
|
+
storefrontAccessToken
|
|
3571
|
+
});
|
|
1213
3572
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
1214
3573
|
shopDomain,
|
|
1215
3574
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
1216
3575
|
bundleGid,
|
|
1217
|
-
bundleType: "
|
|
3576
|
+
bundleType: "multi_step",
|
|
1218
3577
|
enabled: analyticsEnabled !== false
|
|
1219
3578
|
});
|
|
1220
|
-
const [
|
|
1221
|
-
const [
|
|
1222
|
-
const [
|
|
1223
|
-
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;
|
|
1224
3584
|
const setConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
1225
|
-
|
|
3585
|
+
const setModalConfigVarsRef = useWidgetConfigVars(bundle?.widgetConfig);
|
|
3586
|
+
useEffect15(() => {
|
|
1226
3587
|
if (result.status === "error") {
|
|
1227
3588
|
onError?.(result.error);
|
|
1228
3589
|
return;
|
|
1229
3590
|
}
|
|
1230
|
-
if (result.status === "success" && result.bundle.bundleType !== "
|
|
3591
|
+
if (result.status === "success" && result.bundle.bundleType !== "multi_step") {
|
|
1231
3592
|
onError?.(
|
|
1232
3593
|
new Error(
|
|
1233
|
-
`
|
|
3594
|
+
`MultiStepBundle: expected bundleType="multi_step", got "${result.bundle.bundleType}"`
|
|
1234
3595
|
)
|
|
1235
3596
|
);
|
|
1236
3597
|
}
|
|
1237
3598
|
}, [result, onError]);
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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]
|
|
1264
3638
|
);
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
)
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
+
}
|
|
1280
3668
|
}
|
|
1281
|
-
|
|
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
|
+
}));
|
|
1282
3788
|
setAddingToCart(true);
|
|
1283
3789
|
setCartError(null);
|
|
1284
3790
|
try {
|
|
1285
3791
|
await onAddToCart(lines);
|
|
1286
|
-
const
|
|
3792
|
+
const totalPrice = selections.reduce(
|
|
3793
|
+
(sum, stepPicks) => sum + stepPicks.reduce((s, sel) => s + sel.price * sel.quantity, 0),
|
|
3794
|
+
0
|
|
3795
|
+
);
|
|
1287
3796
|
trackAddToCart({
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
totalPrice: Math.round(unitPrice * quantity * 100) / 100
|
|
3797
|
+
quantity: totalQuantity,
|
|
3798
|
+
totalPrice: Math.round(totalPrice * 100) / 100
|
|
1291
3799
|
});
|
|
3800
|
+
setSelections(steps.map(() => []));
|
|
1292
3801
|
} catch (err) {
|
|
1293
3802
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1294
3803
|
setCartError(error.message);
|
|
@@ -1296,169 +3805,306 @@ function VolumeBundle(props) {
|
|
|
1296
3805
|
} finally {
|
|
1297
3806
|
setAddingToCart(false);
|
|
1298
3807
|
}
|
|
1299
|
-
}, [
|
|
1300
|
-
bundle,
|
|
1301
|
-
product,
|
|
1302
|
-
quantity,
|
|
1303
|
-
activeTier,
|
|
1304
|
-
basePrice,
|
|
1305
|
-
onAddToCart,
|
|
1306
|
-
onError,
|
|
1307
|
-
trackAddToCart,
|
|
1308
|
-
selectedVariant,
|
|
1309
|
-
tierSavings
|
|
1310
|
-
]);
|
|
3808
|
+
}, [bundle, selections, valid, onAddToCart, onError, trackAddToCart, totalQuantity, steps]);
|
|
1311
3809
|
if (result.status === "loading") {
|
|
1312
|
-
return /* @__PURE__ */
|
|
1313
|
-
/* @__PURE__ */
|
|
1314
|
-
/* @__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" })
|
|
1315
3813
|
] });
|
|
1316
3814
|
}
|
|
1317
3815
|
if (result.status === "error") return null;
|
|
1318
|
-
if (!bundle) return null;
|
|
1319
|
-
|
|
1320
|
-
|
|
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(
|
|
1321
3834
|
"div",
|
|
1322
3835
|
{
|
|
1323
3836
|
ref: (el) => {
|
|
1324
3837
|
elementRef(el);
|
|
1325
3838
|
setConfigVarsRef(el);
|
|
1326
3839
|
},
|
|
1327
|
-
className: `lb-bundle lb-bundle--
|
|
3840
|
+
className: `lb-bundle lb-bundle--multi-step lb-mix-match lb-multi-step ${className ?? ""}`,
|
|
1328
3841
|
role: "region",
|
|
1329
3842
|
"aria-label": bundle.title,
|
|
3843
|
+
"data-required-quantity": totalMin,
|
|
1330
3844
|
children: [
|
|
1331
|
-
/* @__PURE__ */
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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",
|
|
1335
3852
|
{
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
/* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
1344
|
-
/* @__PURE__ */ jsxs4("p", { className: "lb-bundle__product-price", children: [
|
|
1345
|
-
formatMoney3(basePrice, currency),
|
|
1346
|
-
" each"
|
|
1347
|
-
] }),
|
|
1348
|
-
unitPriceText && /* @__PURE__ */ jsx4("p", { className: "lb-bundle__product-unit-price", children: unitPriceText }),
|
|
1349
|
-
showPicker && /* @__PURE__ */ jsx4("div", { className: "lb-bundle__product-variant-pickers", children: optionNames.map((optionName, optionIndex) => {
|
|
1350
|
-
const dropdownOptions = optionsFor(optionIndex).map((o) => ({
|
|
1351
|
-
value: o.value,
|
|
1352
|
-
label: o.value,
|
|
1353
|
-
disabled: o.disabled
|
|
1354
|
-
}));
|
|
1355
|
-
return /* @__PURE__ */ jsx4(
|
|
1356
|
-
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",
|
|
1357
3860
|
{
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
onChange: (v) => setOptionValue(optionIndex, v),
|
|
1361
|
-
ariaLabel: optionName
|
|
3861
|
+
className: `lb-mix-match__progress-segment${i < completedSteps ? " lb-mix-match__progress-segment--filled" : ""}`,
|
|
3862
|
+
"data-progress-segment": true
|
|
1362
3863
|
},
|
|
1363
|
-
|
|
1364
|
-
)
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
{
|
|
1371
|
-
className: "lb-bundle__tiers",
|
|
1372
|
-
role: "table",
|
|
1373
|
-
"aria-label": "Volume discounts",
|
|
1374
|
-
children: tierSavings.map((ts) => /* @__PURE__ */ jsxs4(
|
|
1375
|
-
"div",
|
|
1376
|
-
{
|
|
1377
|
-
className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
|
|
1378
|
-
role: "row",
|
|
1379
|
-
children: [
|
|
1380
|
-
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
|
|
1381
|
-
ts.tier.minQuantity,
|
|
1382
|
-
"+ items"
|
|
1383
|
-
] }),
|
|
1384
|
-
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-price", role: "cell", children: [
|
|
1385
|
-
formatMoney3(ts.unitPrice, currency),
|
|
1386
|
-
" each"
|
|
1387
|
-
] }),
|
|
1388
|
-
/* @__PURE__ */ jsxs4("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
|
|
1389
|
-
"Save ",
|
|
1390
|
-
ts.savingsPercent.toFixed(0),
|
|
1391
|
-
"%"
|
|
1392
|
-
] })
|
|
1393
|
-
]
|
|
1394
|
-
},
|
|
1395
|
-
ts.tier.minQuantity
|
|
1396
|
-
))
|
|
1397
|
-
}
|
|
1398
|
-
),
|
|
1399
|
-
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-selector", children: [
|
|
1400
|
-
/* @__PURE__ */ jsx4("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
|
|
1401
|
-
/* @__PURE__ */ jsxs4("div", { className: "lb-bundle__quantity-control", children: [
|
|
1402
|
-
/* @__PURE__ */ jsx4(
|
|
1403
|
-
"button",
|
|
1404
|
-
{
|
|
1405
|
-
"aria-label": "Decrease quantity",
|
|
1406
|
-
onClick: () => setQuantity((q) => Math.max(1, q - 1)),
|
|
1407
|
-
children: "\u2212"
|
|
1408
|
-
}
|
|
1409
|
-
),
|
|
1410
|
-
/* @__PURE__ */ jsx4(
|
|
1411
|
-
"input",
|
|
3864
|
+
i
|
|
3865
|
+
))
|
|
3866
|
+
}
|
|
3867
|
+
),
|
|
3868
|
+
/* @__PURE__ */ jsxs8("div", { className: "lb-mix-match__progress-labels", children: [
|
|
3869
|
+
/* @__PURE__ */ jsx8(
|
|
3870
|
+
"span",
|
|
1412
3871
|
{
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
onChange: (e) => {
|
|
1418
|
-
const val = parseInt(e.target.value, 10);
|
|
1419
|
-
if (!isNaN(val) && val > 0) setQuantity(val);
|
|
1420
|
-
},
|
|
1421
|
-
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`
|
|
1422
3876
|
}
|
|
1423
3877
|
),
|
|
1424
|
-
/* @__PURE__ */
|
|
1425
|
-
"
|
|
3878
|
+
/* @__PURE__ */ jsx8(
|
|
3879
|
+
"span",
|
|
1426
3880
|
{
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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"
|
|
1430
3885
|
}
|
|
1431
3886
|
)
|
|
1432
3887
|
] })
|
|
1433
3888
|
] }),
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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
|
+
] })
|
|
1444
4018
|
] }),
|
|
1445
|
-
/* @__PURE__ */
|
|
4019
|
+
cartError && /* @__PURE__ */ jsx8("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
4020
|
+
/* @__PURE__ */ jsx8(
|
|
1446
4021
|
"button",
|
|
1447
4022
|
{
|
|
1448
4023
|
className: "lb-bundle__cta",
|
|
1449
4024
|
onClick: handleAddToCart,
|
|
1450
|
-
disabled: addingToCart,
|
|
4025
|
+
disabled: addingToCart || !valid,
|
|
1451
4026
|
"aria-busy": addingToCart,
|
|
1452
|
-
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
|
|
1453
4053
|
}
|
|
1454
4054
|
)
|
|
1455
4055
|
]
|
|
1456
4056
|
}
|
|
1457
4057
|
);
|
|
1458
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
|
+
}
|
|
1459
4105
|
|
|
1460
4106
|
// src/hooks/useBundlesForProduct.ts
|
|
1461
|
-
import { useState as
|
|
4107
|
+
import { useState as useState13, useEffect as useEffect16 } from "react";
|
|
1462
4108
|
import {
|
|
1463
4109
|
fetchBundlesForProduct,
|
|
1464
4110
|
injectCustomCss as injectCustomCss2
|
|
@@ -1469,10 +4115,10 @@ var INITIAL_STATE2 = {
|
|
|
1469
4115
|
error: null
|
|
1470
4116
|
};
|
|
1471
4117
|
function useBundlesForProduct(options) {
|
|
1472
|
-
const [state, setState] =
|
|
4118
|
+
const [state, setState] = useState13(
|
|
1473
4119
|
INITIAL_STATE2
|
|
1474
4120
|
);
|
|
1475
|
-
|
|
4121
|
+
useEffect16(() => {
|
|
1476
4122
|
const controller = new AbortController();
|
|
1477
4123
|
setState(INITIAL_STATE2);
|
|
1478
4124
|
fetchBundlesForProduct({
|
|
@@ -1492,13 +4138,13 @@ function useBundlesForProduct(options) {
|
|
|
1492
4138
|
error: err instanceof Error ? err : new Error(String(err))
|
|
1493
4139
|
});
|
|
1494
4140
|
});
|
|
1495
|
-
|
|
4141
|
+
fetchShopSettings({
|
|
1496
4142
|
shopDomain: options.shopDomain,
|
|
1497
4143
|
storefrontAccessToken: options.storefrontAccessToken,
|
|
1498
4144
|
signal: controller.signal
|
|
1499
|
-
}).then((
|
|
4145
|
+
}).then((settings) => {
|
|
1500
4146
|
if (controller.signal.aborted) return;
|
|
1501
|
-
injectCustomCss2(options.shopDomain,
|
|
4147
|
+
injectCustomCss2(options.shopDomain, settings.customCss);
|
|
1502
4148
|
}).catch(() => {
|
|
1503
4149
|
});
|
|
1504
4150
|
return () => {
|
|
@@ -1520,7 +4166,6 @@ import {
|
|
|
1520
4166
|
mergeWidgetConfig,
|
|
1521
4167
|
applyWidgetConfigVars as applyWidgetConfigVars2,
|
|
1522
4168
|
thumbnailRatioVars,
|
|
1523
|
-
applyABVariantB,
|
|
1524
4169
|
parseCents,
|
|
1525
4170
|
formatCents,
|
|
1526
4171
|
percentageDiscountUnit,
|
|
@@ -1528,18 +4173,17 @@ import {
|
|
|
1528
4173
|
computeBundleSaleCents,
|
|
1529
4174
|
calculateTierSavings as calculateTierSavings2,
|
|
1530
4175
|
getActiveTier as getActiveTier2,
|
|
4176
|
+
productsForStep as productsForStep3,
|
|
4177
|
+
stepHeadroom as stepHeadroom3,
|
|
1531
4178
|
validateQuantity,
|
|
1532
4179
|
transformImageUrl,
|
|
1533
4180
|
formatCountdown,
|
|
1534
|
-
formatMoney as
|
|
1535
|
-
formatUnitPrice as
|
|
1536
|
-
calculateDiscount as
|
|
4181
|
+
formatMoney as formatMoney8,
|
|
4182
|
+
formatUnitPrice as formatUnitPrice8,
|
|
4183
|
+
calculateDiscount as calculateDiscount4,
|
|
1537
4184
|
reportImpression as reportImpression2,
|
|
1538
4185
|
reportAddToCart as reportAddToCart2,
|
|
1539
4186
|
observeImpression as observeImpression2,
|
|
1540
|
-
getABTestAssignment,
|
|
1541
|
-
setConsent,
|
|
1542
|
-
hasConsent,
|
|
1543
4187
|
injectCustomCss as injectCustomCss3,
|
|
1544
4188
|
sanitizeCustomCss,
|
|
1545
4189
|
createStorefrontClient as createStorefrontClient3,
|
|
@@ -1548,22 +4192,26 @@ import {
|
|
|
1548
4192
|
parseMetaobjectBundleStrict as parseMetaobjectBundleStrict2,
|
|
1549
4193
|
BUNDLE_METAOBJECT_QUERY as BUNDLE_METAOBJECT_QUERY2,
|
|
1550
4194
|
BUNDLES_FOR_PRODUCT_QUERY,
|
|
1551
|
-
|
|
4195
|
+
SHOP_SETTINGS_QUERY as SHOP_SETTINGS_QUERY2,
|
|
4196
|
+
DEFAULT_OUT_OF_STOCK_BEHAVIOR as DEFAULT_OUT_OF_STOCK_BEHAVIOR3,
|
|
4197
|
+
parseOutOfStockBehavior as parseOutOfStockBehavior2
|
|
1552
4198
|
} from "@lime-bundles/core";
|
|
1553
4199
|
export {
|
|
1554
4200
|
BUNDLES_FOR_PRODUCT_QUERY,
|
|
1555
4201
|
BUNDLE_METAOBJECT_QUERY2 as BUNDLE_METAOBJECT_QUERY,
|
|
4202
|
+
BogoBundle,
|
|
1556
4203
|
BundleParseError2 as BundleParseError,
|
|
4204
|
+
DEFAULT_OUT_OF_STOCK_BEHAVIOR3 as DEFAULT_OUT_OF_STOCK_BEHAVIOR,
|
|
1557
4205
|
FixedBundle,
|
|
1558
4206
|
MixMatchBundle,
|
|
1559
|
-
|
|
4207
|
+
MultiStepBundle,
|
|
4208
|
+
SHOP_SETTINGS_QUERY2 as SHOP_SETTINGS_QUERY,
|
|
1560
4209
|
StorefrontApiError,
|
|
1561
4210
|
VariantDropdown,
|
|
1562
4211
|
VolumeBundle,
|
|
1563
4212
|
WIDGET_CONFIG_DEFAULTS,
|
|
1564
|
-
applyABVariantB,
|
|
1565
4213
|
applyWidgetConfigVars2 as applyWidgetConfigVars,
|
|
1566
|
-
|
|
4214
|
+
calculateDiscount4 as calculateDiscount,
|
|
1567
4215
|
calculateTierSavings2 as calculateTierSavings,
|
|
1568
4216
|
computeBundleSaleCents,
|
|
1569
4217
|
computeFixedPricing,
|
|
@@ -1571,29 +4219,32 @@ export {
|
|
|
1571
4219
|
fetchBundleData,
|
|
1572
4220
|
fetchBundlesForProduct2 as fetchBundlesForProduct,
|
|
1573
4221
|
fetchShopCustomCss,
|
|
4222
|
+
fetchShopSettings,
|
|
4223
|
+
fetchShopSettingsOrDefault,
|
|
1574
4224
|
formatCents,
|
|
1575
4225
|
formatCountdown,
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
getABTestAssignment,
|
|
4226
|
+
formatMoney8 as formatMoney,
|
|
4227
|
+
formatUnitPrice8 as formatUnitPrice,
|
|
1579
4228
|
getActiveTier2 as getActiveTier,
|
|
1580
|
-
hasConsent,
|
|
1581
4229
|
injectCustomCss3 as injectCustomCss,
|
|
1582
4230
|
mergeWidgetConfig,
|
|
1583
4231
|
observeImpression2 as observeImpression,
|
|
1584
4232
|
parseCents,
|
|
1585
4233
|
parseMetaobjectBundle,
|
|
1586
4234
|
parseMetaobjectBundleStrict2 as parseMetaobjectBundleStrict,
|
|
4235
|
+
parseOutOfStockBehavior2 as parseOutOfStockBehavior,
|
|
1587
4236
|
percentageDiscountUnit,
|
|
4237
|
+
productsForStep3 as productsForStep,
|
|
1588
4238
|
reportAddToCart2 as reportAddToCart,
|
|
1589
4239
|
reportImpression2 as reportImpression,
|
|
1590
4240
|
sanitizeCustomCss,
|
|
1591
|
-
|
|
4241
|
+
stepHeadroom3 as stepHeadroom,
|
|
1592
4242
|
thumbnailRatioVars,
|
|
1593
4243
|
transformImageUrl,
|
|
1594
4244
|
useAnalytics,
|
|
1595
4245
|
useBundleData,
|
|
1596
4246
|
useBundlesForProduct,
|
|
4247
|
+
useShopSettings,
|
|
1597
4248
|
useVariantSelection,
|
|
1598
4249
|
useWidgetConfigVars,
|
|
1599
4250
|
validateQuantity
|