@lime-bundles/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,689 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ FixedBundle: () => FixedBundle,
24
+ MixMatchBundle: () => MixMatchBundle,
25
+ VolumeBundle: () => VolumeBundle,
26
+ useAnalytics: () => useAnalytics,
27
+ useBundleData: () => useBundleData,
28
+ useCart: () => useCart
29
+ });
30
+ module.exports = __toCommonJS(src_exports);
31
+
32
+ // src/components/FixedBundle.tsx
33
+ var import_react4 = require("react");
34
+ var import_core4 = require("@lime-bundles/core");
35
+
36
+ // src/hooks/useBundleData.ts
37
+ var import_react = require("react");
38
+ var import_core = require("@lime-bundles/core");
39
+ function useBundleData(options) {
40
+ const [bundle, setBundle] = (0, import_react.useState)(null);
41
+ const [loading, setLoading] = (0, import_react.useState)(true);
42
+ const [error, setError] = (0, import_react.useState)(null);
43
+ const abortRef = (0, import_react.useRef)(null);
44
+ (0, import_react.useEffect)(() => {
45
+ abortRef.current?.abort();
46
+ const controller = new AbortController();
47
+ abortRef.current = controller;
48
+ setLoading(true);
49
+ setError(null);
50
+ const client = (0, import_core.createStorefrontClient)({
51
+ shopDomain: options.shopDomain,
52
+ accessToken: options.storefrontAccessToken
53
+ });
54
+ client.query(import_core.BUNDLE_METAOBJECT_QUERY, {
55
+ id: options.bundleGid
56
+ }).then((data) => {
57
+ if (controller.signal.aborted) return;
58
+ if (!data.metaobject) {
59
+ setError("Bundle not found");
60
+ setBundle(null);
61
+ } else {
62
+ const parsed = (0, import_core.parseMetaobjectBundle)(
63
+ data.metaobject.id,
64
+ data.metaobject.fields
65
+ );
66
+ if (parsed) {
67
+ setBundle(parsed);
68
+ } else {
69
+ setError("Bundle is not active or has expired");
70
+ setBundle(null);
71
+ }
72
+ }
73
+ setLoading(false);
74
+ }).catch((err) => {
75
+ if (controller.signal.aborted) return;
76
+ setError(err instanceof Error ? err.message : "Failed to load bundle");
77
+ setLoading(false);
78
+ });
79
+ return () => {
80
+ controller.abort();
81
+ };
82
+ }, [options.shopDomain, options.storefrontAccessToken, options.bundleGid]);
83
+ return { bundle, loading, error };
84
+ }
85
+
86
+ // src/hooks/useCart.ts
87
+ var import_react2 = require("react");
88
+ var import_core2 = require("@lime-bundles/core");
89
+ function useCart(options) {
90
+ const [loading, setLoading] = (0, import_react2.useState)(false);
91
+ const [error, setError] = (0, import_react2.useState)(null);
92
+ const [lastResult, setLastResult] = (0, import_react2.useState)(null);
93
+ const cart = (0, import_react2.useMemo)(() => {
94
+ const apiType = options.cartApi ?? (0, import_core2.detectCartApi)();
95
+ if (apiType === "ajax") {
96
+ return (0, import_core2.createAjaxCartApi)(options.bundleGid, options.bundleType);
97
+ }
98
+ const client = (0, import_core2.createStorefrontClient)({
99
+ shopDomain: options.shopDomain,
100
+ accessToken: options.storefrontAccessToken
101
+ });
102
+ return (0, import_core2.createStorefrontCartApi)(
103
+ client,
104
+ options.bundleGid,
105
+ options.bundleType,
106
+ options.cartId
107
+ );
108
+ }, [
109
+ options.shopDomain,
110
+ options.storefrontAccessToken,
111
+ options.bundleGid,
112
+ options.bundleType,
113
+ options.cartId,
114
+ options.cartApi
115
+ ]);
116
+ const addToCart = (0, import_react2.useCallback)(
117
+ async (items) => {
118
+ setLoading(true);
119
+ setError(null);
120
+ try {
121
+ const result = await cart.addLines(items);
122
+ setLastResult(result);
123
+ if (!result.success) {
124
+ setError(result.error ?? "Failed to add to cart");
125
+ }
126
+ return result;
127
+ } catch (err) {
128
+ const message = err instanceof Error ? err.message : "Failed to add to cart";
129
+ setError(message);
130
+ const result = { success: false, error: message };
131
+ setLastResult(result);
132
+ return result;
133
+ } finally {
134
+ setLoading(false);
135
+ }
136
+ },
137
+ [cart]
138
+ );
139
+ return { addToCart, loading, error, lastResult };
140
+ }
141
+
142
+ // src/hooks/useAnalytics.ts
143
+ var import_react3 = require("react");
144
+ var import_core3 = require("@lime-bundles/core");
145
+ function useAnalytics(options) {
146
+ const impressionFiredRef = (0, import_react3.useRef)(false);
147
+ const [element, setElement] = (0, import_react3.useState)(null);
148
+ const elementRef = (0, import_react3.useCallback)((el) => {
149
+ setElement(el);
150
+ }, []);
151
+ const config = (0, import_react3.useMemo)(
152
+ () => ({ shopDomain: options.shopDomain, appUrl: options.appUrl }),
153
+ [options.shopDomain, options.appUrl]
154
+ );
155
+ (0, import_react3.useEffect)(() => {
156
+ impressionFiredRef.current = false;
157
+ }, [options.bundleGid, options.abVariant]);
158
+ (0, import_react3.useEffect)(() => {
159
+ if (options.enabled === false) return;
160
+ if (impressionFiredRef.current) return;
161
+ if (!element) return;
162
+ const cleanup = (0, import_core3.observeImpression)(element, () => {
163
+ if (impressionFiredRef.current) return;
164
+ impressionFiredRef.current = true;
165
+ (0, import_core3.reportImpression)(config, {
166
+ bundleGid: options.bundleGid,
167
+ bundleType: options.bundleType,
168
+ abTestId: options.abTestId,
169
+ abVariant: options.abVariant
170
+ });
171
+ });
172
+ return cleanup;
173
+ }, [
174
+ element,
175
+ config,
176
+ options.enabled,
177
+ options.bundleGid,
178
+ options.bundleType,
179
+ options.abTestId,
180
+ options.abVariant
181
+ ]);
182
+ const trackAddToCart = (0, import_react3.useCallback)(
183
+ (event) => {
184
+ if (options.enabled === false) return;
185
+ (0, import_core3.reportAddToCart)(config, {
186
+ bundleGid: options.bundleGid,
187
+ bundleType: options.bundleType,
188
+ productId: event.productId,
189
+ quantity: event.quantity,
190
+ totalPrice: event.totalPrice,
191
+ abTestId: options.abTestId,
192
+ abVariant: options.abVariant
193
+ });
194
+ },
195
+ [
196
+ config,
197
+ options.enabled,
198
+ options.bundleGid,
199
+ options.bundleType,
200
+ options.abTestId,
201
+ options.abVariant
202
+ ]
203
+ );
204
+ return { elementRef, trackAddToCart };
205
+ }
206
+
207
+ // src/components/FixedBundle.tsx
208
+ var import_jsx_runtime = require("react/jsx-runtime");
209
+ function FixedBundle(props) {
210
+ const {
211
+ shopDomain,
212
+ storefrontAccessToken,
213
+ bundleGid,
214
+ cartId,
215
+ cartApi,
216
+ appUrl,
217
+ analyticsEnabled,
218
+ onAddToCart,
219
+ onError,
220
+ className
221
+ } = props;
222
+ const { bundle, loading, error: loadError } = useBundleData({
223
+ shopDomain,
224
+ storefrontAccessToken,
225
+ bundleGid
226
+ });
227
+ const { addToCart, loading: cartLoading, error: cartError } = useCart({
228
+ shopDomain,
229
+ storefrontAccessToken,
230
+ bundleGid,
231
+ bundleType: "fixed",
232
+ cartId,
233
+ cartApi
234
+ });
235
+ const { elementRef, trackAddToCart } = useAnalytics({
236
+ shopDomain,
237
+ appUrl: appUrl ?? `https://${shopDomain}`,
238
+ bundleGid,
239
+ bundleType: "fixed",
240
+ enabled: analyticsEnabled !== false
241
+ });
242
+ const handleAddToCart = (0, import_react4.useCallback)(async () => {
243
+ if (!bundle) return;
244
+ const items = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
245
+ const variant = p.variants.nodes.find((v) => v.availableForSale);
246
+ return { variantId: variant.id, quantity: 1 };
247
+ });
248
+ if (items.length === 0) return;
249
+ const result = await addToCart(items);
250
+ if (result.success) {
251
+ const totalPrice = bundle.products.reduce((sum, p) => {
252
+ const price = parseFloat(p.priceRange.minVariantPrice.amount);
253
+ return sum + (0, import_core4.calculateDiscount)(price, bundle.discountConfig.discountType, bundle.discountConfig.discountValue);
254
+ }, 0);
255
+ trackAddToCart({
256
+ productId: bundle.products[0]?.id ?? "",
257
+ quantity: 1,
258
+ totalPrice: Math.round(totalPrice * 100) / 100
259
+ });
260
+ onAddToCart?.(items);
261
+ }
262
+ }, [bundle, addToCart, trackAddToCart, onAddToCart]);
263
+ if (loading) {
264
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
265
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
266
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
267
+ ] });
268
+ }
269
+ if (loadError || !bundle) {
270
+ if (onError) onError(loadError ?? "Bundle not found");
271
+ return null;
272
+ }
273
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
274
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
275
+ "div",
276
+ {
277
+ ref: elementRef,
278
+ className: `lb-bundle lb-bundle--fixed ${className ?? ""}`,
279
+ role: "region",
280
+ "aria-label": bundle.title,
281
+ children: [
282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
283
+ bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
284
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lb-bundle__product", part: "product", children: [
285
+ product.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
286
+ "img",
287
+ {
288
+ src: product.featuredImage.url,
289
+ alt: product.featuredImage.altText ?? product.title,
290
+ className: "lb-bundle__product-image",
291
+ loading: "lazy"
292
+ }
293
+ ),
294
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lb-bundle__product-info", children: [
295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
296
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core4.formatMoney)(product.priceRange.minVariantPrice.amount, currency) })
297
+ ] })
298
+ ] }, product.id)) }),
299
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
301
+ "button",
302
+ {
303
+ className: "lb-bundle__cta",
304
+ onClick: handleAddToCart,
305
+ disabled: cartLoading,
306
+ "aria-busy": cartLoading,
307
+ children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? "Add Bundle to Cart"
308
+ }
309
+ )
310
+ ]
311
+ }
312
+ );
313
+ }
314
+
315
+ // src/components/MixMatchBundle.tsx
316
+ var import_react5 = require("react");
317
+ var import_core5 = require("@lime-bundles/core");
318
+ var import_jsx_runtime2 = require("react/jsx-runtime");
319
+ function MixMatchBundle(props) {
320
+ const {
321
+ shopDomain,
322
+ storefrontAccessToken,
323
+ bundleGid,
324
+ cartId,
325
+ cartApi,
326
+ appUrl,
327
+ analyticsEnabled,
328
+ onAddToCart,
329
+ onError,
330
+ className
331
+ } = props;
332
+ const { bundle, loading, error: loadError } = useBundleData({
333
+ shopDomain,
334
+ storefrontAccessToken,
335
+ bundleGid
336
+ });
337
+ const { addToCart, loading: cartLoading, error: cartError } = useCart({
338
+ shopDomain,
339
+ storefrontAccessToken,
340
+ bundleGid,
341
+ bundleType: "mix_match",
342
+ cartId,
343
+ cartApi
344
+ });
345
+ const { elementRef, trackAddToCart } = useAnalytics({
346
+ shopDomain,
347
+ appUrl: appUrl ?? `https://${shopDomain}`,
348
+ bundleGid,
349
+ bundleType: "mix_match",
350
+ enabled: analyticsEnabled !== false
351
+ });
352
+ const [selections, setSelections] = (0, import_react5.useState)(/* @__PURE__ */ new Map());
353
+ const totalQuantity = Array.from(selections.values()).reduce(
354
+ (sum, s) => sum + s.quantity,
355
+ 0
356
+ );
357
+ const validation = bundle ? (0, import_core5.validateQuantity)(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
358
+ const toggleProduct = (0, import_react5.useCallback)(
359
+ (productId, variant) => {
360
+ setSelections((prev) => {
361
+ const next = new Map(prev);
362
+ const key = `${productId}:${variant.id}`;
363
+ if (next.has(key)) {
364
+ next.delete(key);
365
+ } else {
366
+ next.set(key, { productId, variantId: variant.id, quantity: 1 });
367
+ }
368
+ return next;
369
+ });
370
+ },
371
+ []
372
+ );
373
+ const updateQuantity = (0, import_react5.useCallback)(
374
+ (productId, variantId, quantity) => {
375
+ setSelections((prev) => {
376
+ const next = new Map(prev);
377
+ const key = `${productId}:${variantId}`;
378
+ if (quantity <= 0) {
379
+ next.delete(key);
380
+ } else {
381
+ next.set(key, { productId, variantId, quantity });
382
+ }
383
+ return next;
384
+ });
385
+ },
386
+ []
387
+ );
388
+ const handleAddToCart = (0, import_react5.useCallback)(async () => {
389
+ if (!bundle || !validation.valid) return;
390
+ const items = Array.from(selections.values()).map((s) => ({
391
+ variantId: s.variantId,
392
+ quantity: s.quantity
393
+ }));
394
+ const result = await addToCart(items);
395
+ if (result.success) {
396
+ const totalPrice = items.reduce((sum, item) => {
397
+ const product = bundle.products.find(
398
+ (p) => p.variants.nodes.some((v) => v.id === item.variantId)
399
+ );
400
+ const variant = product?.variants.nodes.find(
401
+ (v) => v.id === item.variantId
402
+ );
403
+ return sum + parseFloat(variant?.price.amount ?? "0") * item.quantity;
404
+ }, 0);
405
+ trackAddToCart({
406
+ productId: bundle.products[0]?.id ?? "",
407
+ quantity: totalQuantity,
408
+ totalPrice: Math.round(totalPrice * 100) / 100
409
+ });
410
+ onAddToCart?.(items);
411
+ setSelections(/* @__PURE__ */ new Map());
412
+ }
413
+ }, [bundle, selections, validation.valid, addToCart, trackAddToCart, totalQuantity, onAddToCart]);
414
+ if (loading) {
415
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
416
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
417
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
418
+ ] });
419
+ }
420
+ if (loadError || !bundle) {
421
+ if (onError) onError(loadError ?? "Bundle not found");
422
+ return null;
423
+ }
424
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
425
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
426
+ "div",
427
+ {
428
+ ref: elementRef,
429
+ className: `lb-bundle lb-bundle--mix-match ${className ?? ""}`,
430
+ role: "region",
431
+ "aria-label": bundle.title,
432
+ children: [
433
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
434
+ bundle.discountLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
435
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__instructions", children: bundle.minQuantity && bundle.maxQuantity ? `Select ${bundle.minQuantity}\u2013${bundle.maxQuantity} items` : bundle.minQuantity ? `Select at least ${bundle.minQuantity} items` : bundle.maxQuantity ? `Select up to ${bundle.maxQuantity} items` : "Select your items" }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => {
437
+ const defaultVariant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
438
+ if (!defaultVariant) return null;
439
+ const key = `${product.id}:${defaultVariant.id}`;
440
+ const selected = selections.get(key);
441
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
442
+ "div",
443
+ {
444
+ className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
445
+ children: [
446
+ product.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
447
+ "img",
448
+ {
449
+ src: product.featuredImage.url,
450
+ alt: product.featuredImage.altText ?? product.title,
451
+ className: "lb-bundle__product-image",
452
+ loading: "lazy"
453
+ }
454
+ ),
455
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product-info", children: [
456
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
457
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core5.formatMoney)(defaultVariant.price.amount, currency) })
458
+ ] }),
459
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
460
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
461
+ "button",
462
+ {
463
+ "aria-label": `Decrease ${product.title}`,
464
+ onClick: () => updateQuantity(product.id, defaultVariant.id, selected.quantity - 1),
465
+ children: "\u2212"
466
+ }
467
+ ),
468
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: selected.quantity }),
469
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
470
+ "button",
471
+ {
472
+ "aria-label": `Increase ${product.title}`,
473
+ onClick: () => updateQuantity(product.id, defaultVariant.id, selected.quantity + 1),
474
+ children: "+"
475
+ }
476
+ )
477
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
478
+ "button",
479
+ {
480
+ className: "lb-bundle__select-btn",
481
+ onClick: () => toggleProduct(product.id, defaultVariant),
482
+ disabled: !defaultVariant.availableForSale,
483
+ children: defaultVariant.availableForSale ? "Select" : "Sold out"
484
+ }
485
+ ) })
486
+ ]
487
+ },
488
+ product.id
489
+ );
490
+ }) }),
491
+ validation.message && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
492
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
493
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
494
+ "button",
495
+ {
496
+ className: "lb-bundle__cta",
497
+ onClick: handleAddToCart,
498
+ disabled: cartLoading || !validation.valid,
499
+ "aria-busy": cartLoading,
500
+ children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${totalQuantity} Items to Cart`
501
+ }
502
+ )
503
+ ]
504
+ }
505
+ );
506
+ }
507
+
508
+ // src/components/VolumeBundle.tsx
509
+ var import_react6 = require("react");
510
+ var import_core6 = require("@lime-bundles/core");
511
+ var import_jsx_runtime3 = require("react/jsx-runtime");
512
+ function VolumeBundle(props) {
513
+ const {
514
+ shopDomain,
515
+ storefrontAccessToken,
516
+ bundleGid,
517
+ cartId,
518
+ cartApi,
519
+ appUrl,
520
+ analyticsEnabled,
521
+ onAddToCart,
522
+ onError,
523
+ className
524
+ } = props;
525
+ const { bundle, loading, error: loadError } = useBundleData({
526
+ shopDomain,
527
+ storefrontAccessToken,
528
+ bundleGid
529
+ });
530
+ const { addToCart, loading: cartLoading, error: cartError } = useCart({
531
+ shopDomain,
532
+ storefrontAccessToken,
533
+ bundleGid,
534
+ bundleType: "volume",
535
+ cartId,
536
+ cartApi
537
+ });
538
+ const { elementRef, trackAddToCart } = useAnalytics({
539
+ shopDomain,
540
+ appUrl: appUrl ?? `https://${shopDomain}`,
541
+ bundleGid,
542
+ bundleType: "volume",
543
+ enabled: analyticsEnabled !== false
544
+ });
545
+ const [quantity, setQuantity] = (0, import_react6.useState)(1);
546
+ const product = bundle?.products[0];
547
+ const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
548
+ const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
549
+ const tierSavings = bundle ? (0, import_core6.calculateTierSavings)(bundle.volumeTiers, basePrice, quantity) : [];
550
+ const activeTier = bundle ? (0, import_core6.getActiveTier)(bundle.volumeTiers, quantity) : null;
551
+ const handleAddToCart = (0, import_react6.useCallback)(async () => {
552
+ if (!bundle || !product) return;
553
+ const variant = product.variants.nodes.find((v) => v.availableForSale);
554
+ if (!variant) return;
555
+ const items = [
556
+ { variantId: variant.id, quantity }
557
+ ];
558
+ const result = await addToCart(items);
559
+ if (result.success) {
560
+ const unitPrice = activeTier ? basePrice * (1 - activeTier.discountValue / 100) : basePrice;
561
+ trackAddToCart({
562
+ productId: product.id,
563
+ quantity,
564
+ totalPrice: Math.round(unitPrice * quantity * 100) / 100
565
+ });
566
+ onAddToCart?.(items);
567
+ }
568
+ }, [bundle, product, quantity, activeTier, basePrice, addToCart, trackAddToCart, onAddToCart]);
569
+ if (loading) {
570
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
571
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
572
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--tiers" })
573
+ ] });
574
+ }
575
+ if (loadError || !bundle || !product) {
576
+ if (onError) onError(loadError ?? "Bundle not found");
577
+ return null;
578
+ }
579
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
580
+ "div",
581
+ {
582
+ ref: elementRef,
583
+ className: `lb-bundle lb-bundle--volume ${className ?? ""}`,
584
+ role: "region",
585
+ "aria-label": bundle.title,
586
+ children: [
587
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "lb-bundle__title", children: bundle.title }),
588
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
589
+ product.featuredImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
590
+ "img",
591
+ {
592
+ src: product.featuredImage.url,
593
+ alt: product.featuredImage.altText ?? product.title,
594
+ className: "lb-bundle__product-image",
595
+ loading: "lazy"
596
+ }
597
+ ),
598
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product-info", children: [
599
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
600
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "lb-bundle__product-price", children: [
601
+ (0, import_core6.formatMoney)(basePrice, currency),
602
+ " each"
603
+ ] })
604
+ ] })
605
+ ] }),
606
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-bundle__tiers", role: "table", "aria-label": "Volume discounts", children: tierSavings.map((ts) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
607
+ "div",
608
+ {
609
+ className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
610
+ role: "row",
611
+ children: [
612
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
613
+ ts.tier.minQuantity,
614
+ "+ items"
615
+ ] }),
616
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-price", role: "cell", children: [
617
+ (0, import_core6.formatMoney)(ts.unitPrice, currency),
618
+ " each"
619
+ ] }),
620
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
621
+ "Save ",
622
+ ts.savingsPercent.toFixed(0),
623
+ "%"
624
+ ] }),
625
+ ts.tier.label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__tier-label", role: "cell", children: ts.tier.label })
626
+ ]
627
+ },
628
+ ts.tier.minQuantity
629
+ )) }),
630
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-selector", children: [
631
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
632
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
634
+ "button",
635
+ {
636
+ "aria-label": "Decrease quantity",
637
+ onClick: () => setQuantity((q) => Math.max(1, q - 1)),
638
+ children: "\u2212"
639
+ }
640
+ ),
641
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
642
+ "input",
643
+ {
644
+ id: `lb-qty-${bundle.id}`,
645
+ type: "number",
646
+ min: 1,
647
+ value: quantity,
648
+ onChange: (e) => {
649
+ const val = parseInt(e.target.value, 10);
650
+ if (!isNaN(val) && val > 0) setQuantity(val);
651
+ },
652
+ className: "lb-bundle__quantity-input"
653
+ }
654
+ ),
655
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
656
+ "button",
657
+ {
658
+ "aria-label": "Increase quantity",
659
+ onClick: () => setQuantity((q) => q + 1),
660
+ children: "+"
661
+ }
662
+ )
663
+ ] })
664
+ ] }),
665
+ cartError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
666
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
667
+ "button",
668
+ {
669
+ className: "lb-bundle__cta",
670
+ onClick: handleAddToCart,
671
+ disabled: cartLoading,
672
+ "aria-busy": cartLoading,
673
+ children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`
674
+ }
675
+ )
676
+ ]
677
+ }
678
+ );
679
+ }
680
+ // Annotate the CommonJS export names for ESM import in node:
681
+ 0 && (module.exports = {
682
+ FixedBundle,
683
+ MixMatchBundle,
684
+ VolumeBundle,
685
+ useAnalytics,
686
+ useBundleData,
687
+ useCart
688
+ });
689
+ //# sourceMappingURL=index.cjs.map