@riosst100/pwa-marketplace 3.1.2 → 3.1.4
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 +1 -1
- package/src/components/BrandLandingPage/brandLanding.js +0 -5
- package/src/components/CheckoutHeader/accountTrigger.js +104 -0
- package/src/components/CheckoutHeader/cartTrigger.js +110 -0
- package/src/components/CheckoutHeader/cartTrigger.module.css +47 -0
- package/src/components/CheckoutHeader/storeSwitcher.js +119 -0
- package/src/components/CheckoutHeader/storeSwitcher.module.css +107 -0
- package/src/components/CheckoutHeader/switcherItem.js +47 -0
- package/src/components/CheckoutHeader/wishlistTrigger.js +23 -0
- package/src/components/FilterListContent/filterListPage.js +0 -25
- package/src/components/FilterTop/filterTop.js +1 -1
- package/src/components/HelpCenter/helpCenter.js +151 -0
- package/src/components/HelpCenter/helpcenter.module.css +225 -0
- package/src/components/HelpCenter/index.js +1 -0
- package/src/components/HelpCenter/questionDetail.js +89 -0
- package/src/components/ProductContent/productContent.js +1 -1
- package/src/components/RMAPage/components/productItem.css +15 -0
- package/src/components/RMAPage/components/productItem.module.css +15 -0
- package/src/components/RelatedProducts/index.js +1 -0
- package/src/components/RelatedProducts/relatedProducts.js +44 -0
- package/src/components/SellerMegaMenu/__tests__/MegaMenu.spec.js +91 -0
- package/src/components/SellerMegaMenu/__tests__/MegaMenuItem.spec.js +123 -0
- package/src/components/SellerMegaMenu/__tests__/Submenu.spec.js +61 -0
- package/src/components/SellerMegaMenu/__tests__/SubmenuColumn.spec.js +50 -0
- package/src/components/SellerMegaMenu/__tests__/__snapshots__/MegaMenu.spec.js.snap +114 -0
- package/src/components/SellerMegaMenu/__tests__/__snapshots__/MegaMenuItem.spec.js.snap +71 -0
- package/src/components/SellerMegaMenu/__tests__/__snapshots__/Submenu.spec.js.snap +59 -0
- package/src/components/SellerMegaMenu/__tests__/__snapshots__/SubmenuColumn.spec.js.snap +34 -0
- package/src/components/SellerMegaMenu/customSubmenuColumn.js +75 -0
- package/src/components/SellerMegaMenu/customSubmenuColumn.module.css +29 -0
- package/src/components/SellerMegaMenu/shopByColumn.js +121 -0
- package/src/components/SellerProducts/productContent.js +2 -4
- package/src/components/SetsData/setsData.js +0 -25
- package/src/components/ShopBy/shopBy.js +3 -78
- package/src/components/ShopBySets/shopBySets.js +2 -2
- package/src/components/ShowMore/showMore.js +3 -49
- package/src/overwrites/peregrine/lib/talons/OrderHistoryPage/orderHistoryPage.gql.js +1 -15
- package/src/overwrites/peregrine/lib/talons/OrderHistoryPage/orderRow.gql.js +0 -11
- package/src/overwrites/peregrine/lib/talons/RelatedProducts/productReview.gql.js +89 -0
- package/src/overwrites/peregrine/lib/talons/RelatedProducts/useRelatedProducts.js +833 -0
- package/src/overwrites/peregrine/lib/talons/RootComponents/Category/categoryContent.gql.js +0 -16
- package/src/overwrites/peregrine/lib/talons/RootComponents/Category/useCategoryContent.js +1 -5
- package/src/overwrites/venia-ui/lib/RootComponents/Category/category.js +118 -62
- package/src/overwrites/venia-ui/lib/RootComponents/Category/categoryContent.js +3 -50
- package/src/overwrites/venia-ui/lib/components/Breadcrumbs/breadcrumbs.js +1 -7
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/editModal.js +41 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/index.js +1 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productDetail.js +80 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productDetail.module.css +33 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productForm.js +153 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/EditModal/productForm.module.css +52 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/errorMessage.js +31 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/errorMessage.module.css +13 -0
- package/src/overwrites/venia-ui/lib/components/CartPage/ProductListingBySeller/quantity.js +40 -0
- package/src/overwrites/venia-ui/lib/components/FilterSidebar/filterSidebar.js +21 -92
- package/src/overwrites/venia-ui/lib/components/MegaMenu/megaMenuItem.js +6 -2
- package/src/overwrites/venia-ui/lib/components/MegaMenu/submenu.js +0 -21
- package/src/overwrites/venia-ui/lib/components/OrderHistoryPage/Reviews/reviewModal.js +8 -0
- package/src/overwrites/venia-ui/lib/components/OrderHistoryPage/orderHistoryPage.js +3 -3
- package/src/overwrites/venia-ui/lib/components/OrderHistoryPage/orderRow.js +4 -85
- package/src/talons/HelpCenter/helpCenter.gql.js +93 -0
- package/src/talons/HelpCenter/useHelpCenter.js +59 -0
- package/src/talons/ProductContent/productContent.gql.js +0 -16
- package/src/talons/ProductContent/useProductContent.js +0 -4
- package/src/talons/RelatedProducts/relatedProducts.gql.js +209 -0
- package/src/talons/RelatedProducts/useRelatedProducts.js +112 -0
- package/src/talons/SellerProducts/productContent.gql.js +1 -17
- package/src/talons/SellerProducts/useProductContent.js +1 -36
- package/src/components/AttributesBlock/attributesBlock.js +0 -54
- package/src/components/AttributesBlock/attributesBlock.module.css +0 -28
- package/src/components/ShopBy/shopBy copy.js +0 -172
- package/src/components/SubCategory/customSubCategory.js +0 -45
- package/src/components/SubCategory/customSubCategory.module.css +0 -22
- package/src/talons/AttributesBlock/attributesBlock.gql.js +0 -15
- package/src/talons/AttributesBlock/useAttributesBlock.js +0 -38
- package/src/talons/SubCategory/useCustomSubCategory.js +0 -50
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import { useCallback, useState, useMemo } from 'react';
|
|
2
|
+
import { useToasts } from '@magento/peregrine/lib';
|
|
3
|
+
import { useIntl } from 'react-intl';
|
|
4
|
+
import { useMutation, useQuery } from '@apollo/client';
|
|
5
|
+
import { useCartContext } from '@magento/peregrine/lib/context/cart';
|
|
6
|
+
import { useUserContext } from '@magento/peregrine/lib/context/user';
|
|
7
|
+
|
|
8
|
+
import { useHistory } from 'react-router-dom';
|
|
9
|
+
|
|
10
|
+
import { appendOptionsToPayload } from '@magento/peregrine/lib/util/appendOptionsToPayload';
|
|
11
|
+
import { findMatchingVariant } from '@magento/peregrine/lib/util/findMatchingProductVariant';
|
|
12
|
+
import { isProductConfigurable } from '@magento/peregrine/lib/util/isProductConfigurable';
|
|
13
|
+
import { isSupportedProductType as isSupported } from '@magento/peregrine/lib/util/isSupportedProductType';
|
|
14
|
+
import { deriveErrorMessage } from '@magento/peregrine/lib/util/deriveErrorMessage';
|
|
15
|
+
import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
|
|
16
|
+
import defaultOperations from '@magento/peregrine/lib/talons/ProductFullDetail/productFullDetail.gql';
|
|
17
|
+
import productReviewOperations from '@riosst100/pwa-marketplace/src/overwrites/peregrine/lib/talons/RelatedProducts/productReview.gql';
|
|
18
|
+
import { useEventingContext } from '@magento/peregrine/lib/context/eventing';
|
|
19
|
+
import { getOutOfStockVariants } from '@magento/peregrine/lib/util/getOutOfStockVariants';
|
|
20
|
+
|
|
21
|
+
const INITIAL_OPTION_CODES = new Map();
|
|
22
|
+
const INITIAL_OPTION_SELECTIONS = new Map();
|
|
23
|
+
const OUT_OF_STOCK_CODE = 'OUT_OF_STOCK';
|
|
24
|
+
const IN_STOCK_CODE = 'IN_STOCK';
|
|
25
|
+
|
|
26
|
+
const deriveOptionCodesFromProduct = product => {
|
|
27
|
+
// If this is a simple product it has no option codes.
|
|
28
|
+
if (!isProductConfigurable(product)) {
|
|
29
|
+
return INITIAL_OPTION_CODES;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Initialize optionCodes based on the options of the product.
|
|
33
|
+
const initialOptionCodes = new Map();
|
|
34
|
+
for (const {
|
|
35
|
+
attribute_id,
|
|
36
|
+
attribute_code
|
|
37
|
+
} of product.configurable_options) {
|
|
38
|
+
initialOptionCodes.set(attribute_id, attribute_code);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return initialOptionCodes;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Similar to deriving the initial codes for each option.
|
|
45
|
+
const deriveOptionSelectionsFromProduct = product => {
|
|
46
|
+
if (!isProductConfigurable(product)) {
|
|
47
|
+
return INITIAL_OPTION_SELECTIONS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const initialOptionSelections = new Map();
|
|
51
|
+
for (const { attribute_id } of product.configurable_options) {
|
|
52
|
+
initialOptionSelections.set(attribute_id, undefined);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return initialOptionSelections;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getIsMissingOptions = (product, optionSelections) => {
|
|
59
|
+
// Non-configurable products can't be missing options.
|
|
60
|
+
if (!isProductConfigurable(product)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Configurable products are missing options if we have fewer
|
|
65
|
+
// option selections than the product has options.
|
|
66
|
+
const { configurable_options } = product;
|
|
67
|
+
const numProductOptions = configurable_options.length;
|
|
68
|
+
const numProductSelections = Array.from(optionSelections.values()).filter(
|
|
69
|
+
value => !!value
|
|
70
|
+
).length;
|
|
71
|
+
|
|
72
|
+
return numProductSelections < numProductOptions;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getIsOutOfStock = (product, optionCodes, optionSelections) => {
|
|
76
|
+
const { stock_status, variants } = product;
|
|
77
|
+
const isConfigurable = isProductConfigurable(product);
|
|
78
|
+
const optionsSelected =
|
|
79
|
+
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
80
|
+
0;
|
|
81
|
+
|
|
82
|
+
if (isConfigurable && optionsSelected) {
|
|
83
|
+
const item = findMatchingVariant({
|
|
84
|
+
optionCodes,
|
|
85
|
+
optionSelections,
|
|
86
|
+
variants
|
|
87
|
+
});
|
|
88
|
+
const stockStatus = item?.product?.stock_status;
|
|
89
|
+
|
|
90
|
+
return stockStatus === OUT_OF_STOCK_CODE || !stockStatus;
|
|
91
|
+
}
|
|
92
|
+
return stock_status === OUT_OF_STOCK_CODE;
|
|
93
|
+
};
|
|
94
|
+
const getIsAllOutOfStock = product => {
|
|
95
|
+
const { stock_status, variants } = product;
|
|
96
|
+
const isConfigurable = isProductConfigurable(product);
|
|
97
|
+
|
|
98
|
+
if (isConfigurable) {
|
|
99
|
+
const inStockItem = variants.find(item => {
|
|
100
|
+
return item.product.stock_status === IN_STOCK_CODE;
|
|
101
|
+
});
|
|
102
|
+
return !inStockItem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return stock_status === OUT_OF_STOCK_CODE;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
|
|
109
|
+
let value = [];
|
|
110
|
+
|
|
111
|
+
const { media_gallery_entries, variants } = product;
|
|
112
|
+
const isConfigurable = isProductConfigurable(product);
|
|
113
|
+
|
|
114
|
+
let variantGallery = variants && variants.length ? variants.flatMap(variant => variant.product.media_gallery_entries || []) : [];
|
|
115
|
+
if (variantGallery) {
|
|
116
|
+
// variantGallery = variantGallery.sort((a, b) => a.uid - b.uid);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
value = isConfigurable && variants && variants.length > 0
|
|
120
|
+
? [
|
|
121
|
+
...media_gallery_entries,
|
|
122
|
+
...variantGallery
|
|
123
|
+
]
|
|
124
|
+
: media_gallery_entries;
|
|
125
|
+
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const getSelectedMedia = (product, optionCodes, optionSelections) => {
|
|
130
|
+
let valueSelected = [];
|
|
131
|
+
|
|
132
|
+
const { media_gallery_entries, variants } = product;
|
|
133
|
+
const isConfigurable = isProductConfigurable(product);
|
|
134
|
+
|
|
135
|
+
// Selections are initialized to "code => undefined". Once we select a value, like color, the selections change. This filters out unselected options.
|
|
136
|
+
const optionsSelected =
|
|
137
|
+
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
138
|
+
0;
|
|
139
|
+
|
|
140
|
+
if (isConfigurable && optionsSelected) {
|
|
141
|
+
// If any of the possible variants matches the selection add that
|
|
142
|
+
// variant's image to the media gallery. NOTE: This _can_, and does,
|
|
143
|
+
// include variants such as size. If Magento is configured to display
|
|
144
|
+
// an image for a size attribute, it will render that image.
|
|
145
|
+
const item = findMatchingVariant({
|
|
146
|
+
optionCodes,
|
|
147
|
+
optionSelections,
|
|
148
|
+
variants
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
valueSelected = item
|
|
152
|
+
? item.product.media_gallery_entries
|
|
153
|
+
: media_gallery_entries;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return valueSelected;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// We only want to display breadcrumbs for one category on a PDP even if a
|
|
160
|
+
// product has multiple related categories. This function filters and selects
|
|
161
|
+
// one category id for that purpose.
|
|
162
|
+
const getBreadcrumbCategoryId = categories => {
|
|
163
|
+
// Exit if there are no categories for this product.
|
|
164
|
+
if (!categories || !categories.length) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const breadcrumbSet = new Set();
|
|
168
|
+
categories.forEach(({ breadcrumbs }) => {
|
|
169
|
+
// breadcrumbs can be `null`...
|
|
170
|
+
(breadcrumbs || []).forEach(({ category_id }) =>
|
|
171
|
+
breadcrumbSet.add(category_id)
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Until we can get the single canonical breadcrumb path to a product we
|
|
176
|
+
// will just return the first category id of the potential leaf categories.
|
|
177
|
+
const leafCategory = categories.find(
|
|
178
|
+
category => !breadcrumbSet.has(category.uid)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// If we couldn't find a leaf category then just use the first category
|
|
182
|
+
// in the list for this product.
|
|
183
|
+
return leafCategory.uid || categories[0].uid;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getConfigPrice = (product, optionCodes, optionSelections) => {
|
|
187
|
+
let value;
|
|
188
|
+
|
|
189
|
+
const { variants } = product;
|
|
190
|
+
const isConfigurable = isProductConfigurable(product);
|
|
191
|
+
|
|
192
|
+
const optionsSelected =
|
|
193
|
+
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
194
|
+
0;
|
|
195
|
+
|
|
196
|
+
if (!isConfigurable) {
|
|
197
|
+
value = product.price_range?.maximum_price;
|
|
198
|
+
} else {
|
|
199
|
+
const item = findMatchingVariant({
|
|
200
|
+
optionCodes,
|
|
201
|
+
optionSelections,
|
|
202
|
+
variants
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const minPrice = product.price_range?.minimum_price.final_price.value;
|
|
206
|
+
const maxPrice = product.price_range?.maximum_price.final_price.value;
|
|
207
|
+
|
|
208
|
+
let val = minPrice;
|
|
209
|
+
if (minPrice != maxPrice) {
|
|
210
|
+
val = minPrice + ' - ' + maxPrice;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const priceRange = {
|
|
214
|
+
final_price: {
|
|
215
|
+
value: val,
|
|
216
|
+
currency: product.price_range?.minimum_price.final_price.currency
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
value = optionsSelected && item
|
|
221
|
+
? item.product.price_range?.maximum_price
|
|
222
|
+
: priceRange;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return value;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const getConfigName = (product, optionCodes, optionSelections) => {
|
|
229
|
+
let value;
|
|
230
|
+
|
|
231
|
+
const { variants } = product;
|
|
232
|
+
const isConfigurable = isProductConfigurable(product);
|
|
233
|
+
|
|
234
|
+
const optionsSelected =
|
|
235
|
+
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
236
|
+
0;
|
|
237
|
+
|
|
238
|
+
if (!isConfigurable) {
|
|
239
|
+
value = product.name;
|
|
240
|
+
} else {
|
|
241
|
+
const item = findMatchingVariant({
|
|
242
|
+
optionCodes,
|
|
243
|
+
optionSelections,
|
|
244
|
+
variants
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
value = optionsSelected && item
|
|
248
|
+
? item.product.name
|
|
249
|
+
: product.name;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return value;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const getConfigSku = (product, optionCodes, optionSelections) => {
|
|
256
|
+
let value;
|
|
257
|
+
|
|
258
|
+
const { variants } = product;
|
|
259
|
+
const isConfigurable = isProductConfigurable(product);
|
|
260
|
+
|
|
261
|
+
const optionsSelected =
|
|
262
|
+
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
263
|
+
0;
|
|
264
|
+
|
|
265
|
+
if (!isConfigurable) {
|
|
266
|
+
value = product.name;
|
|
267
|
+
} else {
|
|
268
|
+
const item = findMatchingVariant({
|
|
269
|
+
optionCodes,
|
|
270
|
+
optionSelections,
|
|
271
|
+
variants
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
value = optionsSelected && item
|
|
275
|
+
? item.product.sku
|
|
276
|
+
: product.sku;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return value;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const attributeLabelCompare = (attribute1, attribute2) => {
|
|
283
|
+
const label1 = attribute1['attribute_metadata']['label'].toLowerCase();
|
|
284
|
+
const label2 = attribute2['attribute_metadata']['label'].toLowerCase();
|
|
285
|
+
if (label1 < label2) return -1;
|
|
286
|
+
else if (label1 > label2) return 1;
|
|
287
|
+
else return 0;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const getCustomAttributes = (product, optionCodes, optionSelections) => {
|
|
291
|
+
const { custom_attributes, variants } = product;
|
|
292
|
+
const isConfigurable = isProductConfigurable(product);
|
|
293
|
+
const optionsSelected =
|
|
294
|
+
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
295
|
+
0;
|
|
296
|
+
|
|
297
|
+
if (isConfigurable && optionsSelected) {
|
|
298
|
+
const item = findMatchingVariant({
|
|
299
|
+
optionCodes,
|
|
300
|
+
optionSelections,
|
|
301
|
+
variants
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return item && item.product
|
|
305
|
+
? [...item.product.custom_attributes].sort(attributeLabelCompare)
|
|
306
|
+
: [];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return custom_attributes
|
|
310
|
+
? [...custom_attributes].sort(attributeLabelCompare)
|
|
311
|
+
: [];
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {GraphQLDocument} props.addConfigurableProductToCartMutation - configurable product mutation
|
|
316
|
+
* @param {GraphQLDocument} props.addSimpleProductToCartMutation - configurable product mutation
|
|
317
|
+
* @param {Object.<string, GraphQLDocument>} props.operations - collection of operation overrides merged into defaults
|
|
318
|
+
* @param {Object} props.product - the product, see RootComponents/Product
|
|
319
|
+
*
|
|
320
|
+
* @returns {{
|
|
321
|
+
* breadcrumbCategoryId: string|undefined,
|
|
322
|
+
* errorMessage: string|undefined,
|
|
323
|
+
* handleAddToCart: func,
|
|
324
|
+
* handleSelectionChange: func,
|
|
325
|
+
* handleSetQuantity: func,
|
|
326
|
+
* isAddToCartDisabled: boolean,
|
|
327
|
+
* isSupportedProductType: boolean,
|
|
328
|
+
* mediaGalleryEntries: array,
|
|
329
|
+
* productDetails: object,
|
|
330
|
+
* quantity: number
|
|
331
|
+
* }}
|
|
332
|
+
*/
|
|
333
|
+
export const useRelatedProducts = props => {
|
|
334
|
+
const {
|
|
335
|
+
addConfigurableProductToCartMutation,
|
|
336
|
+
addSimpleProductToCartMutation,
|
|
337
|
+
product,
|
|
338
|
+
isPreview
|
|
339
|
+
} = props;
|
|
340
|
+
|
|
341
|
+
const history = useHistory();
|
|
342
|
+
|
|
343
|
+
const handleToastAction = useCallback(
|
|
344
|
+
(removeToast) => {
|
|
345
|
+
history.push('/cart');
|
|
346
|
+
removeToast();
|
|
347
|
+
},
|
|
348
|
+
[history]
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const [, { addToast }] = useToasts();
|
|
352
|
+
|
|
353
|
+
const { data: productReviewData, loading: loadingProductReview, error: errorProductReview, refetch: refetchProductReviews } = useQuery(
|
|
354
|
+
productReviewOperations.getProductReviews,
|
|
355
|
+
{
|
|
356
|
+
variables: { url_key: product?.url_key },
|
|
357
|
+
skip: !product?.url_key,
|
|
358
|
+
fetchPolicy: 'network-only'
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Query for ratings metadata
|
|
363
|
+
const { data: ratingsMetadataData, loading: loadingRatingsMetadata } = useQuery(
|
|
364
|
+
productReviewOperations.getProductReviewRatingsMetadata,
|
|
365
|
+
{ fetchPolicy: 'network-only' }
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Mutation for creating review (allow partial data with errors)
|
|
369
|
+
const [createProductReview, { loading: loadingCreateReview, error: errorCreateReview }] = useMutation(
|
|
370
|
+
productReviewOperations.createProductReview,
|
|
371
|
+
{ errorPolicy: 'all' }
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Handler for submitting review with robust toast handling
|
|
375
|
+
const handleSubmitReview = async (formValues) => {
|
|
376
|
+
try {
|
|
377
|
+
const result = await createProductReview({
|
|
378
|
+
variables: { input: formValues },
|
|
379
|
+
errorPolicy: 'all'
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const gqlErrors = result?.errors || [];
|
|
383
|
+
const review = result?.data?.createProductReview?.review;
|
|
384
|
+
|
|
385
|
+
if (gqlErrors.length > 0 || !review) {
|
|
386
|
+
const message = gqlErrors[0]?.message || 'Failed to submit review!';
|
|
387
|
+
addToast({ type: 'error', message });
|
|
388
|
+
return { success: false, error: gqlErrors };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await refetchProductReviews();
|
|
392
|
+
addToast({ type: 'success', message: 'Review submitted successfully!' });
|
|
393
|
+
return { success: true };
|
|
394
|
+
} catch (e) {
|
|
395
|
+
addToast({ type: 'error', message: e?.message || 'Failed to submit review!' });
|
|
396
|
+
return { success: false, error: e };
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const [, { dispatch }] = useEventingContext();
|
|
401
|
+
|
|
402
|
+
const hasDeprecatedOperationProp = !!(
|
|
403
|
+
addConfigurableProductToCartMutation || addSimpleProductToCartMutation
|
|
404
|
+
);
|
|
405
|
+
const operations = mergeOperations(defaultOperations, props.operations);
|
|
406
|
+
|
|
407
|
+
const productType = product.__typename;
|
|
408
|
+
|
|
409
|
+
const isSupportedProductType = isSupported(productType);
|
|
410
|
+
|
|
411
|
+
const [{ cartId }] = useCartContext();
|
|
412
|
+
const [{ isSignedIn, currentUser }] = useUserContext();
|
|
413
|
+
const { formatMessage } = useIntl();
|
|
414
|
+
|
|
415
|
+
const { data: storeConfigData } = useQuery(
|
|
416
|
+
operations.getWishlistConfigQuery,
|
|
417
|
+
{
|
|
418
|
+
fetchPolicy: 'cache-and-network'
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const [
|
|
423
|
+
addConfigurableProductToCart,
|
|
424
|
+
{
|
|
425
|
+
error: errorAddingConfigurableProduct,
|
|
426
|
+
loading: isAddConfigurableLoading
|
|
427
|
+
}
|
|
428
|
+
] = useMutation(
|
|
429
|
+
addConfigurableProductToCartMutation ||
|
|
430
|
+
operations.addConfigurableProductToCartMutation
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const [
|
|
434
|
+
addSimpleProductToCart,
|
|
435
|
+
{ error: errorAddingSimpleProduct, loading: isAddSimpleLoading }
|
|
436
|
+
] = useMutation(
|
|
437
|
+
addSimpleProductToCartMutation ||
|
|
438
|
+
operations.addSimpleProductToCartMutation
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const [
|
|
442
|
+
addProductToCart,
|
|
443
|
+
{
|
|
444
|
+
data: addToCartResponseData,
|
|
445
|
+
error: errorAddingProductToCart,
|
|
446
|
+
loading: isAddProductLoading
|
|
447
|
+
}
|
|
448
|
+
] = useMutation(operations.addProductToCartMutation);
|
|
449
|
+
|
|
450
|
+
const breadcrumbCategoryId = useMemo(
|
|
451
|
+
() => getBreadcrumbCategoryId(product.categories),
|
|
452
|
+
[product.categories]
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const derivedOptionSelections = useMemo(
|
|
456
|
+
() => deriveOptionSelectionsFromProduct(product),
|
|
457
|
+
[product]
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const [optionSelections, setOptionSelections] = useState(
|
|
461
|
+
derivedOptionSelections
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const [singleOptionSelection, setSingleOptionSelection] = useState();
|
|
465
|
+
|
|
466
|
+
const derivedOptionCodes = useMemo(
|
|
467
|
+
() => deriveOptionCodesFromProduct(product),
|
|
468
|
+
[product]
|
|
469
|
+
);
|
|
470
|
+
const [optionCodes] = useState(derivedOptionCodes);
|
|
471
|
+
|
|
472
|
+
const isMissingOptions = useMemo(
|
|
473
|
+
() => getIsMissingOptions(product, optionSelections),
|
|
474
|
+
[product, optionSelections]
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const isOutOfStock = useMemo(
|
|
478
|
+
() => getIsOutOfStock(product, optionCodes, optionSelections),
|
|
479
|
+
[product, optionCodes, optionSelections]
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// Check if display out of stock products option is selected in the Admin Dashboard
|
|
483
|
+
const isOutOfStockProductDisplayed = useMemo(() => {
|
|
484
|
+
let totalVariants = 1;
|
|
485
|
+
const isConfigurable = isProductConfigurable(product);
|
|
486
|
+
if (product.configurable_options && isConfigurable) {
|
|
487
|
+
for (const option of product.configurable_options) {
|
|
488
|
+
const length = option.values.length;
|
|
489
|
+
totalVariants = totalVariants * length;
|
|
490
|
+
}
|
|
491
|
+
return product.variants.length === totalVariants;
|
|
492
|
+
}
|
|
493
|
+
}, [product]);
|
|
494
|
+
|
|
495
|
+
const isEverythingOutOfStock = useMemo(() => getIsAllOutOfStock(product), [
|
|
496
|
+
product
|
|
497
|
+
]);
|
|
498
|
+
|
|
499
|
+
const outOfStockVariants = useMemo(
|
|
500
|
+
() =>
|
|
501
|
+
getOutOfStockVariants(
|
|
502
|
+
product,
|
|
503
|
+
optionCodes,
|
|
504
|
+
singleOptionSelection,
|
|
505
|
+
optionSelections,
|
|
506
|
+
isOutOfStockProductDisplayed
|
|
507
|
+
),
|
|
508
|
+
[
|
|
509
|
+
product,
|
|
510
|
+
optionCodes,
|
|
511
|
+
singleOptionSelection,
|
|
512
|
+
optionSelections,
|
|
513
|
+
isOutOfStockProductDisplayed
|
|
514
|
+
]
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const mediaGalleryEntries = useMemo(
|
|
518
|
+
() => getMediaGalleryEntries(product, optionCodes, optionSelections),
|
|
519
|
+
[product, optionCodes, optionSelections]
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const selectedMedia = useMemo(
|
|
523
|
+
() => getSelectedMedia(product, optionCodes, optionSelections),
|
|
524
|
+
[product, optionCodes, optionSelections]
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const customAttributes = useMemo(
|
|
528
|
+
() => getCustomAttributes(product, optionCodes, optionSelections),
|
|
529
|
+
[product, optionCodes, optionSelections]
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const productPrice = useMemo(
|
|
533
|
+
() => getConfigPrice(product, optionCodes, optionSelections),
|
|
534
|
+
[product, optionCodes, optionSelections]
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const productName = useMemo(
|
|
538
|
+
() => getConfigName(product, optionCodes, optionSelections),
|
|
539
|
+
[product, optionCodes, optionSelections]
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const productSku = useMemo(
|
|
543
|
+
() => getConfigSku(product, optionCodes, optionSelections),
|
|
544
|
+
[product, optionCodes, optionSelections]
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// The map of ids to values (and their uids)
|
|
548
|
+
// For example:
|
|
549
|
+
// { "179" => [{ uid: "abc", value_index: 1 }, { uid: "def", value_index: 2 }]}
|
|
550
|
+
const attributeIdToValuesMap = useMemo(() => {
|
|
551
|
+
const map = new Map();
|
|
552
|
+
// For simple items, this will be an empty map.
|
|
553
|
+
const options = product.configurable_options || [];
|
|
554
|
+
for (const { attribute_id, values } of options) {
|
|
555
|
+
map.set(attribute_id, values);
|
|
556
|
+
}
|
|
557
|
+
return map;
|
|
558
|
+
}, [product.configurable_options]);
|
|
559
|
+
|
|
560
|
+
// An array of selected option uids. Useful for passing to mutations.
|
|
561
|
+
// For example:
|
|
562
|
+
// ["abc", "def"]
|
|
563
|
+
const selectedOptionsArray = useMemo(() => {
|
|
564
|
+
const selectedOptions = [];
|
|
565
|
+
|
|
566
|
+
optionSelections.forEach((value, key) => {
|
|
567
|
+
const values = attributeIdToValuesMap.get(key);
|
|
568
|
+
|
|
569
|
+
const selectedValue = values?.find(
|
|
570
|
+
item => item.value_index === value
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
if (selectedValue) {
|
|
574
|
+
selectedOptions.push(selectedValue.uid);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
return selectedOptions;
|
|
578
|
+
}, [attributeIdToValuesMap, optionSelections]);
|
|
579
|
+
|
|
580
|
+
const handleAddToCart = useCallback(
|
|
581
|
+
async formValues => {
|
|
582
|
+
const { quantity, preorder } = formValues;
|
|
583
|
+
|
|
584
|
+
/*
|
|
585
|
+
@deprecated in favor of general addProductsToCart mutation. Will support until the next MAJOR.
|
|
586
|
+
*/
|
|
587
|
+
if (hasDeprecatedOperationProp) {
|
|
588
|
+
const payload = {
|
|
589
|
+
item: product,
|
|
590
|
+
productType,
|
|
591
|
+
quantity
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
if (isProductConfigurable(product)) {
|
|
595
|
+
appendOptionsToPayload(
|
|
596
|
+
payload,
|
|
597
|
+
optionSelections,
|
|
598
|
+
optionCodes
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (isSupportedProductType) {
|
|
603
|
+
const variables = {
|
|
604
|
+
cartId,
|
|
605
|
+
parentSku: payload.parentSku,
|
|
606
|
+
product: payload.item,
|
|
607
|
+
quantity: payload.quantity,
|
|
608
|
+
sku: payload.item.sku
|
|
609
|
+
};
|
|
610
|
+
// Use the proper mutation for the type.
|
|
611
|
+
if (productType === 'SimpleProduct') {
|
|
612
|
+
try {
|
|
613
|
+
await addSimpleProductToCart({
|
|
614
|
+
variables
|
|
615
|
+
});
|
|
616
|
+
} catch {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
} else if (productType === 'ConfigurableProduct') {
|
|
620
|
+
try {
|
|
621
|
+
await addConfigurableProductToCart({
|
|
622
|
+
variables
|
|
623
|
+
});
|
|
624
|
+
} catch {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
console.error(
|
|
630
|
+
'Unsupported product type. Cannot add to cart.'
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
const variables = {
|
|
635
|
+
cartId,
|
|
636
|
+
product: {
|
|
637
|
+
sku: product.sku,
|
|
638
|
+
quantity
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
if (preorder && Object.keys(preorder).length) {
|
|
643
|
+
variables.product.preorder = preorder;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (selectedOptionsArray.length) {
|
|
647
|
+
variables.product.selected_options = selectedOptionsArray;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
await addProductToCart({ variables });
|
|
652
|
+
|
|
653
|
+
const selectedOptionsLabels =
|
|
654
|
+
selectedOptionsArray?.map((uid, i) => ({
|
|
655
|
+
attribute: product.configurable_options[i].label,
|
|
656
|
+
value:
|
|
657
|
+
product.configurable_options[i].values.findLast(
|
|
658
|
+
x => x.uid === uid
|
|
659
|
+
)?.label || null
|
|
660
|
+
})) || null;
|
|
661
|
+
|
|
662
|
+
dispatch({
|
|
663
|
+
type: 'CART_ADD_ITEM',
|
|
664
|
+
payload: {
|
|
665
|
+
cartId,
|
|
666
|
+
sku: product.sku,
|
|
667
|
+
name: product.name,
|
|
668
|
+
pricing: product.price,
|
|
669
|
+
priceTotal: productPrice.final_price.value,
|
|
670
|
+
currencyCode: productPrice.final_price.currency,
|
|
671
|
+
discountAmount: productPrice.discount.amount_off,
|
|
672
|
+
selectedOptions: selectedOptionsLabels,
|
|
673
|
+
quantity
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
} catch {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
[
|
|
682
|
+
addConfigurableProductToCart,
|
|
683
|
+
addProductToCart,
|
|
684
|
+
addSimpleProductToCart,
|
|
685
|
+
cartId,
|
|
686
|
+
dispatch,
|
|
687
|
+
hasDeprecatedOperationProp,
|
|
688
|
+
isSupportedProductType,
|
|
689
|
+
optionCodes,
|
|
690
|
+
optionSelections,
|
|
691
|
+
product,
|
|
692
|
+
productPrice,
|
|
693
|
+
productType,
|
|
694
|
+
selectedOptionsArray
|
|
695
|
+
]
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const handleSelectionChange = useCallback(
|
|
699
|
+
(optionId, selection) => {
|
|
700
|
+
// We must create a new Map here so that React knows that the value
|
|
701
|
+
// of optionSelections has changed.
|
|
702
|
+
const nextOptionSelections = new Map([...optionSelections]);
|
|
703
|
+
nextOptionSelections.set(optionId, selection);
|
|
704
|
+
setOptionSelections(nextOptionSelections);
|
|
705
|
+
// Create a new Map to keep track of single selections with key as String
|
|
706
|
+
const nextSingleOptionSelection = new Map();
|
|
707
|
+
nextSingleOptionSelection.set(optionId, selection);
|
|
708
|
+
setSingleOptionSelection(nextSingleOptionSelection);
|
|
709
|
+
},
|
|
710
|
+
[optionSelections]
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
// Normalization object for product details we need for rendering.
|
|
714
|
+
const productDetails = {
|
|
715
|
+
description: product.description,
|
|
716
|
+
shortDescription: product.short_description,
|
|
717
|
+
name: productName,
|
|
718
|
+
price: productPrice?.final_price,
|
|
719
|
+
price_range: product?.price_range,
|
|
720
|
+
sku: productSku,
|
|
721
|
+
publish_status: product.publish_status,
|
|
722
|
+
custom_table_metadata: product.custom_table_metadata,
|
|
723
|
+
term_and_conditions: product.term_and_conditions,
|
|
724
|
+
link_to_other_stores: product.link_to_other_stores,
|
|
725
|
+
shipping_policy: product.shipping_policy,
|
|
726
|
+
return_policy: product.return_policy,
|
|
727
|
+
preorder: product.preorder,
|
|
728
|
+
auction_data: product.auction_data
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const sellerDetails = {
|
|
732
|
+
name: product.seller?.name,
|
|
733
|
+
url_key: product.seller?.url_key,
|
|
734
|
+
city: product.seller?.city,
|
|
735
|
+
country: product.seller?.country
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const derivedErrorMessage = useMemo(
|
|
739
|
+
() =>
|
|
740
|
+
deriveErrorMessage([
|
|
741
|
+
errorAddingSimpleProduct,
|
|
742
|
+
errorAddingConfigurableProduct,
|
|
743
|
+
errorAddingProductToCart,
|
|
744
|
+
...(addToCartResponseData?.addProductsToCart?.user_errors || [])
|
|
745
|
+
]),
|
|
746
|
+
[
|
|
747
|
+
errorAddingConfigurableProduct,
|
|
748
|
+
errorAddingProductToCart,
|
|
749
|
+
errorAddingSimpleProduct,
|
|
750
|
+
addToCartResponseData
|
|
751
|
+
]
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
const wishlistItemOptions = useMemo(() => {
|
|
755
|
+
const options = {
|
|
756
|
+
quantity: 1,
|
|
757
|
+
sku: product.sku
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
if (productType === 'ConfigurableProduct') {
|
|
761
|
+
options.selected_options = selectedOptionsArray;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return options;
|
|
765
|
+
}, [product, productType, selectedOptionsArray]);
|
|
766
|
+
|
|
767
|
+
const wishlistButtonProps = {
|
|
768
|
+
buttonText: isSelected =>
|
|
769
|
+
isSelected
|
|
770
|
+
? formatMessage({
|
|
771
|
+
id: 'wishlistButton.addedText',
|
|
772
|
+
defaultMessage: 'Added to Favorites'
|
|
773
|
+
})
|
|
774
|
+
: formatMessage({
|
|
775
|
+
id: 'wishlistButton.addText',
|
|
776
|
+
defaultMessage: 'Add to Favorites'
|
|
777
|
+
}),
|
|
778
|
+
item: wishlistItemOptions,
|
|
779
|
+
storeConfig: storeConfigData ? storeConfigData.storeConfig : {}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const defaultNickname = useMemo(() => {
|
|
783
|
+
if (!currentUser) return '';
|
|
784
|
+
const first = currentUser.firstname || '';
|
|
785
|
+
const last = currentUser.lastname || '';
|
|
786
|
+
const full = `${first} ${last}`.trim();
|
|
787
|
+
if (full) return full;
|
|
788
|
+
const email = currentUser.email || '';
|
|
789
|
+
return email ? email.split('@')[0] : '';
|
|
790
|
+
}, [currentUser]);
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
addToCartResponseData,
|
|
794
|
+
errorAddingProductToCart,
|
|
795
|
+
isAddProductLoading,
|
|
796
|
+
breadcrumbCategoryId,
|
|
797
|
+
errorMessage: derivedErrorMessage,
|
|
798
|
+
handleAddToCart,
|
|
799
|
+
handleSelectionChange,
|
|
800
|
+
isOutOfStock,
|
|
801
|
+
isEverythingOutOfStock,
|
|
802
|
+
outOfStockVariants,
|
|
803
|
+
isAddToCartDisabled:
|
|
804
|
+
isPreview || isOutOfStock ||
|
|
805
|
+
isEverythingOutOfStock ||
|
|
806
|
+
isMissingOptions ||
|
|
807
|
+
isAddConfigurableLoading ||
|
|
808
|
+
isAddSimpleLoading ||
|
|
809
|
+
isAddProductLoading,
|
|
810
|
+
isSupportedProductType,
|
|
811
|
+
mediaGalleryEntries,
|
|
812
|
+
selectedMedia,
|
|
813
|
+
shouldShowWishlistButton:
|
|
814
|
+
isSignedIn &&
|
|
815
|
+
storeConfigData &&
|
|
816
|
+
!!storeConfigData.storeConfig.magento_wishlist_general_is_enabled,
|
|
817
|
+
productDetails,
|
|
818
|
+
customAttributes,
|
|
819
|
+
wishlistButtonProps,
|
|
820
|
+
wishlistItemOptions,
|
|
821
|
+
sellerDetails,
|
|
822
|
+
productReviewData,
|
|
823
|
+
loadingProductReview,
|
|
824
|
+
errorProductReview,
|
|
825
|
+
ratingsMetadataData,
|
|
826
|
+
loadingRatingsMetadata,
|
|
827
|
+
handleSubmitReview,
|
|
828
|
+
loadingCreateReview,
|
|
829
|
+
errorCreateReview,
|
|
830
|
+
defaultNickname,
|
|
831
|
+
handleToastAction
|
|
832
|
+
};
|
|
833
|
+
};
|