@rebuy/rebuy-hydrogen 2.3.1 → 3.0.0-beta.1
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/package.json +52 -26
- package/src/components/AddToCartBtn/AddToCartBtn.tsx +45 -0
- package/src/components/AddToCartBtn/HydrogenAddToCartBtn.tsx +43 -0
- package/src/components/AddToCartBtn/HydrogenReactAddToCartBtn.tsx +35 -0
- package/src/components/AddToCartBtn/index.ts +1 -0
- package/src/components/AddToCartBtn/types.ts +27 -0
- package/src/components/ProductCard/ProductCard.tsx +70 -0
- package/src/components/ProductCard/index.ts +1 -0
- package/src/components/ProductCard/types.ts +10 -0
- package/src/components/ProductPrice/ProductPrice.tsx +49 -0
- package/src/components/ProductPrice/index.ts +1 -0
- package/src/components/Title/Title.tsx +20 -0
- package/src/components/Title/index.ts +1 -0
- package/src/components/Title/types.ts +7 -0
- package/src/components/VariantSelect/VariantSelect.tsx +45 -0
- package/src/components/VariantSelect/index.ts +1 -0
- package/src/components/VariantSelect/types.ts +6 -0
- package/src/context/RebuyContext.tsx +9 -0
- package/src/hooks/titleLevel.tsx +42 -0
- package/src/index.ts +7 -0
- package/src/providers/RebuyHydrogenContextProvider.tsx +112 -0
- package/src/providers/RebuyHydrogenReactContextProvider.tsx +192 -0
- package/src/providers/types.ts +58 -0
- package/src/queries/cart.queries.ts +467 -0
- package/src/types/common.ts +8 -0
- package/src/types/css.d.ts +11 -0
- package/src/types/env.d.ts +12 -0
- package/src/types/rebuy.d.ts +31 -0
- package/src/types/rebuyCustom.ts +263 -0
- package/src/types/rebuySmartCart.ts +188 -0
- package/src/types/shopify.ts +142 -0
- package/src/types/widgets.ts +29 -0
- package/src/utils/convertToRebuyProduct.tsx +319 -0
- package/src/utils/createContextParameters.ts +142 -0
- package/src/utils/getEncodedAttributes.ts +11 -0
- package/src/utils/getRebuyConfig.ts +31 -0
- package/src/widgetContainer/RebuyWidgetContainer.tsx +183 -0
- package/src/widgets/RebuyCompleteTheLook/RebuyCompleteTheLook.tsx +50 -0
- package/src/widgets/RebuyCompleteTheLook/index.ts +1 -0
- package/src/widgets/RebuyCompleteTheLook/types.ts +5 -0
- package/src/widgets/RebuyDynamicBundleProducts/BundleImages.tsx +62 -0
- package/src/widgets/RebuyDynamicBundleProducts/BundlePrice.tsx +93 -0
- package/src/widgets/RebuyDynamicBundleProducts/BundleSelection.tsx +65 -0
- package/src/widgets/RebuyDynamicBundleProducts/RebuyDynamicBundleProducts.tsx +118 -0
- package/src/widgets/RebuyDynamicBundleProducts/Select.tsx +41 -0
- package/src/widgets/RebuyDynamicBundleProducts/index.ts +1 -0
- package/src/widgets/RebuyDynamicBundleProducts/types.ts +23 -0
- package/src/widgets/RebuyProductAddOns/RebuyProductAddOnCard.tsx +66 -0
- package/src/widgets/RebuyProductAddOns/RebuyProductAddOns.tsx +218 -0
- package/src/widgets/RebuyProductAddOns/index.ts +1 -0
- package/src/widgets/RebuyProductAddOns/types.ts +24 -0
- package/src/widgets/RebuyProductRecommendations/RebuyProductRecommendations.tsx +50 -0
- package/src/widgets/RebuyProductRecommendations/index.ts +1 -0
- package/src/widgets/RebuyProductRecommendations/types.ts +5 -0
- package/RebuyCompleteTheLook.client.jsx +0 -188
- package/RebuyContextProvider.client.jsx +0 -222
- package/RebuyContexts.client.jsx +0 -3
- package/RebuyDynamicBundleProducts.client.jsx +0 -415
- package/RebuyProductAddOnCard.client.jsx +0 -89
- package/RebuyProductAddOns.client.jsx +0 -227
- package/RebuyProductRecommendations.client.jsx +0 -68
- package/RebuyProductViewed.client.jsx +0 -62
- package/RebuyRecentlyViewedProducts.client.jsx +0 -68
- package/RebuyWidgetContainer.client.jsx +0 -136
@@ -1,222 +0,0 @@
|
|
1
|
-
import { RebuyClient } from '@rebuy/rebuy';
|
2
|
-
import { RebuyContext } from '@rebuy/rebuy-hydrogen/RebuyContexts.client';
|
3
|
-
import * as Utilities from '@rebuy/rebuy/utilities';
|
4
|
-
import { useCart, useShop, useUrl } from '@shopify/hydrogen';
|
5
|
-
import { useEffect, useMemo, useState } from 'react';
|
6
|
-
|
7
|
-
const REBUY_API_KEY = import.meta.env.PUBLIC_REBUY_API_KEY;
|
8
|
-
const API = '/api/v1';
|
9
|
-
|
10
|
-
const getEncodedAttributes = (attributes) =>
|
11
|
-
encodeURIComponent(
|
12
|
-
JSON.stringify(
|
13
|
-
attributes.reduce(
|
14
|
-
(merged, { key, value }) => ({ ...merged, [key]: value }),
|
15
|
-
{}
|
16
|
-
)
|
17
|
-
)
|
18
|
-
);
|
19
|
-
|
20
|
-
export const RebuyContextProvider = ({ children }) => {
|
21
|
-
// Shopify
|
22
|
-
const cart = useCart();
|
23
|
-
const shop = useShop();
|
24
|
-
const url = useUrl();
|
25
|
-
|
26
|
-
// Default state
|
27
|
-
const [initialized, setInitialized] = useState(false);
|
28
|
-
const [rebuyConfig, setRebuyConfig] = useState(null);
|
29
|
-
const [config, setConfig] = useState({ shop: null });
|
30
|
-
// const [Rebuy, setRebuy] = useState(null);
|
31
|
-
const queryObject = Utilities.queryStringToObject(url.search);
|
32
|
-
const utmObject = Utilities.utmObjectFromString(url);
|
33
|
-
|
34
|
-
if (utmObject != null) {
|
35
|
-
for (const key in utmObject) {
|
36
|
-
url.searchParams.set(key, utmObject[key]);
|
37
|
-
}
|
38
|
-
}
|
39
|
-
|
40
|
-
// Initialization
|
41
|
-
useEffect(() => {
|
42
|
-
const getRebuyConfig = async () => {
|
43
|
-
try {
|
44
|
-
const request = {
|
45
|
-
url: `${API}/user/config`,
|
46
|
-
parameters: { shop: shop.storeDomain },
|
47
|
-
};
|
48
|
-
|
49
|
-
const { data: rebuy, ...response } = await new RebuyClient(
|
50
|
-
REBUY_API_KEY
|
51
|
-
).getShieldedAsset(request.url, request.parameters);
|
52
|
-
|
53
|
-
// Missing Rebuy shop data?
|
54
|
-
if (!rebuy?.shop) {
|
55
|
-
throw new Error(
|
56
|
-
'Rebuy configuration is not properly set up - missing shop',
|
57
|
-
{ cause: response }
|
58
|
-
);
|
59
|
-
}
|
60
|
-
|
61
|
-
setRebuyConfig(rebuy);
|
62
|
-
} catch (err) {
|
63
|
-
console.warn('Error fetching Rebuy shop config');
|
64
|
-
console.error(err, err.cause);
|
65
|
-
}
|
66
|
-
};
|
67
|
-
|
68
|
-
const onBeforeReady = async () => {
|
69
|
-
// Initializing...
|
70
|
-
if (!rebuyConfig?.shop) {
|
71
|
-
return await getRebuyConfig();
|
72
|
-
}
|
73
|
-
};
|
74
|
-
|
75
|
-
onBeforeReady();
|
76
|
-
}, [rebuyConfig, shop]);
|
77
|
-
|
78
|
-
useEffect(() => {
|
79
|
-
const applyConfig = async () => {
|
80
|
-
// Still fetching Rebuy config OR config already applied. Abort!
|
81
|
-
if (!rebuyConfig || config.shop) return;
|
82
|
-
|
83
|
-
// Bring it all together
|
84
|
-
const appConfig = { ...config, ...rebuyConfig };
|
85
|
-
|
86
|
-
setConfig(appConfig);
|
87
|
-
};
|
88
|
-
|
89
|
-
applyConfig();
|
90
|
-
}, [config, rebuyConfig]);
|
91
|
-
|
92
|
-
useEffect(() => {
|
93
|
-
const onReady = () => {
|
94
|
-
// Still initializing... Abort!
|
95
|
-
if (!config.shop) return;
|
96
|
-
|
97
|
-
// Initialized!
|
98
|
-
setInitialized(true);
|
99
|
-
// const { api_key, cache_key } = config.shop;
|
100
|
-
// setRebuy(new RebuyClient(api_key, { cache_key }));
|
101
|
-
};
|
102
|
-
|
103
|
-
onReady();
|
104
|
-
}, [config]);
|
105
|
-
|
106
|
-
const contextParameters = useMemo(() => {
|
107
|
-
// Still initializing... Abort!
|
108
|
-
if (!config.shop) return null;
|
109
|
-
|
110
|
-
const { cache_key } = config.shop;
|
111
|
-
const contextParameters = {
|
112
|
-
url: url.href,
|
113
|
-
cache_key,
|
114
|
-
};
|
115
|
-
// Cart object
|
116
|
-
const cartContext = {};
|
117
|
-
|
118
|
-
// Set time
|
119
|
-
if (Object.prototype.hasOwnProperty.call(queryObject, 'time')) {
|
120
|
-
contextParameters.time = queryObject.time;
|
121
|
-
}
|
122
|
-
|
123
|
-
// Set Cart: token
|
124
|
-
if (cart.id) {
|
125
|
-
cartContext.token = Utilities.getIdFromGraphUrl(cart.id, 'Cart');
|
126
|
-
contextParameters.cart_token = Utilities.getIdFromGraphUrl(
|
127
|
-
cart.id,
|
128
|
-
'Cart'
|
129
|
-
);
|
130
|
-
}
|
131
|
-
|
132
|
-
// Set Cart: subtotal
|
133
|
-
if (cart.cost?.subtotalAmount?.amount) {
|
134
|
-
const { amount } = cart.cost.subtotalAmount;
|
135
|
-
|
136
|
-
cartContext.subtotal = Utilities.amountToCents(amount);
|
137
|
-
contextParameters.cart_subtotal = Utilities.amountToCents(amount);
|
138
|
-
}
|
139
|
-
|
140
|
-
// Set Cart: line count
|
141
|
-
if (typeof cart.lines != 'undefined') {
|
142
|
-
const totalLines = cart.lines.length;
|
143
|
-
|
144
|
-
cartContext.line_count = totalLines;
|
145
|
-
contextParameters.cart_count = totalLines;
|
146
|
-
contextParameters.cart_line_count = totalLines;
|
147
|
-
}
|
148
|
-
|
149
|
-
// Set Cart: item count
|
150
|
-
if (typeof cart.totalQuantity != 'undefined') {
|
151
|
-
const { totalQuantity } = cart;
|
152
|
-
|
153
|
-
cartContext.item_count = totalQuantity;
|
154
|
-
contextParameters.cart_item_count = totalQuantity;
|
155
|
-
}
|
156
|
-
|
157
|
-
// Set Cart: line items
|
158
|
-
if (typeof cart.lines != 'undefined') {
|
159
|
-
cartContext.items = [];
|
160
|
-
|
161
|
-
for (const cartItem of cart.lines) {
|
162
|
-
const item = {
|
163
|
-
quantity: cartItem.quantity,
|
164
|
-
};
|
165
|
-
|
166
|
-
if (cartItem.product?.id) {
|
167
|
-
item.product_id = Utilities.getIdFromGraphUrl(
|
168
|
-
cartItem.product.id,
|
169
|
-
'Product'
|
170
|
-
);
|
171
|
-
}
|
172
|
-
|
173
|
-
if (cartItem.merchandise?.id) {
|
174
|
-
item.variant_id = Utilities.getIdFromGraphUrl(
|
175
|
-
cartItem.merchandise.id,
|
176
|
-
'ProductVariant'
|
177
|
-
);
|
178
|
-
}
|
179
|
-
|
180
|
-
if (cartItem.attributes) {
|
181
|
-
item.properties = getEncodedAttributes(cartItem.attributes);
|
182
|
-
}
|
183
|
-
|
184
|
-
// TBD: item.selling_plan
|
185
|
-
|
186
|
-
cartContext.items.push(item);
|
187
|
-
}
|
188
|
-
}
|
189
|
-
|
190
|
-
// Set Cart: attributes
|
191
|
-
if (cart.attributes) {
|
192
|
-
cartContext.attributes = getEncodedAttributes(cart.attributes);
|
193
|
-
}
|
194
|
-
|
195
|
-
// Set Cart: notes
|
196
|
-
if (cart.note) {
|
197
|
-
cartContext.note = cart.note;
|
198
|
-
}
|
199
|
-
|
200
|
-
// Set cart
|
201
|
-
contextParameters.cart = cartContext;
|
202
|
-
|
203
|
-
return contextParameters;
|
204
|
-
}, [cart, config, queryObject, url]);
|
205
|
-
|
206
|
-
// Static reference (JSON) + memoization
|
207
|
-
// ^ prevent re-rendering children when context params are unchanged
|
208
|
-
const contextParametersJSON = JSON.stringify(contextParameters);
|
209
|
-
const contextValue = useMemo(
|
210
|
-
() => ({ contextParameters: JSON.parse(contextParametersJSON) }),
|
211
|
-
[contextParametersJSON]
|
212
|
-
);
|
213
|
-
|
214
|
-
// Still initializing...
|
215
|
-
if (!initialized) return null;
|
216
|
-
|
217
|
-
return (
|
218
|
-
<RebuyContext.Provider value={contextValue}>
|
219
|
-
{children}
|
220
|
-
</RebuyContext.Provider>
|
221
|
-
);
|
222
|
-
};
|
package/RebuyContexts.client.jsx
DELETED
@@ -1,415 +0,0 @@
|
|
1
|
-
import {
|
2
|
-
Image,
|
3
|
-
Link,
|
4
|
-
Money,
|
5
|
-
useCart,
|
6
|
-
useProductOptions,
|
7
|
-
} from '@shopify/hydrogen';
|
8
|
-
import clsx from 'clsx';
|
9
|
-
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
10
|
-
import { Heading, Text } from '~/components';
|
11
|
-
import { Button } from '~/components/elements';
|
12
|
-
|
13
|
-
const isDiscounted = (price, compareAtPrice) =>
|
14
|
-
Number(compareAtPrice?.amount) > Number(price?.amount);
|
15
|
-
|
16
|
-
const CompareAtPrice = ({ data: compareAtPrice, className }) => {
|
17
|
-
const styles = clsx('strike', className);
|
18
|
-
|
19
|
-
return (
|
20
|
-
<Money
|
21
|
-
withoutTrailingZeros
|
22
|
-
data={compareAtPrice}
|
23
|
-
as="span"
|
24
|
-
className={styles}
|
25
|
-
/>
|
26
|
-
);
|
27
|
-
};
|
28
|
-
|
29
|
-
const RebuyProductPrice = ({ selectedVariant = {} }) => {
|
30
|
-
const { priceV2: price, compareAtPriceV2: compareAtPrice } =
|
31
|
-
selectedVariant;
|
32
|
-
|
33
|
-
return (
|
34
|
-
price && (
|
35
|
-
<div className="gap-4">
|
36
|
-
<Text className="flex gap-2">
|
37
|
-
<Money data={price} withoutTrailingZeros as="span" />
|
38
|
-
{isDiscounted(price, compareAtPrice) && (
|
39
|
-
<CompareAtPrice
|
40
|
-
data={compareAtPrice}
|
41
|
-
className={'opacity-50'}
|
42
|
-
/>
|
43
|
-
)}
|
44
|
-
</Text>
|
45
|
-
</div>
|
46
|
-
)
|
47
|
-
);
|
48
|
-
};
|
49
|
-
|
50
|
-
const VariantSelector = ({ product, onSelectVariant, className = '' }) => {
|
51
|
-
const styles = clsx('rounded py-1', className);
|
52
|
-
|
53
|
-
return (
|
54
|
-
product && (
|
55
|
-
<select
|
56
|
-
onChange={(e) => onSelectVariant(product, e.target.value)}
|
57
|
-
aria-label="select variant"
|
58
|
-
className={styles}
|
59
|
-
value={product.selectedVariant.id}
|
60
|
-
>
|
61
|
-
<optgroup label={getOptionsLabel(product)}>
|
62
|
-
{product.variants.nodes.map(({ id, title }) => (
|
63
|
-
<option key={id + '-BundleVariant'} value={id}>
|
64
|
-
{title}
|
65
|
-
</option>
|
66
|
-
))}
|
67
|
-
</optgroup>
|
68
|
-
</select>
|
69
|
-
)
|
70
|
-
);
|
71
|
-
};
|
72
|
-
|
73
|
-
const getOptionsLabel = (product) => {
|
74
|
-
const options = product.variants.nodes[0].selectedOptions;
|
75
|
-
const optionsFromKeys = Object.keys(options[0]);
|
76
|
-
const optionsFromValues = options.map((option) => option.name);
|
77
|
-
const useValues = optionsFromKeys.every((key) =>
|
78
|
-
['name', 'value'].includes(key)
|
79
|
-
);
|
80
|
-
|
81
|
-
// Return delimited label for available option(s) e.g. Color / Size, Scent, etc
|
82
|
-
return (useValues ? optionsFromValues : optionsFromKeys).join(' / ');
|
83
|
-
};
|
84
|
-
|
85
|
-
const AddToCartPriceMarkup = ({ products, addLineItemsToCart }) => {
|
86
|
-
const isDisabled =
|
87
|
-
products.filter((product) => product.selected).length < 1;
|
88
|
-
|
89
|
-
const totalBundlePrice = () => {
|
90
|
-
let total = 0;
|
91
|
-
let currencyCode = 'USD';
|
92
|
-
|
93
|
-
for (const product of products) {
|
94
|
-
if (product.selected && product.selectedVariant) {
|
95
|
-
const { priceV2: price } = product.selectedVariant;
|
96
|
-
|
97
|
-
total += Number(price.amount);
|
98
|
-
currencyCode = price.currencyCode;
|
99
|
-
}
|
100
|
-
}
|
101
|
-
|
102
|
-
return {
|
103
|
-
amount: String(total),
|
104
|
-
currencyCode,
|
105
|
-
};
|
106
|
-
};
|
107
|
-
|
108
|
-
const totalBundleCompareAtPrice = () => {
|
109
|
-
let compareAtTotal = 0;
|
110
|
-
let currencyCode = 'USD';
|
111
|
-
|
112
|
-
for (const product of products) {
|
113
|
-
if (product.selected && product.selectedVariant) {
|
114
|
-
const { priceV2: price, compareAtPriceV2: compareAtPrice } =
|
115
|
-
product.selectedVariant;
|
116
|
-
|
117
|
-
currencyCode = price.currencyCode;
|
118
|
-
compareAtTotal += Number((compareAtPrice || price).amount);
|
119
|
-
}
|
120
|
-
}
|
121
|
-
|
122
|
-
return {
|
123
|
-
amount: String(compareAtTotal),
|
124
|
-
currencyCode,
|
125
|
-
};
|
126
|
-
};
|
127
|
-
|
128
|
-
const price = totalBundlePrice();
|
129
|
-
const compareAtPrice = totalBundleCompareAtPrice();
|
130
|
-
|
131
|
-
return (
|
132
|
-
products.length > 0 && (
|
133
|
-
<div className="flex items-center flex-col">
|
134
|
-
{!isDisabled && (
|
135
|
-
<Text className="flex items-center gap-2 mb-2">
|
136
|
-
<span>Total Price:</span>
|
137
|
-
<Money data={price} withoutTrailingZeros as="span" />
|
138
|
-
{isDiscounted(price, compareAtPrice) && (
|
139
|
-
<CompareAtPrice
|
140
|
-
data={compareAtPrice}
|
141
|
-
className={'opacity-50'}
|
142
|
-
/>
|
143
|
-
)}
|
144
|
-
</Text>
|
145
|
-
)}
|
146
|
-
|
147
|
-
<Button
|
148
|
-
as="button"
|
149
|
-
variant={isDisabled ? 'secondary' : 'primary'}
|
150
|
-
disabled={isDisabled}
|
151
|
-
onClick={addLineItemsToCart}
|
152
|
-
>
|
153
|
-
<Text as="span">
|
154
|
-
{isDisabled ? 'Create your bundle below' : 'Add to bag'}
|
155
|
-
</Text>
|
156
|
-
</Button>
|
157
|
-
</div>
|
158
|
-
)
|
159
|
-
);
|
160
|
-
};
|
161
|
-
|
162
|
-
const BundleImages = ({ products }) => {
|
163
|
-
const selected = products.filter((product) => product.selected);
|
164
|
-
|
165
|
-
return (
|
166
|
-
<ul className="flex flex-nowrap justify-center gap-1 mb-4">
|
167
|
-
{products.map((product, index) => {
|
168
|
-
const image = product.selectedVariant.image;
|
169
|
-
const productImage = image ? (
|
170
|
-
<Image
|
171
|
-
className="fadeIn"
|
172
|
-
data={image}
|
173
|
-
alt={image.altText || `Picture of ${product.title}`}
|
174
|
-
title={product.title}
|
175
|
-
width={80}
|
176
|
-
height={80}
|
177
|
-
loaderOptions={{ scale: 2 }}
|
178
|
-
/>
|
179
|
-
) : (
|
180
|
-
// No image defined
|
181
|
-
product.title
|
182
|
-
);
|
183
|
-
// Hide delimiter for first selected item
|
184
|
-
const showDelimiter = selected[0]?.id !== product.id;
|
185
|
-
|
186
|
-
return (
|
187
|
-
<Fragment key={product.id + '-BundleImages-' + index}>
|
188
|
-
{product.selected && (
|
189
|
-
<>
|
190
|
-
{showDelimiter && (
|
191
|
-
<li className="flex items-center">
|
192
|
-
<span className="font-semibold">+</span>
|
193
|
-
</li>
|
194
|
-
)}
|
195
|
-
<li className="flex items-center">
|
196
|
-
{product.default ? (
|
197
|
-
// Already on product page
|
198
|
-
productImage
|
199
|
-
) : (
|
200
|
-
// Link to product
|
201
|
-
<Link
|
202
|
-
to={`/products/${product.handle}`}
|
203
|
-
title={product.title}
|
204
|
-
>
|
205
|
-
{productImage}
|
206
|
-
</Link>
|
207
|
-
)}
|
208
|
-
</li>
|
209
|
-
</>
|
210
|
-
)}
|
211
|
-
</Fragment>
|
212
|
-
);
|
213
|
-
})}
|
214
|
-
</ul>
|
215
|
-
);
|
216
|
-
};
|
217
|
-
|
218
|
-
const BundleSelection = ({ products, onToggleBundleItem, onSelectVariant }) => {
|
219
|
-
return (
|
220
|
-
<ul>
|
221
|
-
{products.map((product, index) => {
|
222
|
-
const bundleItemRowStyle = clsx(
|
223
|
-
'mb-4',
|
224
|
-
!product.selected ? 'opacity-50' : ''
|
225
|
-
);
|
226
|
-
const bundleItemLabelStyle = clsx(
|
227
|
-
'flex flex-row gap-2 mb-1 cursor-pointer'
|
228
|
-
);
|
229
|
-
const { availableForSale } = product.selectedVariant;
|
230
|
-
const isOutOfStock = !availableForSale;
|
231
|
-
|
232
|
-
return (
|
233
|
-
<li
|
234
|
-
key={product.id + '-BundleSelection' + index}
|
235
|
-
className={bundleItemRowStyle}
|
236
|
-
>
|
237
|
-
<div className="flex flex-row items-start gap-2">
|
238
|
-
<input
|
239
|
-
className="mt-1 rounded-sm accent-black cursor-pointer"
|
240
|
-
type="checkbox"
|
241
|
-
value={product.id}
|
242
|
-
id={`${product.id}-toggle`}
|
243
|
-
checked={product.selected && availableForSale}
|
244
|
-
disabled={isOutOfStock}
|
245
|
-
onChange={() => onToggleBundleItem(product)}
|
246
|
-
/>
|
247
|
-
<div className="flex-grow">
|
248
|
-
<label
|
249
|
-
htmlFor={`${product.id}-toggle`}
|
250
|
-
className={bundleItemLabelStyle}
|
251
|
-
>
|
252
|
-
{isOutOfStock && <b>SOLD OUT</b>}
|
253
|
-
{product.default && <b>This item:</b>}
|
254
|
-
{product.title}
|
255
|
-
|
256
|
-
<RebuyProductPrice
|
257
|
-
selectedVariant={
|
258
|
-
product.selectedVariant
|
259
|
-
}
|
260
|
-
/>
|
261
|
-
</label>
|
262
|
-
|
263
|
-
{product.variants?.nodes.length > 1 && (
|
264
|
-
<VariantSelector
|
265
|
-
product={product}
|
266
|
-
onSelectVariant={onSelectVariant}
|
267
|
-
className="w-full"
|
268
|
-
/>
|
269
|
-
)}
|
270
|
-
</div>
|
271
|
-
</div>
|
272
|
-
</li>
|
273
|
-
);
|
274
|
-
})}
|
275
|
-
</ul>
|
276
|
-
);
|
277
|
-
};
|
278
|
-
|
279
|
-
export const RebuyDynamicBundleProducts = ({
|
280
|
-
product = {},
|
281
|
-
products = [],
|
282
|
-
metadata = {},
|
283
|
-
title = 'Frequently purchased together',
|
284
|
-
className = '',
|
285
|
-
}) => {
|
286
|
-
// Hooks
|
287
|
-
const { linesAdd } = useCart();
|
288
|
-
const { selectedVariant } = useProductOptions();
|
289
|
-
|
290
|
-
// State
|
291
|
-
const [bundleProducts, setBundleProduct] = useState([]);
|
292
|
-
const language = metadata.widget?.language ?? {};
|
293
|
-
const componentTitle = language?.title || title;
|
294
|
-
const styles = clsx('grid gap-4 py-6 md:py-8 lg:py-12', className);
|
295
|
-
|
296
|
-
const rebuyAttributes = useMemo(() => {
|
297
|
-
return [
|
298
|
-
{ key: '_source', value: 'Rebuy' },
|
299
|
-
{
|
300
|
-
key: '_attribution',
|
301
|
-
value: 'Rebuy Dynamic Bundle Recommendations',
|
302
|
-
},
|
303
|
-
];
|
304
|
-
}, []);
|
305
|
-
|
306
|
-
// Initializiation
|
307
|
-
useEffect(() => {
|
308
|
-
const formatProducts = (products = []) => {
|
309
|
-
for (let product of products) {
|
310
|
-
product.selectedVariant = { ...product.variants.nodes[0] };
|
311
|
-
product.selected = product.selectedVariant.availableForSale;
|
312
|
-
}
|
313
|
-
|
314
|
-
return products;
|
315
|
-
};
|
316
|
-
|
317
|
-
// Flag the main product (PDP)
|
318
|
-
product.default = true;
|
319
|
-
|
320
|
-
setBundleProduct(formatProducts([product, ...products]));
|
321
|
-
}, [product, products]);
|
322
|
-
|
323
|
-
// Mirror selectedVariant from PDP + allow override
|
324
|
-
useEffect(() => {
|
325
|
-
const bundleWithVariantMatch = (currentBundle) => {
|
326
|
-
for (let product of currentBundle) {
|
327
|
-
if (product.default) {
|
328
|
-
product.selectedVariant = selectedVariant;
|
329
|
-
product.selected = product.selectedVariant.availableForSale;
|
330
|
-
break;
|
331
|
-
}
|
332
|
-
}
|
333
|
-
|
334
|
-
return [...currentBundle];
|
335
|
-
};
|
336
|
-
|
337
|
-
setBundleProduct(bundleWithVariantMatch);
|
338
|
-
}, [selectedVariant]);
|
339
|
-
|
340
|
-
const onToggleBundleItem = useCallback(
|
341
|
-
(product) => {
|
342
|
-
product.selected = !product.selected;
|
343
|
-
|
344
|
-
setBundleProduct([...bundleProducts]);
|
345
|
-
},
|
346
|
-
[bundleProducts]
|
347
|
-
);
|
348
|
-
|
349
|
-
const onSelectVariant = useCallback(
|
350
|
-
(product, variantId) => {
|
351
|
-
const variant = product.variants.nodes.find(
|
352
|
-
({ id }) => id === variantId
|
353
|
-
);
|
354
|
-
|
355
|
-
if (variant) {
|
356
|
-
product.selectedVariant = variant;
|
357
|
-
product.selected = variant.availableForSale;
|
358
|
-
|
359
|
-
setBundleProduct([...bundleProducts]);
|
360
|
-
}
|
361
|
-
},
|
362
|
-
[bundleProducts]
|
363
|
-
);
|
364
|
-
|
365
|
-
const addLineItemsToCart = useCallback(() => {
|
366
|
-
const itemsToAdd = [];
|
367
|
-
|
368
|
-
for (let product of bundleProducts) {
|
369
|
-
if (product.selected && product.selectedVariant) {
|
370
|
-
itemsToAdd.push({
|
371
|
-
quantity: 1,
|
372
|
-
merchandiseId: product.selectedVariant.id,
|
373
|
-
attributes: rebuyAttributes,
|
374
|
-
});
|
375
|
-
}
|
376
|
-
}
|
377
|
-
|
378
|
-
linesAdd(itemsToAdd);
|
379
|
-
}, [bundleProducts, linesAdd, rebuyAttributes]);
|
380
|
-
|
381
|
-
return (
|
382
|
-
bundleProducts.length > 0 && (
|
383
|
-
<div className={styles}>
|
384
|
-
{componentTitle && (
|
385
|
-
<Heading
|
386
|
-
size="lead"
|
387
|
-
width="full"
|
388
|
-
className="px-6 md:px-8 lg:px-12"
|
389
|
-
>
|
390
|
-
{componentTitle}
|
391
|
-
</Heading>
|
392
|
-
)}
|
393
|
-
|
394
|
-
<div className="px-6 md:px-8 lg:px-12">
|
395
|
-
<div className="flex flex-col lg:flex-row mb-4">
|
396
|
-
<BundleImages products={bundleProducts} />
|
397
|
-
|
398
|
-
<AddToCartPriceMarkup
|
399
|
-
addLineItemsToCart={addLineItemsToCart}
|
400
|
-
products={bundleProducts}
|
401
|
-
/>
|
402
|
-
</div>
|
403
|
-
|
404
|
-
<BundleSelection
|
405
|
-
products={bundleProducts}
|
406
|
-
onToggleBundleItem={onToggleBundleItem}
|
407
|
-
onSelectVariant={onSelectVariant}
|
408
|
-
/>
|
409
|
-
</div>
|
410
|
-
</div>
|
411
|
-
)
|
412
|
-
);
|
413
|
-
};
|
414
|
-
|
415
|
-
export default RebuyDynamicBundleProducts;
|