@riosst100/pwa-marketplace 2.1.4 → 2.1.6
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/componentOverrideMapping.js +7 -0
- package/src/components/FavoriteSeller/AddToListButton/addToListButton.js +54 -54
- package/src/components/FavoriteSeller/AddToListButton/addToListButton.module.css +17 -17
- package/src/components/FavoriteSeller/AddToListButton/index.js +1 -1
- package/src/components/FavoriteSeller/AddToListButton/useCommonToasts.js +33 -33
- package/src/components/FilterTop/CustomFilters/customFilters.js +130 -132
- package/src/components/FilterTop/filterTop.js +1 -8
- package/src/components/LinkToOtherStores/index.js +61 -0
- package/src/components/NonSportCardsSets/nonSportCardsSets.js +0 -2
- package/src/components/RFQ/index.js +6 -3
- package/src/components/SellerDetail/sellerDetail.js +18 -1
- package/src/components/SellerInformation/sellerInformation.js +5 -3
- package/src/components/SellerSocialMedia/index.js +96 -0
- package/src/overwrites/pagebuilder/lib/ContentTypes/Products/products.js +13 -0
- package/src/overwrites/peregrine/lib/talons/MagentoRoute/magentoRoute.gql.js +27 -0
- package/src/overwrites/peregrine/lib/talons/MagentoRoute/useMagentoRoute.js +193 -0
- package/src/overwrites/peregrine/lib/talons/ProductFullDetail/useProductFullDetail.js +48 -11
- package/src/overwrites/peregrine/lib/talons/ProductImageCarousel/useProductImageCarousel.js +77 -0
- package/src/overwrites/peregrine/lib/talons/ProductOptions/useOption.js +59 -0
- package/src/overwrites/peregrine/lib/talons/ProductOptions/useTile.js +47 -0
- package/src/overwrites/peregrine/lib/talons/RootComponents/Category/categoryFragments.gql.js +13 -0
- package/src/overwrites/peregrine/lib/talons/RootComponents/Product/product.gql.js +5 -2
- package/src/overwrites/peregrine/lib/talons/RootComponents/Product/productDetailFragment.gql.js +23 -0
- package/src/overwrites/peregrine/lib/talons/RootComponents/Product/useProduct.js +121 -0
- package/src/overwrites/venia-ui/lib/RootComponents/Category/categoryContent.js +0 -6
- package/src/overwrites/venia-ui/lib/components/AccountInformationPage/accountInformationPage.js +0 -1
- package/src/overwrites/venia-ui/lib/components/AccountInformationPage/editForm.js +0 -1
- package/src/overwrites/venia-ui/lib/components/Breadcrumbs/breadcrumbs.js +0 -3
- package/src/overwrites/venia-ui/lib/components/CheckoutPage/OrderSummary/orderSummary.js +0 -1
- package/src/overwrites/venia-ui/lib/components/Gallery/item.js +17 -3
- package/src/overwrites/venia-ui/lib/components/Price/price.js +113 -0
- package/src/overwrites/venia-ui/lib/components/ProductFullDetail/components/auctionDetail.js +1 -1
- package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +107 -105
- package/src/overwrites/venia-ui/lib/components/ProductImageCarousel/carousel.js +3 -1
- package/src/overwrites/venia-ui/lib/components/ProductOptions/option.js +112 -0
- package/src/overwrites/venia-ui/lib/components/ProductOptions/option.module.css +30 -0
- package/src/overwrites/venia-ui/lib/components/ProductOptions/options.js +49 -0
- package/src/overwrites/venia-ui/lib/components/ProductOptions/tile.js +118 -0
- package/src/overwrites/venia-ui/lib/components/ProductOptions/tile.module.css +68 -0
- package/src/overwrites/venia-ui/lib/components/ProductOptions/tileList.js +78 -0
- package/src/overwrites/venia-ui/lib/components/ProductOptions/tileList.module.css +6 -0
- package/src/overwrites/venia-ui/lib/components/ProductOptions/tileList.shimmer.js +32 -0
- package/src/talons/CustomFilters/useCustomFilters.js +0 -2
- package/src/talons/FavoriteSeller/AddToListButton/addToListButton.gql.js +30 -30
- package/src/talons/FavoriteSeller/AddToListButton/useAddToFavoriteListButton.js +0 -1
- package/src/talons/LegoSets/useLegoSets.js +0 -5
- package/src/talons/TrainsSets/useTrainsSets.js +0 -3
|
@@ -76,6 +76,23 @@ const SellerDetail = props => {
|
|
|
76
76
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const getSellerAddressDisplay = (seller) => {
|
|
80
|
+
let city = seller?.city;
|
|
81
|
+
let country = seller?.country;
|
|
82
|
+
|
|
83
|
+
let result = '';
|
|
84
|
+
if (city && country) {
|
|
85
|
+
result = `${city}, ${country}`;
|
|
86
|
+
}
|
|
87
|
+
if (city && !country) {
|
|
88
|
+
result = `${city}`;
|
|
89
|
+
}
|
|
90
|
+
if (!city && country) {
|
|
91
|
+
result = `${country}`;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
return (
|
|
80
97
|
<div className=' py-8'>
|
|
81
98
|
<Slider seller={seller} rootClassname='mb-[30px]' />
|
|
@@ -106,7 +123,7 @@ const SellerDetail = props => {
|
|
|
106
123
|
</div>
|
|
107
124
|
</div>
|
|
108
125
|
<div class="relative w-fit font-normal text-[#999999] text-[12px] tracking-[0] leading-[14px] whitespace-nowrap">
|
|
109
|
-
{seller ? seller
|
|
126
|
+
{seller ? getSellerAddressDisplay(seller) : ''}
|
|
110
127
|
</div>
|
|
111
128
|
</div>
|
|
112
129
|
<div className='flex flex-wrap items-start gap-4 relative'>
|
|
@@ -3,6 +3,7 @@ import SellerLocation from '../SellerLocation';
|
|
|
3
3
|
import { Location, ShopAdd } from 'iconsax-react';
|
|
4
4
|
import OperatingHours from '@riosst100/pwa-marketplace/src/components/OperatingHours';
|
|
5
5
|
import SellerAddressCard from '@riosst100/pwa-marketplace/src/components/Seller/sellerAddressCard';
|
|
6
|
+
import SellerSocialMedia from '@riosst100/pwa-marketplace/src/components/SellerSocialMedia';
|
|
6
7
|
|
|
7
8
|
const SellerInformation = ({ seller }) => {
|
|
8
9
|
return (
|
|
@@ -59,10 +60,11 @@ const SellerInformation = ({ seller }) => {
|
|
|
59
60
|
{seller ? seller.term_and_conditions : ''}
|
|
60
61
|
</div>
|
|
61
62
|
</div>
|
|
62
|
-
</div
|
|
63
|
-
</div
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<SellerSocialMedia seller={seller} />
|
|
64
66
|
{seller ? <SellerLocation storeLocators={seller.store_locators} /> : ''}
|
|
65
|
-
</div
|
|
67
|
+
</div>
|
|
66
68
|
</>
|
|
67
69
|
)
|
|
68
70
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const SellerSocialMedia = (props) => {
|
|
4
|
+
|
|
5
|
+
const { seller } = props;
|
|
6
|
+
|
|
7
|
+
const { twitter_id, facebook_id, youtube_id, instagram_id, linkedin_id } = seller;
|
|
8
|
+
|
|
9
|
+
const socialMediaLinks = [
|
|
10
|
+
{
|
|
11
|
+
code: 'X',
|
|
12
|
+
link: twitter_id
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
code: 'Facebook',
|
|
16
|
+
link: facebook_id
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: 'Youtube',
|
|
20
|
+
link: youtube_id
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: 'Instagram',
|
|
24
|
+
link: instagram_id
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: 'LinkedIn',
|
|
28
|
+
link: linkedin_id
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const getLogo = (code) => {
|
|
33
|
+
let img = '';
|
|
34
|
+
if (code == "X") {
|
|
35
|
+
img = 'https://upload.wikimedia.org/wikipedia/commons/5/53/X_logo_2023_original.svg';
|
|
36
|
+
}
|
|
37
|
+
if (code == "Facebook") {
|
|
38
|
+
img = 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/Facebook_f_logo_%282019%29.svg/1280px-Facebook_f_logo_%282019%29.svg.png';
|
|
39
|
+
}
|
|
40
|
+
if (code == "Youtube") {
|
|
41
|
+
img = 'https://cdn3.iconfinder.com/data/icons/social-network-30/512/social-06-512.png';
|
|
42
|
+
}
|
|
43
|
+
if (code == "Instagram") {
|
|
44
|
+
img = 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Instagram_logo_2022.svg/768px-Instagram_logo_2022.svg.png';
|
|
45
|
+
}
|
|
46
|
+
if (code == "LinkedIn") {
|
|
47
|
+
img = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/LinkedIn_icon.svg/1024px-LinkedIn_icon.svg.png';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return img;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let sellerSocialMediaResult = [];
|
|
54
|
+
|
|
55
|
+
if (socialMediaLinks && socialMediaLinks.length) {
|
|
56
|
+
socialMediaLinks.forEach((socialMedia) => {
|
|
57
|
+
if (socialMedia.code && socialMedia.link) {
|
|
58
|
+
const logo = getLogo(socialMedia.code);
|
|
59
|
+
sellerSocialMediaResult.push(
|
|
60
|
+
<a href={socialMedia.link} className='platform_logo-wrapper rounded-md px-5 py-2.5 border border-gray-100 flex items-center' target="_blank">
|
|
61
|
+
{logo ? <img
|
|
62
|
+
alt={socialMedia.code}
|
|
63
|
+
// width="auto"
|
|
64
|
+
// height={30}
|
|
65
|
+
src={logo}
|
|
66
|
+
style={{
|
|
67
|
+
"height":"20px"
|
|
68
|
+
}}
|
|
69
|
+
/> : socialMedia.code}
|
|
70
|
+
</a>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return sellerSocialMediaResult && sellerSocialMediaResult.length ? <>
|
|
77
|
+
<div class="flex items-start relative self-stretch w-full flex-[0_0_auto]">
|
|
78
|
+
<div class="flex flex-col items-start gap-[15px] relative flex-1 grow">
|
|
79
|
+
<div class="relative w-fit mt-[-1.00px] [font-family:'Frederik-DemiBold',Helvetica] font-bold text-[14px] tracking-[0] leading-[normal] whitespace-nowrap">Social Media</div>
|
|
80
|
+
<div class="inline-flex items-center justify-center gap-[10px] relative flex-[0_0_auto]">
|
|
81
|
+
<div class="flex flex-col items-start gap-[10px]" style={
|
|
82
|
+
{
|
|
83
|
+
"display": "flex",
|
|
84
|
+
"flex-direction": "row",
|
|
85
|
+
"align-items": "center"
|
|
86
|
+
}
|
|
87
|
+
}>
|
|
88
|
+
{sellerSocialMediaResult}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</> : '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default SellerSocialMedia;
|
|
@@ -335,6 +335,19 @@ export const GET_PRODUCTS_BY_URL_KEY = gql`
|
|
|
335
335
|
amount_off
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
+
minimum_price {
|
|
339
|
+
final_price {
|
|
340
|
+
currency
|
|
341
|
+
value
|
|
342
|
+
}
|
|
343
|
+
regular_price {
|
|
344
|
+
currency
|
|
345
|
+
value
|
|
346
|
+
}
|
|
347
|
+
discount {
|
|
348
|
+
amount_off
|
|
349
|
+
}
|
|
350
|
+
}
|
|
338
351
|
}
|
|
339
352
|
sku
|
|
340
353
|
small_image {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { gql } from '@apollo/client';
|
|
2
|
+
|
|
3
|
+
export const RESOLVE_URL = gql`
|
|
4
|
+
query ResolveURL($url: String!, $product_preview_token: String) {
|
|
5
|
+
route(url: $url, product_preview_token: $product_preview_token) {
|
|
6
|
+
relative_url
|
|
7
|
+
redirect_code
|
|
8
|
+
type
|
|
9
|
+
... on CmsPage {
|
|
10
|
+
identifier
|
|
11
|
+
}
|
|
12
|
+
# eslint-disable-next-line @graphql-eslint/require-id-when-available
|
|
13
|
+
... on ProductInterface {
|
|
14
|
+
uid
|
|
15
|
+
__typename
|
|
16
|
+
}
|
|
17
|
+
# eslint-disable-next-line @graphql-eslint/require-id-when-available
|
|
18
|
+
... on CategoryInterface {
|
|
19
|
+
uid
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
resolveUrlQuery: RESOLVE_URL
|
|
27
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useHistory, useLocation } from 'react-router-dom';
|
|
3
|
+
import { useLazyQuery } from '@apollo/client';
|
|
4
|
+
import { useRootComponents } from '@magento/peregrine/lib/context/rootComponents';
|
|
5
|
+
import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
|
|
6
|
+
import { getComponentData } from '@magento/peregrine/lib/util/magentoRouteData';
|
|
7
|
+
import { useAppContext } from '@magento/peregrine/lib/context/app';
|
|
8
|
+
|
|
9
|
+
import { getRootComponent, isRedirect } from '@magento/peregrine/lib/talons/MagentoRoute/helpers';
|
|
10
|
+
import DEFAULT_OPERATIONS from './magentoRoute.gql';
|
|
11
|
+
|
|
12
|
+
import { useParams } from 'react-router-dom';
|
|
13
|
+
|
|
14
|
+
const getInlinedPageData = () => {
|
|
15
|
+
return globalThis.INLINED_PAGE_TYPE && globalThis.INLINED_PAGE_TYPE.type
|
|
16
|
+
? globalThis.INLINED_PAGE_TYPE
|
|
17
|
+
: null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const resetInlinedPageData = () => {
|
|
21
|
+
globalThis.INLINED_PAGE_TYPE = false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const useMagentoRoute = (props = {}) => {
|
|
25
|
+
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
|
|
26
|
+
const { resolveUrlQuery } = operations;
|
|
27
|
+
const { replace } = useHistory();
|
|
28
|
+
const { pathname, search } = useLocation();
|
|
29
|
+
// const { preview } = useParams();
|
|
30
|
+
const [componentMap, setComponentMap] = useRootComponents();
|
|
31
|
+
|
|
32
|
+
const initialized = useRef(false);
|
|
33
|
+
const fetchedPathname = useRef(null);
|
|
34
|
+
|
|
35
|
+
const [appState, appApi] = useAppContext();
|
|
36
|
+
const { actions: appActions } = appApi;
|
|
37
|
+
const { nextRootComponent } = appState;
|
|
38
|
+
const { setNextRootComponent, setPageLoading } = appActions;
|
|
39
|
+
|
|
40
|
+
const setComponent = useCallback(
|
|
41
|
+
(key, value) => {
|
|
42
|
+
setComponentMap(prevMap => new Map(prevMap).set(key, value));
|
|
43
|
+
},
|
|
44
|
+
[setComponentMap]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const component = componentMap.get(pathname);
|
|
48
|
+
|
|
49
|
+
const [runQuery, queryResult] = useLazyQuery(resolveUrlQuery);
|
|
50
|
+
// destructure the query result
|
|
51
|
+
const { data, error, loading } = queryResult;
|
|
52
|
+
const { route } = data || {};
|
|
53
|
+
|
|
54
|
+
// redirect to external url
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (route) {
|
|
57
|
+
const external_URL = route.relative_url;
|
|
58
|
+
if (external_URL && external_URL.startsWith('http')) {
|
|
59
|
+
window.location.replace(external_URL);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, [route]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (initialized.current || !getInlinedPageData()) {
|
|
66
|
+
const params = new URLSearchParams(search);
|
|
67
|
+
const previewToken = params.get('preview');
|
|
68
|
+
|
|
69
|
+
runQuery({
|
|
70
|
+
fetchPolicy: 'cache-and-network',
|
|
71
|
+
nextFetchPolicy: 'cache-first',
|
|
72
|
+
variables: { url: pathname, product_preview_token: previewToken }
|
|
73
|
+
});
|
|
74
|
+
fetchedPathname.current = pathname;
|
|
75
|
+
}
|
|
76
|
+
}, [initialized, pathname, search]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (component) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
(async () => {
|
|
84
|
+
const { type, ...routeData } = route || {};
|
|
85
|
+
const { id, identifier, uid } = routeData || {};
|
|
86
|
+
const isEmpty = !id && !identifier && !uid;
|
|
87
|
+
|
|
88
|
+
if (!type || isEmpty) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const rootComponent = await getRootComponent(type);
|
|
94
|
+
setComponent(pathname, {
|
|
95
|
+
component: rootComponent,
|
|
96
|
+
...getComponentData(routeData),
|
|
97
|
+
type
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
101
|
+
console.error(error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setComponent(pathname, error);
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
}, [route]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
108
|
+
|
|
109
|
+
const { id, identifier, uid, redirect_code, relative_url, type } =
|
|
110
|
+
route || {};
|
|
111
|
+
|
|
112
|
+
// evaluate both results and determine the response type
|
|
113
|
+
const empty = !route || !type || (!id && !identifier && !uid);
|
|
114
|
+
const redirect = isRedirect(redirect_code);
|
|
115
|
+
const fetchError = component instanceof Error && component;
|
|
116
|
+
const routeError = fetchError || error;
|
|
117
|
+
const isInitialized = initialized.current || !getInlinedPageData();
|
|
118
|
+
|
|
119
|
+
let showPageLoader = false;
|
|
120
|
+
let routeData;
|
|
121
|
+
|
|
122
|
+
if (component && !fetchError) {
|
|
123
|
+
// FOUND
|
|
124
|
+
routeData = component;
|
|
125
|
+
} else if (routeError) {
|
|
126
|
+
// ERROR
|
|
127
|
+
routeData = { hasError: true, routeError };
|
|
128
|
+
} else if (empty && fetchedPathname.current === pathname && !loading) {
|
|
129
|
+
// NOT FOUND
|
|
130
|
+
routeData = { isNotFound: true };
|
|
131
|
+
} else if (nextRootComponent) {
|
|
132
|
+
// LOADING with full page shimmer
|
|
133
|
+
showPageLoader = true;
|
|
134
|
+
routeData = { isLoading: true, shimmer: nextRootComponent };
|
|
135
|
+
} else if (redirect) {
|
|
136
|
+
// REDIRECT
|
|
137
|
+
routeData = {
|
|
138
|
+
isRedirect: true,
|
|
139
|
+
relativeUrl: relative_url.startsWith('/')
|
|
140
|
+
? relative_url
|
|
141
|
+
: '/' + relative_url
|
|
142
|
+
};
|
|
143
|
+
} else {
|
|
144
|
+
// LOADING
|
|
145
|
+
const isInitialLoad = !isInitialized;
|
|
146
|
+
routeData = { isLoading: true, initial: isInitialLoad };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
(async () => {
|
|
151
|
+
const inlinedData = getInlinedPageData();
|
|
152
|
+
if (inlinedData) {
|
|
153
|
+
try {
|
|
154
|
+
const componentType = inlinedData.type;
|
|
155
|
+
const rootComponent = await getRootComponent(componentType);
|
|
156
|
+
setComponent(pathname, {
|
|
157
|
+
component: rootComponent,
|
|
158
|
+
type: componentType,
|
|
159
|
+
...getComponentData(inlinedData)
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
setComponent(pathname, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
initialized.current = true;
|
|
166
|
+
})();
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
// Unmount
|
|
170
|
+
resetInlinedPageData();
|
|
171
|
+
};
|
|
172
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
173
|
+
|
|
174
|
+
// perform a redirect if necesssary
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (routeData && routeData.isRedirect) {
|
|
177
|
+
replace(routeData.relativeUrl);
|
|
178
|
+
}
|
|
179
|
+
}, [pathname, replace, routeData]);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (component) {
|
|
183
|
+
// Reset loading shimmer whenever component resolves
|
|
184
|
+
setNextRootComponent(null);
|
|
185
|
+
}
|
|
186
|
+
}, [component, pathname, setNextRootComponent]);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
setPageLoading(showPageLoader);
|
|
190
|
+
}, [showPageLoader, setPageLoading]);
|
|
191
|
+
|
|
192
|
+
return routeData;
|
|
193
|
+
};
|
|
@@ -107,14 +107,28 @@ const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
|
|
|
107
107
|
const { media_gallery_entries, variants } = product;
|
|
108
108
|
const isConfigurable = isProductConfigurable(product);
|
|
109
109
|
|
|
110
|
+
value = isConfigurable && variants && variants.length > 0
|
|
111
|
+
? [
|
|
112
|
+
...variants.flatMap(variant => variant.product.media_gallery_entries || []),
|
|
113
|
+
...media_gallery_entries
|
|
114
|
+
]
|
|
115
|
+
: media_gallery_entries;
|
|
116
|
+
|
|
117
|
+
return value;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const getSelectedMedia = (product, optionCodes, optionSelections) => {
|
|
121
|
+
let valueSelected = [];
|
|
122
|
+
|
|
123
|
+
const { media_gallery_entries, variants } = product;
|
|
124
|
+
const isConfigurable = isProductConfigurable(product);
|
|
125
|
+
|
|
110
126
|
// Selections are initialized to "code => undefined". Once we select a value, like color, the selections change. This filters out unselected options.
|
|
111
127
|
const optionsSelected =
|
|
112
128
|
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
113
129
|
0;
|
|
114
130
|
|
|
115
|
-
if (
|
|
116
|
-
value = media_gallery_entries;
|
|
117
|
-
} else {
|
|
131
|
+
if (isConfigurable && optionsSelected) {
|
|
118
132
|
// If any of the possible variants matches the selection add that
|
|
119
133
|
// variant's image to the media gallery. NOTE: This _can_, and does,
|
|
120
134
|
// include variants such as size. If Magento is configured to display
|
|
@@ -125,12 +139,12 @@ const getMediaGalleryEntries = (product, optionCodes, optionSelections) => {
|
|
|
125
139
|
variants
|
|
126
140
|
});
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
?
|
|
142
|
+
valueSelected = item
|
|
143
|
+
? item.product.media_gallery_entries
|
|
130
144
|
: media_gallery_entries;
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
return
|
|
147
|
+
return valueSelected;
|
|
134
148
|
};
|
|
135
149
|
|
|
136
150
|
// We only want to display breadcrumbs for one category on a PDP even if a
|
|
@@ -170,7 +184,7 @@ const getConfigPrice = (product, optionCodes, optionSelections) => {
|
|
|
170
184
|
Array.from(optionSelections.values()).filter(value => !!value).length >
|
|
171
185
|
0;
|
|
172
186
|
|
|
173
|
-
if (!isConfigurable
|
|
187
|
+
if (!isConfigurable) {
|
|
174
188
|
value = product.price_range?.maximum_price;
|
|
175
189
|
} else {
|
|
176
190
|
const item = findMatchingVariant({
|
|
@@ -179,9 +193,24 @@ const getConfigPrice = (product, optionCodes, optionSelections) => {
|
|
|
179
193
|
variants
|
|
180
194
|
});
|
|
181
195
|
|
|
182
|
-
|
|
196
|
+
const minPrice = product.price_range?.minimum_price.final_price.value;
|
|
197
|
+
const maxPrice = product.price_range?.maximum_price.final_price.value;
|
|
198
|
+
|
|
199
|
+
let val = minPrice;
|
|
200
|
+
if (minPrice != maxPrice) {
|
|
201
|
+
val = minPrice + ' - ' + maxPrice;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const priceRange = {
|
|
205
|
+
final_price: {
|
|
206
|
+
value: val,
|
|
207
|
+
currency: product.price_range?.minimum_price.final_price.currency
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
value = optionsSelected && item
|
|
183
212
|
? item.product.price_range?.maximum_price
|
|
184
|
-
:
|
|
213
|
+
: priceRange;
|
|
185
214
|
}
|
|
186
215
|
|
|
187
216
|
return value;
|
|
@@ -242,7 +271,8 @@ export const useProductFullDetail = props => {
|
|
|
242
271
|
const {
|
|
243
272
|
addConfigurableProductToCartMutation,
|
|
244
273
|
addSimpleProductToCartMutation,
|
|
245
|
-
product
|
|
274
|
+
product,
|
|
275
|
+
isPreview
|
|
246
276
|
} = props;
|
|
247
277
|
|
|
248
278
|
const [, { dispatch }] = useEventingContext();
|
|
@@ -368,6 +398,11 @@ export const useProductFullDetail = props => {
|
|
|
368
398
|
[product, optionCodes, optionSelections]
|
|
369
399
|
);
|
|
370
400
|
|
|
401
|
+
const selectedMedia = useMemo(
|
|
402
|
+
() => getSelectedMedia(product, optionCodes, optionSelections),
|
|
403
|
+
[product, optionCodes, optionSelections]
|
|
404
|
+
);
|
|
405
|
+
|
|
371
406
|
const customAttributes = useMemo(
|
|
372
407
|
() => getCustomAttributes(product, optionCodes, optionSelections),
|
|
373
408
|
[product, optionCodes, optionSelections]
|
|
@@ -555,6 +590,7 @@ export const useProductFullDetail = props => {
|
|
|
555
590
|
price_range: product?.price_range,
|
|
556
591
|
sku: product.sku,
|
|
557
592
|
term_and_conditions: product.term_and_conditions,
|
|
593
|
+
link_to_other_stores: product.link_to_other_stores,
|
|
558
594
|
shipping_policy: product.shipping_policy,
|
|
559
595
|
return_policy: product.return_policy,
|
|
560
596
|
preorder: product.preorder,
|
|
@@ -621,7 +657,7 @@ export const useProductFullDetail = props => {
|
|
|
621
657
|
isEverythingOutOfStock,
|
|
622
658
|
outOfStockVariants,
|
|
623
659
|
isAddToCartDisabled:
|
|
624
|
-
isOutOfStock ||
|
|
660
|
+
isPreview || isOutOfStock ||
|
|
625
661
|
isEverythingOutOfStock ||
|
|
626
662
|
isMissingOptions ||
|
|
627
663
|
isAddConfigurableLoading ||
|
|
@@ -629,6 +665,7 @@ export const useProductFullDetail = props => {
|
|
|
629
665
|
isAddProductLoading,
|
|
630
666
|
isSupportedProductType,
|
|
631
667
|
mediaGalleryEntries,
|
|
668
|
+
selectedMedia,
|
|
632
669
|
shouldShowWishlistButton:
|
|
633
670
|
isSignedIn &&
|
|
634
671
|
storeConfigData &&
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import { useCarousel } from '@magento/peregrine';
|
|
3
|
+
import { generateUrlFromContainerWidth } from '@magento/peregrine/lib/util/imageUtils';
|
|
4
|
+
import {
|
|
5
|
+
MESSAGE_TYPES,
|
|
6
|
+
VALID_SERVICE_WORKER_ENVIRONMENT,
|
|
7
|
+
sendMessageToSW
|
|
8
|
+
} from '@magento/peregrine/lib/util/swUtils';
|
|
9
|
+
|
|
10
|
+
export const useProductImageCarousel = props => {
|
|
11
|
+
const { images, hoveredMedia, selectedMedia, type, imageWidth } = props;
|
|
12
|
+
const [carouselState, carouselApi] = useCarousel(images);
|
|
13
|
+
const { activeItemIndex, sortedImages } = carouselState;
|
|
14
|
+
const { handlePrevious, handleNext, setActiveItemIndex } = carouselApi;
|
|
15
|
+
|
|
16
|
+
const handleThumbnailClick = useCallback(
|
|
17
|
+
index => {
|
|
18
|
+
setActiveItemIndex(index);
|
|
19
|
+
},
|
|
20
|
+
[setActiveItemIndex]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Whenever the incoming images changes reset the active item to the first.
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (hoveredMedia || selectedMedia && selectedMedia.length) {
|
|
26
|
+
|
|
27
|
+
// Extract the uid from the first element of selectedMedia
|
|
28
|
+
const selectedMediaUid = hoveredMedia ? hoveredMedia[0].uid : selectedMedia[0].uid; // Get the uid from selectedMedia
|
|
29
|
+
|
|
30
|
+
// Find the index of the image with the matching uid
|
|
31
|
+
const selectedMediaIndex = sortedImages.findIndex(image => {
|
|
32
|
+
return image.uid === selectedMediaUid;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (selectedMediaIndex) {
|
|
36
|
+
setActiveItemIndex(selectedMediaIndex);
|
|
37
|
+
} else {
|
|
38
|
+
setActiveItemIndex(0);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
setActiveItemIndex(0);
|
|
42
|
+
}
|
|
43
|
+
}, [sortedImages, selectedMedia, hoveredMedia, setActiveItemIndex]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (VALID_SERVICE_WORKER_ENVIRONMENT) {
|
|
47
|
+
const urls = images.map(
|
|
48
|
+
({ file }) =>
|
|
49
|
+
new URL(
|
|
50
|
+
generateUrlFromContainerWidth(file, imageWidth, type),
|
|
51
|
+
location.origin
|
|
52
|
+
).href
|
|
53
|
+
);
|
|
54
|
+
sendMessageToSW(MESSAGE_TYPES.PREFETCH_IMAGES, {
|
|
55
|
+
urls
|
|
56
|
+
}).catch(err => {
|
|
57
|
+
console.error(
|
|
58
|
+
'Unable to send PREFETCH_IMAGES message to SW',
|
|
59
|
+
err
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}, [images, imageWidth, type]);
|
|
64
|
+
|
|
65
|
+
const currentImage = sortedImages[activeItemIndex] || {};
|
|
66
|
+
const altText = currentImage.label || 'image-product';
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
currentImage,
|
|
70
|
+
activeItemIndex,
|
|
71
|
+
altText,
|
|
72
|
+
handleNext,
|
|
73
|
+
handlePrevious,
|
|
74
|
+
handleThumbnailClick,
|
|
75
|
+
sortedImages
|
|
76
|
+
};
|
|
77
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Talon for Option.
|
|
5
|
+
*
|
|
6
|
+
* @param {number} props.attribute_id the id of the option
|
|
7
|
+
* @param {function} props.onSelectionChange callback handler for when the option is clicked
|
|
8
|
+
* @param {string} props.selectedValue the label of the selected option
|
|
9
|
+
* @param {array} props.values an array containing possible values
|
|
10
|
+
*/
|
|
11
|
+
export const useOption = props => {
|
|
12
|
+
const { attribute_id, attribute_code, onSelectionChange, selectedValue, values, variants } = props;
|
|
13
|
+
const [selection, setSelection] = useState(null);
|
|
14
|
+
const initialSelection = useMemo(() => {
|
|
15
|
+
let initialSelection = {};
|
|
16
|
+
const searchValue = selection || selectedValue;
|
|
17
|
+
if (searchValue) {
|
|
18
|
+
initialSelection =
|
|
19
|
+
values.find(value => value.default_label === searchValue) || {};
|
|
20
|
+
}
|
|
21
|
+
return initialSelection;
|
|
22
|
+
}, [selectedValue, selection, values]);
|
|
23
|
+
|
|
24
|
+
const filteredVariants = useMemo(() => {
|
|
25
|
+
let filteredVariants = {};
|
|
26
|
+
if (attribute_code) {
|
|
27
|
+
filteredVariants = variants.filter(variant => {
|
|
28
|
+
return variant.attributes.some(attribute => attribute.code === attribute_code);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return filteredVariants;
|
|
32
|
+
}, [attribute_code, variants]);
|
|
33
|
+
|
|
34
|
+
const valuesMap = useMemo(() => {
|
|
35
|
+
return new Map(
|
|
36
|
+
values.map(value => [value.value_index, value.store_label])
|
|
37
|
+
);
|
|
38
|
+
}, [values]);
|
|
39
|
+
|
|
40
|
+
const selectedValueDescription =
|
|
41
|
+
selection || initialSelection.default_label || 'None';
|
|
42
|
+
|
|
43
|
+
const handleSelectionChange = useCallback(
|
|
44
|
+
selection => {
|
|
45
|
+
setSelection(valuesMap.get(selection));
|
|
46
|
+
|
|
47
|
+
if (onSelectionChange) {
|
|
48
|
+
onSelectionChange(attribute_id, selection);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[attribute_id, onSelectionChange, valuesMap]
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
handleSelectionChange,
|
|
55
|
+
initialSelection,
|
|
56
|
+
selectedValueDescription,
|
|
57
|
+
filteredVariants
|
|
58
|
+
};
|
|
59
|
+
};
|