@evenicanpm/storefront-core 2.3.0 → 2.4.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/CHANGELOG.md +8 -0
- package/package.json +29 -9
- package/src/api-manager/datasources/d365/d365-address.datasource.ts +11 -0
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +23 -1
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +6 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +29 -14
- package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
- package/src/api-manager/index.ts +2 -1
- package/src/api-manager/schemas/product.schema.ts +1 -1
- package/src/auth/msal.ts +1 -4
- package/src/cms/blocks/block-manager.tsx +1 -1
- package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
- package/src/cms/blocks/components/footer/index.tsx +9 -71
- package/src/cms/blocks/components/footer/interfaces.ts +4 -0
- package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
- package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
- package/src/cms/endpoints.ts +7 -2
- package/src/components/BazaarMenu.tsx +1 -1
- package/src/components/Typography.tsx +4 -2
- package/src/components/_tests_/site-logo.test.tsx +1 -1
- package/src/components/carousel-cards/carousel-card-1/CarouselCard1.stories.tsx +1 -1
- package/src/components/categories/category-menu.tsx +1 -1
- package/src/components/countries-input.tsx +4 -0
- package/src/components/header/components/user.tsx +4 -5
- package/src/components/navbar/mobile-menu/mobile-menu.test.tsx +1 -1
- package/src/components/navbar/utils/transform-nav.test.ts +1 -1
- package/src/components/navbar/utils/transform-nav.ts +1 -1
- package/src/components/product-cards/product-card/product-card.tsx +5 -2
- package/src/components/product-cards/product-card/styles/index.ts +1 -5
- package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
- package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
- package/src/components/products-view/compound/products-grid-view.tsx +5 -1
- package/src/components/products-view/compound/products-list-view.tsx +5 -1
- package/src/components/site-logo.tsx +1 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
- package/src/global.d.ts +3 -0
- package/src/pages/account/account-navigation.tsx +2 -2
- package/src/pages/account/account-routes.ts +38 -4
- package/src/pages/account/addresses/address-form.tsx +20 -2
- package/src/pages/account/table-row-skeleton.tsx +31 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +3 -1
- package/src/pages/blog/blog-detail-view.tsx +3 -1
- package/src/pages/cart/cart-item.tsx +1 -7
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +14 -3
- package/src/pages/checkout/checkout-alt-form/steps/address/address-card.tsx +5 -3
- package/src/pages/checkout/checkout-alt-form/steps/address/new-address-form.tsx +1 -1
- package/src/pages/cms-page-view.tsx +1 -1
- package/src/pages/confirmation/ordered-products.tsx +3 -1
- package/src/pages/product-details/bopis/find-in-store-modal.tsx +4 -4
- package/src/pages/product-details/bopis/pickup-option-select.tsx +2 -2
- package/src/pages/product-details/bopis/search-header.tsx +2 -2
- package/src/pages/product-details/product-description.tsx +25 -13
- package/src/pages/product-details/product-intro/compound/context.ts +8 -3
- package/src/pages/product-details/product-intro/compound/product-info.tsx +25 -19
- package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
- package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
- package/src/pages/product-details/product-tabs.tsx +20 -4
- package/src/pages/product-list/product-list-view.tsx +2 -1
- package/src/providers/nav-provider/index.tsx +1 -1
- package/src/providers/nav-provider/utils/createLink.ts +1 -1
- package/tsconfig.json +1 -2
- package/__mocks__/countries.ts +0 -11
- package/__mocks__/create-mutation.ts +0 -68
- package/__mocks__/create-query.ts +0 -94
- package/__mocks__/data/categories.json +0 -795
- package/__mocks__/get-product-by-id.ts +0 -6
- package/__mocks__/next-auth-react.ts +0 -9
- package/__mocks__/next-font.js +0 -4
- package/__mocks__/next-headers.js +0 -13
- package/__mocks__/next-image.tsx +0 -18
- package/__mocks__/next-link.js +0 -19
- package/__mocks__/next-navigation.ts +0 -29
- package/__mocks__/product-dimensions.ts +0 -635
- package/__mocks__/products.ts +0 -533
- package/__mocks__/query-client.ts +0 -3
- package/__mocks__/wishlists.json +0 -408
- package/src/cms/blog.ts +0 -229
- package/src/cms/interfaces/navigation.ts +0 -52
- package/src/cms/interfaces/product-data.ts +0 -83
- package/src/cms/pages.ts +0 -149
|
@@ -7,7 +7,7 @@ import { PageSlug } from "@evenicanpm/storefront-core/src/lib/page-slugs";
|
|
|
7
7
|
import CheckoutStep from "@evenicanpm/storefront-core/src/pages/checkout/checkout-alt-form/checkout-step";
|
|
8
8
|
import { useNotification } from "@evenicanpm/storefront-core/src/providers/notifications/use-notification";
|
|
9
9
|
import type { CardPaymentAcceptResult } from "@msdyn365-commerce/retail-proxy";
|
|
10
|
-
import { Box, Button } from "@mui/material";
|
|
10
|
+
import { Box, Button, CircularProgress } from "@mui/material";
|
|
11
11
|
import Grid from "@mui/material/Grid2";
|
|
12
12
|
import { useQueryClient } from "@tanstack/react-query";
|
|
13
13
|
import { useRouter } from "next/navigation";
|
|
@@ -18,6 +18,7 @@ import React, {
|
|
|
18
18
|
type ReactNode,
|
|
19
19
|
useContext,
|
|
20
20
|
useEffect,
|
|
21
|
+
useRef,
|
|
21
22
|
useState,
|
|
22
23
|
} from "react";
|
|
23
24
|
|
|
@@ -62,12 +63,15 @@ const CheckoutForm = ({ cart, children }: CheckoutFormProps) => {
|
|
|
62
63
|
const { data: channelConfigurationData } = getChannelConfiguration.useData();
|
|
63
64
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
64
65
|
const [totalSteps, setTotalSteps] = useState(0);
|
|
66
|
+
const isSubmittingRef = useRef(false);
|
|
65
67
|
|
|
66
68
|
const cleanupCart = async (transactionId: string) => {
|
|
67
69
|
router.push(`${PageSlug.Confirmation}?transactionId=${transactionId}`);
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
const placeOrder = async () => {
|
|
73
|
+
if (isSubmittingRef.current) return;
|
|
74
|
+
isSubmittingRef.current = true;
|
|
71
75
|
setIsSubmitting(true);
|
|
72
76
|
const cache = queryClient.getQueryCache();
|
|
73
77
|
|
|
@@ -98,6 +102,8 @@ const CheckoutForm = ({ cart, children }: CheckoutFormProps) => {
|
|
|
98
102
|
.data as CardPaymentAcceptResult;
|
|
99
103
|
|
|
100
104
|
if (!paymentAcceptResult?.TokenizedPaymentCard) {
|
|
105
|
+
isSubmittingRef.current = false;
|
|
106
|
+
setIsSubmitting(false);
|
|
101
107
|
return;
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -121,8 +127,9 @@ const CheckoutForm = ({ cart, children }: CheckoutFormProps) => {
|
|
|
121
127
|
message: t("placeOrderErrorMessage"),
|
|
122
128
|
severity: "error",
|
|
123
129
|
});
|
|
130
|
+
isSubmittingRef.current = false;
|
|
131
|
+
setIsSubmitting(false);
|
|
124
132
|
}
|
|
125
|
-
setIsSubmitting(false);
|
|
126
133
|
};
|
|
127
134
|
|
|
128
135
|
return (
|
|
@@ -227,7 +234,11 @@ CheckoutForm.Footer = () => {
|
|
|
227
234
|
color="primary"
|
|
228
235
|
onClick={placeOrder}
|
|
229
236
|
>
|
|
230
|
-
{
|
|
237
|
+
{isSubmitting ? (
|
|
238
|
+
<CircularProgress size={24} color="inherit" />
|
|
239
|
+
) : (
|
|
240
|
+
t("placeOrderButton")
|
|
241
|
+
)}
|
|
231
242
|
</Button>
|
|
232
243
|
</Grid>
|
|
233
244
|
);
|
|
@@ -115,13 +115,15 @@ AddressCard.Body = () => {
|
|
|
115
115
|
if (!ctx) return null;
|
|
116
116
|
|
|
117
117
|
const { address } = ctx;
|
|
118
|
+
// Join only non-empty values so a dangling comma is never rendered.
|
|
119
|
+
const stateRegion = [address.State, address.ThreeLetterISORegionName]
|
|
120
|
+
.filter((value) => value?.trim())
|
|
121
|
+
.join(", ");
|
|
118
122
|
return (
|
|
119
123
|
<>
|
|
120
124
|
<Paragraph color="grey.700">{address.Street}</Paragraph>
|
|
121
125
|
<Paragraph color="grey.700">{address.City}</Paragraph>
|
|
122
|
-
<Paragraph color="grey.700">
|
|
123
|
-
{address.State}, {address.ThreeLetterISORegionName}
|
|
124
|
-
</Paragraph>
|
|
126
|
+
{stateRegion && <Paragraph color="grey.700">{stateRegion}</Paragraph>}
|
|
125
127
|
<Paragraph color="grey.700">{address.ZipCode}</Paragraph>
|
|
126
128
|
<Paragraph color="grey.700">{address.Phone}</Paragraph>
|
|
127
129
|
</>
|
|
@@ -14,9 +14,9 @@ import DialogContent from "@mui/material/DialogContent";
|
|
|
14
14
|
import Grid from "@mui/material/Grid2";
|
|
15
15
|
import TextField from "@mui/material/TextField";
|
|
16
16
|
import { merge } from "lodash";
|
|
17
|
+
import { useTranslations } from "next-intl";
|
|
17
18
|
import React from "react";
|
|
18
19
|
import { Controller, type UseFormReturn, useForm } from "react-hook-form";
|
|
19
|
-
import { useTranslations } from "use-intl";
|
|
20
20
|
import z from "zod";
|
|
21
21
|
|
|
22
22
|
type AddressFormInput = {
|
|
@@ -40,7 +40,7 @@ export const CmsPageView = ({ slug, cmsLocale, isDraftMode }: Props) => {
|
|
|
40
40
|
) : (
|
|
41
41
|
<Container>
|
|
42
42
|
<Paper sx={{ marginTop: "25px", padding: "50px" }} elevation={1}>
|
|
43
|
-
<H1>
|
|
43
|
+
<H1>e4 Storefront</H1>
|
|
44
44
|
<H4>
|
|
45
45
|
Add a page to the "Page" content-type in CMS to add
|
|
46
46
|
content to this page.
|
|
@@ -117,7 +117,9 @@ OrderedProducts.Comment = ({ line }: LineProps) => {
|
|
|
117
117
|
if (!line.Comment) return null;
|
|
118
118
|
return (
|
|
119
119
|
<Paragraph mt={1}>
|
|
120
|
-
<
|
|
120
|
+
<Span sx={{ display: "inline", fontSize: 14, fontWeight: 600 }}>
|
|
121
|
+
{t("commentLabel")}:
|
|
122
|
+
</Span>
|
|
121
123
|
{line.Comment}
|
|
122
124
|
</Paragraph>
|
|
123
125
|
);
|
|
@@ -56,7 +56,7 @@ const initialSearchArea: SearchArea = {
|
|
|
56
56
|
* @param {Function} props.handleAddToCart - Function to handle adding the product to cart
|
|
57
57
|
* @param {number} props.productId - ID of the product being searched
|
|
58
58
|
*
|
|
59
|
-
* @returns {
|
|
59
|
+
* @returns {React.ReactElement} - Rendered modal component that displays available stores with inventory information
|
|
60
60
|
*
|
|
61
61
|
* @example
|
|
62
62
|
* <FindInStoreModal
|
|
@@ -71,7 +71,7 @@ const FindInStoreModal = ({
|
|
|
71
71
|
open,
|
|
72
72
|
handleAddToCart,
|
|
73
73
|
productId,
|
|
74
|
-
}: Props):
|
|
74
|
+
}: Props): React.ReactElement => {
|
|
75
75
|
const t = useTranslations("productDetail");
|
|
76
76
|
const [hideOutOfStock, setHideOutOfStock] = React.useState(false);
|
|
77
77
|
const { data: channelConfig } = getChannelConfiguration.useSuspenseData();
|
|
@@ -154,7 +154,7 @@ const FindInStoreModal = ({
|
|
|
154
154
|
const _stores = Object.values(stores);
|
|
155
155
|
if (hideOutOfStock) {
|
|
156
156
|
return _stores.filter(
|
|
157
|
-
(store) => !isOutOfStock(store
|
|
157
|
+
(store) => !isOutOfStock(store?.location?.OrgUnitNumber || ""),
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
160
|
return _stores;
|
|
@@ -205,7 +205,7 @@ const FindInStoreModal = ({
|
|
|
205
205
|
address={location.Address || ""}
|
|
206
206
|
actionArea={renderActionArea(
|
|
207
207
|
location.OrgUnitNumber || "",
|
|
208
|
-
pickupOptions || [],
|
|
208
|
+
(pickupOptions || []) as DeliveryOption[],
|
|
209
209
|
inventoryRecord?.TotalAvailableInventoryLevelLabel || "",
|
|
210
210
|
)}
|
|
211
211
|
openFrom={location.OpenFrom || 0}
|
|
@@ -31,7 +31,7 @@ type Props = {
|
|
|
31
31
|
* @param {boolean} props.isOutOfStock - Flag indicating if the product is out of stock at this location
|
|
32
32
|
* @param {string} props.inventoryLabel - Label to display when product is out of stock
|
|
33
33
|
*
|
|
34
|
-
* @returns {
|
|
34
|
+
* @returns {React.ReactElement} The rendered pickup option selection component
|
|
35
35
|
*
|
|
36
36
|
* @example
|
|
37
37
|
* <PickupOptionSelect
|
|
@@ -48,7 +48,7 @@ const PickupOptionSelect = ({
|
|
|
48
48
|
onPickupClick,
|
|
49
49
|
isOutOfStock,
|
|
50
50
|
inventoryLabel,
|
|
51
|
-
}: Props):
|
|
51
|
+
}: Props): React.ReactElement => {
|
|
52
52
|
const [selectedOption, setSelectedOption] =
|
|
53
53
|
React.useState<DeliveryOption | null>(pickupOptions[0] || null);
|
|
54
54
|
const handlePickupOptionChange = (event: SelectChangeEvent) => {
|
|
@@ -19,14 +19,14 @@ type Props = {
|
|
|
19
19
|
* @param {Object} props - Component props
|
|
20
20
|
* @param {Function} props.handleSearch - Callback function that handles the search action with the search term
|
|
21
21
|
*
|
|
22
|
-
* @returns {
|
|
22
|
+
* @returns {React.ReactElement} A form with a search input field that triggers the search on submit or Enter key
|
|
23
23
|
*
|
|
24
24
|
* @example
|
|
25
25
|
* ```tsx
|
|
26
26
|
* <SearchHeader handleSearch={(term) => console.log(`Searching for ${term}`)} />
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
const SearchHeader = ({ handleSearch }: Props):
|
|
29
|
+
const SearchHeader = ({ handleSearch }: Props): React.ReactElement => {
|
|
30
30
|
const [searchTerm, setSearchTerm] = React.useState<string>("");
|
|
31
31
|
const t = useTranslations("productDetail.Bopis");
|
|
32
32
|
const renderSearchAdornment = () => (
|
|
@@ -3,22 +3,34 @@
|
|
|
3
3
|
import { H3 } from "@evenicanpm/storefront-core/src/components/Typography";
|
|
4
4
|
import type { AttributeValue } from "@msdyn365-commerce/retail-proxy";
|
|
5
5
|
|
|
6
|
-
type Props = { attributes: AttributeValue[] };
|
|
6
|
+
type Props = { attributes: AttributeValue[]; description?: string };
|
|
7
7
|
|
|
8
|
-
export default function ProductDescription({
|
|
8
|
+
export default function ProductDescription({
|
|
9
|
+
attributes,
|
|
10
|
+
description,
|
|
11
|
+
}: Readonly<Props>) {
|
|
9
12
|
return (
|
|
10
13
|
<div>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
{description && (
|
|
15
|
+
<div style={{ marginBottom: 16 }}>
|
|
16
|
+
<p>{description}</p>
|
|
17
|
+
</div>
|
|
18
|
+
)}
|
|
19
|
+
{attributes?.length > 0 && (
|
|
20
|
+
<>
|
|
21
|
+
<H3 mb={2}>Specifications:</H3>
|
|
22
|
+
{attributes.map((attr, index) => {
|
|
23
|
+
if (attr?.TextValue && attr?.Name) {
|
|
24
|
+
return (
|
|
25
|
+
<div key={`attr-${index + 1}`}>
|
|
26
|
+
{attr.Name}: {attr.TextValue}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
})}
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
22
34
|
</div>
|
|
23
35
|
);
|
|
24
36
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ProductDetails,
|
|
3
|
+
ProductPrice,
|
|
4
|
+
} from "@evenicanpm/storefront-core/src/api-manager/schemas/product.schema";
|
|
2
5
|
import type { SelectedDimensions } from "@evenicanpm/storefront-core/src/hooks/use-variants";
|
|
3
6
|
import type {
|
|
4
7
|
ProductDimensionValueInventoryAvailability,
|
|
@@ -44,8 +47,10 @@ export interface ProductImagesContext {
|
|
|
44
47
|
export interface ProductInfoContext {
|
|
45
48
|
/** Does the product have variants? If not we do not verify dimensions before adding to cart etc. **/
|
|
46
49
|
hasVariants: boolean;
|
|
47
|
-
/**
|
|
48
|
-
|
|
50
|
+
/** Combined loading state for price and inventory */
|
|
51
|
+
isProductDetailsPending: boolean;
|
|
52
|
+
/** */
|
|
53
|
+
productPrices: ProductPrice[] | undefined;
|
|
49
54
|
/** */
|
|
50
55
|
shippingInventoryData: ProductWarehouseInventoryInformation | undefined;
|
|
51
56
|
/** */
|
|
@@ -48,11 +48,17 @@ export interface Props {
|
|
|
48
48
|
*/
|
|
49
49
|
const ProductInfo = ({ children }: Props) => {
|
|
50
50
|
const [showDimensionError, setShowDimensionError] = useState(false);
|
|
51
|
-
const {
|
|
51
|
+
const {
|
|
52
|
+
masterProduct,
|
|
53
|
+
selectedVariant,
|
|
54
|
+
variantPending,
|
|
55
|
+
allDimensionsSelected,
|
|
56
|
+
} = useProductIntro();
|
|
52
57
|
|
|
53
58
|
const hasVariants = masterProduct?.Dimensions?.length !== 0;
|
|
54
59
|
|
|
55
60
|
const id = hasVariants ? selectedVariant?.RecordId : masterProduct?.RecordId;
|
|
61
|
+
const productId = selectedVariant?.RecordId || masterProduct?.RecordId;
|
|
56
62
|
|
|
57
63
|
const shippingInventorySearchCriteria =
|
|
58
64
|
!hasVariants || selectedVariant
|
|
@@ -65,11 +71,23 @@ const ProductInfo = ({ children }: Props) => {
|
|
|
65
71
|
enabled: !!shippingInventorySearchCriteria,
|
|
66
72
|
});
|
|
67
73
|
|
|
74
|
+
const { data: productPrices, isPending: pricesPending } =
|
|
75
|
+
getProductPrices.useData({
|
|
76
|
+
productIds: [productId],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const isProductDetailsPending =
|
|
80
|
+
pricesPending ||
|
|
81
|
+
!productPrices ||
|
|
82
|
+
variantPending ||
|
|
83
|
+
(allDimensionsSelected && inventoryPending);
|
|
84
|
+
|
|
68
85
|
return (
|
|
69
86
|
<ProductInfoContext.Provider
|
|
70
87
|
value={{
|
|
71
88
|
hasVariants,
|
|
72
|
-
|
|
89
|
+
isProductDetailsPending,
|
|
90
|
+
productPrices,
|
|
73
91
|
shippingInventoryData,
|
|
74
92
|
showDimensionError,
|
|
75
93
|
setShowDimensionError,
|
|
@@ -153,23 +171,13 @@ ProductInfo.Dimensions = () => {
|
|
|
153
171
|
};
|
|
154
172
|
|
|
155
173
|
ProductInfo.Price = () => {
|
|
156
|
-
const IntroCtx = useProductIntro();
|
|
157
|
-
if (!IntroCtx)
|
|
158
|
-
throw new Error(
|
|
159
|
-
"ProductInfo.Inventory must be wrapped in ProductInfoContext and a ProductIntroContext",
|
|
160
|
-
);
|
|
161
|
-
const { masterProduct, selectedVariant } = IntroCtx;
|
|
162
174
|
const { formatCurrency } = useCurrencyFormatter();
|
|
163
|
-
|
|
164
|
-
const { data: productPrices, isPending: pricesPending } =
|
|
165
|
-
getProductPrices.useData({
|
|
166
|
-
productIds: [selectedVariant?.RecordId || masterProduct?.RecordId],
|
|
167
|
-
});
|
|
175
|
+
const { isProductDetailsPending, productPrices } = useProductInfo();
|
|
168
176
|
|
|
169
177
|
return (
|
|
170
178
|
<Box pt={1}>
|
|
171
179
|
<H2 color="primary.main" mb={0.5} lineHeight="1">
|
|
172
|
-
{
|
|
180
|
+
{isProductDetailsPending ? (
|
|
173
181
|
<CircularProgress size={20} />
|
|
174
182
|
) : (
|
|
175
183
|
formatCurrency(productPrices?.[0]?.CustomerContextualPrice || 0)
|
|
@@ -190,8 +198,8 @@ ProductInfo.Inventory = () => {
|
|
|
190
198
|
throw new Error(
|
|
191
199
|
"ProductInfo.Inventory must be wrapped in ProductInfoContext and a ProductIntroContext",
|
|
192
200
|
);
|
|
193
|
-
const {
|
|
194
|
-
const { shippingInventoryData,
|
|
201
|
+
const { allDimensionsSelected } = IntroCtx;
|
|
202
|
+
const { shippingInventoryData, isProductDetailsPending } = InfoCtx;
|
|
195
203
|
if (!allDimensionsSelected)
|
|
196
204
|
return (
|
|
197
205
|
<FlexBox alignItems="center" gap={1}>
|
|
@@ -209,9 +217,7 @@ ProductInfo.Inventory = () => {
|
|
|
209
217
|
<FlexBox alignItems="center" gap={1}>
|
|
210
218
|
{/* INVENTORY */}
|
|
211
219
|
<Box sx={{ my: 3 }}>
|
|
212
|
-
{
|
|
213
|
-
<CircularProgress size={20} />
|
|
214
|
-
) : (
|
|
220
|
+
{!isProductDetailsPending && (
|
|
215
221
|
<Typography variant="body2" color="text.secondary" fontWeight={600}>
|
|
216
222
|
{getInventoryLabel(shippingInventoryData)}
|
|
217
223
|
</Typography>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@evenicanpm/storefront-core/src/pages/product-details/product-intro/compound/context";
|
|
12
12
|
import ThumbnailWithSkeleton from "@evenicanpm/storefront-core/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton";
|
|
13
13
|
import Grid from "@mui/material/Grid2";
|
|
14
|
+
import Skeleton from "@mui/material/Skeleton";
|
|
14
15
|
import type React from "react";
|
|
15
16
|
import { useContext, useState } from "react";
|
|
16
17
|
|
|
@@ -124,21 +125,38 @@ ProductImages.Main = () => {
|
|
|
124
125
|
"ProductImages.Main must be used inside a product images context",
|
|
125
126
|
);
|
|
126
127
|
const { name, images, primaryImageUrl, selectedImageIndex } = ctx;
|
|
128
|
+
const [loaded, setLoaded] = useState(false);
|
|
129
|
+
|
|
130
|
+
const src =
|
|
131
|
+
Array.isArray(images) &&
|
|
132
|
+
selectedImageIndex !== undefined &&
|
|
133
|
+
images[selectedImageIndex]
|
|
134
|
+
? images[selectedImageIndex]
|
|
135
|
+
: primaryImageUrl;
|
|
127
136
|
|
|
128
137
|
return (
|
|
129
|
-
<FlexBox
|
|
138
|
+
<FlexBox
|
|
139
|
+
borderRadius={3}
|
|
140
|
+
overflow="hidden"
|
|
141
|
+
justifyContent="center"
|
|
142
|
+
mb={6}
|
|
143
|
+
position="relative"
|
|
144
|
+
>
|
|
145
|
+
{!loaded && (
|
|
146
|
+
<Skeleton
|
|
147
|
+
variant="rounded"
|
|
148
|
+
width={300}
|
|
149
|
+
height={300}
|
|
150
|
+
sx={{ position: "absolute", inset: 0, m: "auto" }}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
130
153
|
<ImageWithFallback
|
|
131
154
|
alt={name ?? "product"}
|
|
132
155
|
width={300}
|
|
133
156
|
height={300}
|
|
134
157
|
loading="eager"
|
|
135
|
-
src={
|
|
136
|
-
|
|
137
|
-
selectedImageIndex !== undefined &&
|
|
138
|
-
images[selectedImageIndex]
|
|
139
|
-
? images[selectedImageIndex]
|
|
140
|
-
: primaryImageUrl
|
|
141
|
-
}
|
|
158
|
+
src={src}
|
|
159
|
+
onLoad={() => setLoaded(true)}
|
|
142
160
|
style={{ objectFit: "contain" }}
|
|
143
161
|
/>
|
|
144
162
|
</FlexBox>
|
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
import { Skeleton } from "@mui/material";
|
|
2
1
|
import Image from "next/image";
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
const shimmer = `<svg width="50" height="50" xmlns="http://www.w3.org/2000/svg"><rect width="50" height="50" fill="#f0f0f0"/></svg>`;
|
|
4
|
+
const blurDataURL = `data:image/svg+xml;base64,${typeof window === "undefined" ? Buffer.from(shimmer).toString("base64") : btoa(shimmer)}`;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Product thumbnail image
|
|
7
8
|
* @category Sub-Component
|
|
8
9
|
*/
|
|
9
10
|
function ThumbnailWithSkeleton({ src }: Readonly<{ src: string }>) {
|
|
10
|
-
const [loaded, setLoaded] = useState(false);
|
|
11
|
-
|
|
12
11
|
return (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}}
|
|
27
|
-
/>
|
|
28
|
-
</>
|
|
12
|
+
<Image
|
|
13
|
+
src={src}
|
|
14
|
+
alt="Product thumbnail"
|
|
15
|
+
width={50}
|
|
16
|
+
height={50}
|
|
17
|
+
placeholder="blur"
|
|
18
|
+
blurDataURL={blurDataURL}
|
|
19
|
+
style={{
|
|
20
|
+
width: "80%",
|
|
21
|
+
height: "80%",
|
|
22
|
+
objectFit: "cover",
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
29
25
|
);
|
|
30
26
|
}
|
|
31
27
|
|
|
@@ -23,11 +23,16 @@ const StyledTabs = styled(Tabs)(({ theme }) => ({
|
|
|
23
23
|
},
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
type Props = {
|
|
26
|
+
type Props = {
|
|
27
|
+
productAttributes: AttributeValue[];
|
|
28
|
+
productDescription?: string;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
};
|
|
27
31
|
|
|
28
32
|
export interface TabsContext {
|
|
29
33
|
selectedOption: number;
|
|
30
34
|
productAttributes: AttributeValue[];
|
|
35
|
+
productDescription?: string;
|
|
31
36
|
handleOptionClick: (_: React.SyntheticEvent, value: number) => void;
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -54,6 +59,7 @@ const TabsContext = createContext<TabsContext | null>(null);
|
|
|
54
59
|
*/
|
|
55
60
|
export default function ProductTabs({
|
|
56
61
|
productAttributes,
|
|
62
|
+
productDescription,
|
|
57
63
|
children,
|
|
58
64
|
}: Readonly<Props>) {
|
|
59
65
|
const [selectedOption, setSelectedOption] = useState(0);
|
|
@@ -62,7 +68,12 @@ export default function ProductTabs({
|
|
|
62
68
|
|
|
63
69
|
return (
|
|
64
70
|
<TabsContext.Provider
|
|
65
|
-
value={{
|
|
71
|
+
value={{
|
|
72
|
+
selectedOption,
|
|
73
|
+
handleOptionClick,
|
|
74
|
+
productAttributes,
|
|
75
|
+
productDescription,
|
|
76
|
+
}}
|
|
66
77
|
>
|
|
67
78
|
<Box mb={6}>{children}</Box>
|
|
68
79
|
</TabsContext.Provider>
|
|
@@ -130,6 +141,11 @@ ProductTabs.Description = () => {
|
|
|
130
141
|
throw new Error(
|
|
131
142
|
"ProductTabs.Tabs must be used inside a ProductTabsContext",
|
|
132
143
|
);
|
|
133
|
-
const { productAttributes } = ctx;
|
|
134
|
-
return
|
|
144
|
+
const { productAttributes, productDescription } = ctx;
|
|
145
|
+
return (
|
|
146
|
+
<ProductDescription
|
|
147
|
+
attributes={productAttributes}
|
|
148
|
+
description={productDescription}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
135
151
|
};
|
|
@@ -15,6 +15,7 @@ import useProductList from "@evenicanpm/storefront-core/src/pages/product-list/u
|
|
|
15
15
|
import generateBreadcrumbs from "@evenicanpm/storefront-core/src/pages/product-list/utils/generate-breadcrumbs";
|
|
16
16
|
import { searchCategory } from "@evenicanpm/storefront-core/src/pages/product-list/utils/search-for-category";
|
|
17
17
|
import { usePreviousRefiners } from "@evenicanpm/storefront-core/src/pages/product-list/utils/use-previous-refiners";
|
|
18
|
+
import type { ProductSearchResult } from "@msdyn365-commerce/retail-proxy";
|
|
18
19
|
import { Box, CircularProgress, Paper, Typography } from "@mui/material";
|
|
19
20
|
import Grid from "@mui/material/Grid2";
|
|
20
21
|
import { useLocale, useTranslations } from "next-intl";
|
|
@@ -149,7 +150,7 @@ ProductListView.Pagination = function Pager() {
|
|
|
149
150
|
ProductListView.Results = function Results() {
|
|
150
151
|
const t = useTranslations("ProductList");
|
|
151
152
|
const { productData, isLoading, view, productPrices } = useProductList();
|
|
152
|
-
const products = productData?.products ?? [];
|
|
153
|
+
const products = (productData?.products ?? []) as ProductSearchResult[];
|
|
153
154
|
const paginationNode = <ProductListView.Pagination />;
|
|
154
155
|
|
|
155
156
|
if (isLoading) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import type { CmsNavItem } from "@evenicanpm/cms/src/content/navigation/types";
|
|
2
3
|
import type { CategoryHierarchy } from "@evenicanpm/storefront-core/src/lib/category-helpers";
|
|
3
4
|
import { createLink } from "@evenicanpm/storefront-core/src/providers/nav-provider/utils/createLink";
|
|
4
5
|
import { useTranslations } from "next-intl";
|
|
5
6
|
import { createContext, type PropsWithChildren, useState } from "react";
|
|
6
|
-
import type { CmsNavItem } from "@/cms/interfaces/navigation";
|
|
7
7
|
|
|
8
8
|
type NavContextType = {
|
|
9
9
|
cmsNav: CmsNavItem[];
|
package/tsconfig.json
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
"resolveJsonModule": true,
|
|
12
12
|
"outDir": "./dist",
|
|
13
13
|
"jsx": "preserve",
|
|
14
|
-
"
|
|
14
|
+
"baseUrl": ".",
|
|
15
15
|
"paths": {
|
|
16
|
-
"*": ["../../../../storefront/node_modules/*"],
|
|
17
16
|
"@evenicanpm/storefront-core/src/*": ["./src/*"],
|
|
18
17
|
"@evenicanpm/storefront-core/src/lib/*": ["src/lib/*"],
|
|
19
18
|
"@evenicanpm/storefront-core/src/api-manager/*": ["src/api-manager/*"],
|
package/__mocks__/countries.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { CountryRegionInfo } from "@msdyn365-commerce/retail-proxy";
|
|
2
|
-
|
|
3
|
-
export const countries: CountryRegionInfo[] = [
|
|
4
|
-
{ ShortName: "Canada", CountryRegionId: "CA", ISOCode: "CA" },
|
|
5
|
-
{
|
|
6
|
-
ShortName: "United States",
|
|
7
|
-
CountryRegionId: "US",
|
|
8
|
-
ISOCode: "US",
|
|
9
|
-
},
|
|
10
|
-
{ ShortName: "Mexico", CountryRegionId: "MX", ISOCode: "MX" },
|
|
11
|
-
];
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { queryClient } from "./query-client";
|
|
2
|
-
|
|
3
|
-
type MutationInput = {
|
|
4
|
-
cartLine?: Array<{ Quantity?: number }>;
|
|
5
|
-
} & Record<string, unknown>;
|
|
6
|
-
|
|
7
|
-
type CartMockData = {
|
|
8
|
-
CartLines: Array<Record<string, unknown>>;
|
|
9
|
-
} & Record<string, unknown>;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
* Mocking up at the react-query level because our components
|
|
14
|
-
* rely on react-query for triggering re-renders and state subscriptions.
|
|
15
|
-
*
|
|
16
|
-
*/
|
|
17
|
-
export const useCreateMutation = (apiManagerPath: readonly unknown[]) => {
|
|
18
|
-
switch (apiManagerPath[1]) {
|
|
19
|
-
case "updateCartLines":
|
|
20
|
-
return {
|
|
21
|
-
mutate: (input: MutationInput) => {
|
|
22
|
-
console.log(input);
|
|
23
|
-
queryClient.setQueryData(["cart"], (oldData: CartMockData) => ({
|
|
24
|
-
...oldData,
|
|
25
|
-
CartLines: [
|
|
26
|
-
{
|
|
27
|
-
...oldData.CartLines[0],
|
|
28
|
-
Quantity: input.cartLine?.[0]?.Quantity,
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
}));
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
case "deleteCartLine":
|
|
35
|
-
return {
|
|
36
|
-
mutate: (input: MutationInput) => {
|
|
37
|
-
console.log(input);
|
|
38
|
-
queryClient.setQueryData(["cart"], (oldData: CartMockData) => ({
|
|
39
|
-
...oldData,
|
|
40
|
-
CartLines: [],
|
|
41
|
-
}));
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
case "addToCart":
|
|
45
|
-
return {
|
|
46
|
-
mutate: (input: MutationInput) => {
|
|
47
|
-
console.log(input);
|
|
48
|
-
queryClient.setQueryData(["cart"], (oldData: CartMockData) => ({
|
|
49
|
-
...oldData,
|
|
50
|
-
CartLines: [
|
|
51
|
-
{
|
|
52
|
-
ProductId: 1,
|
|
53
|
-
Quantity: 1,
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
}));
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
case "sessionInit":
|
|
60
|
-
return { mutateAsync: () => {} };
|
|
61
|
-
default:
|
|
62
|
-
return {
|
|
63
|
-
mutate: () => {
|
|
64
|
-
console.log("No Mock Implemented");
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
};
|