@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +29 -9
  3. package/src/api-manager/datasources/d365/d365-address.datasource.ts +11 -0
  4. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +23 -1
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +6 -1
  6. package/src/api-manager/datasources/d365/d365-user.datasource.ts +29 -14
  7. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
  8. package/src/api-manager/index.ts +2 -1
  9. package/src/api-manager/schemas/product.schema.ts +1 -1
  10. package/src/auth/msal.ts +1 -4
  11. package/src/cms/blocks/block-manager.tsx +1 -1
  12. package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
  13. package/src/cms/blocks/components/footer/index.tsx +9 -71
  14. package/src/cms/blocks/components/footer/interfaces.ts +4 -0
  15. package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
  16. package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
  17. package/src/cms/endpoints.ts +7 -2
  18. package/src/components/BazaarMenu.tsx +1 -1
  19. package/src/components/Typography.tsx +4 -2
  20. package/src/components/_tests_/site-logo.test.tsx +1 -1
  21. package/src/components/carousel-cards/carousel-card-1/CarouselCard1.stories.tsx +1 -1
  22. package/src/components/categories/category-menu.tsx +1 -1
  23. package/src/components/countries-input.tsx +4 -0
  24. package/src/components/header/components/user.tsx +4 -5
  25. package/src/components/navbar/mobile-menu/mobile-menu.test.tsx +1 -1
  26. package/src/components/navbar/utils/transform-nav.test.ts +1 -1
  27. package/src/components/navbar/utils/transform-nav.ts +1 -1
  28. package/src/components/product-cards/product-card/product-card.tsx +5 -2
  29. package/src/components/product-cards/product-card/styles/index.ts +1 -5
  30. package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
  31. package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
  32. package/src/components/products-view/compound/products-grid-view.tsx +5 -1
  33. package/src/components/products-view/compound/products-list-view.tsx +5 -1
  34. package/src/components/site-logo.tsx +1 -1
  35. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
  36. package/src/global.d.ts +3 -0
  37. package/src/pages/account/account-navigation.tsx +2 -2
  38. package/src/pages/account/account-routes.ts +38 -4
  39. package/src/pages/account/addresses/address-form.tsx +20 -2
  40. package/src/pages/account/table-row-skeleton.tsx +31 -0
  41. package/src/pages/account/wishlist/wishlist-item.tsx +3 -1
  42. package/src/pages/blog/blog-detail-view.tsx +3 -1
  43. package/src/pages/cart/cart-item.tsx +1 -7
  44. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +14 -3
  45. package/src/pages/checkout/checkout-alt-form/steps/address/address-card.tsx +5 -3
  46. package/src/pages/checkout/checkout-alt-form/steps/address/new-address-form.tsx +1 -1
  47. package/src/pages/cms-page-view.tsx +1 -1
  48. package/src/pages/confirmation/ordered-products.tsx +3 -1
  49. package/src/pages/product-details/bopis/find-in-store-modal.tsx +4 -4
  50. package/src/pages/product-details/bopis/pickup-option-select.tsx +2 -2
  51. package/src/pages/product-details/bopis/search-header.tsx +2 -2
  52. package/src/pages/product-details/product-description.tsx +25 -13
  53. package/src/pages/product-details/product-intro/compound/context.ts +8 -3
  54. package/src/pages/product-details/product-intro/compound/product-info.tsx +25 -19
  55. package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
  56. package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
  57. package/src/pages/product-details/product-tabs.tsx +20 -4
  58. package/src/pages/product-list/product-list-view.tsx +2 -1
  59. package/src/providers/nav-provider/index.tsx +1 -1
  60. package/src/providers/nav-provider/utils/createLink.ts +1 -1
  61. package/tsconfig.json +1 -2
  62. package/__mocks__/countries.ts +0 -11
  63. package/__mocks__/create-mutation.ts +0 -68
  64. package/__mocks__/create-query.ts +0 -94
  65. package/__mocks__/data/categories.json +0 -795
  66. package/__mocks__/get-product-by-id.ts +0 -6
  67. package/__mocks__/next-auth-react.ts +0 -9
  68. package/__mocks__/next-font.js +0 -4
  69. package/__mocks__/next-headers.js +0 -13
  70. package/__mocks__/next-image.tsx +0 -18
  71. package/__mocks__/next-link.js +0 -19
  72. package/__mocks__/next-navigation.ts +0 -29
  73. package/__mocks__/product-dimensions.ts +0 -635
  74. package/__mocks__/products.ts +0 -533
  75. package/__mocks__/query-client.ts +0 -3
  76. package/__mocks__/wishlists.json +0 -408
  77. package/src/cms/blog.ts +0 -229
  78. package/src/cms/interfaces/navigation.ts +0 -52
  79. package/src/cms/interfaces/product-data.ts +0 -83
  80. 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
- {t("placeOrderButton")}
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>E4 Storefront</H1>
43
+ <H1>e4 Storefront</H1>
44
44
  <H4>
45
45
  Add a page to the &quot;Page&quot; 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
- <H6 sx={{ display: "inline" }}>{t("commentLabel")}: </H6>
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 {JSX.Element} - Rendered modal component that displays available stores with inventory information
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): JSX.Element => {
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.location.OrgUnitNumber || ""),
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 {JSX.Element} The rendered pickup option selection component
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): JSX.Element => {
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 {JSX.Element} A form with a search input field that triggers the search on submit or Enter key
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): JSX.Element => {
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({ attributes }: Readonly<Props>) {
8
+ export default function ProductDescription({
9
+ attributes,
10
+ description,
11
+ }: Readonly<Props>) {
9
12
  return (
10
13
  <div>
11
- <H3 mb={2}>Specifications:</H3>
12
- {attributes?.map((attr, index) => {
13
- if (attr?.TextValue && attr?.Name) {
14
- return (
15
- <div key={`attr-${index + 1}`}>
16
- {attr.Name}: {attr.TextValue}
17
- </div>
18
- );
19
- }
20
- return null;
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 { ProductDetails } from "@evenicanpm/storefront-core/src/api-manager/schemas/product.schema";
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
- inventoryPending: boolean;
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 { masterProduct, selectedVariant } = useProductIntro();
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
- inventoryPending,
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
- {pricesPending || !productPrices ? (
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 { variantPending, allDimensionsSelected } = IntroCtx;
194
- const { shippingInventoryData, inventoryPending } = InfoCtx;
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
- {inventoryPending || variantPending ? (
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 borderRadius={3} overflow="hidden" justifyContent="center" mb={6}>
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
- Array.isArray(images) &&
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
- import { useState } from "react";
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
- * Shows a skeleton until the image loads
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
- {!loaded && <Skeleton variant="rounded" width={50} height={50} />}
15
- <Image
16
- src={src}
17
- alt="Product thumbnail"
18
- width={50}
19
- height={50}
20
- onLoad={() => setLoaded(true)}
21
- style={{
22
- display: loaded ? "block" : "none",
23
- width: "80%",
24
- height: "80%",
25
- objectFit: "cover",
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 = { productAttributes: AttributeValue[]; children: React.ReactNode };
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={{ selectedOption, handleOptionClick, productAttributes }}
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 <ProductDescription attributes={productAttributes} />;
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[];
@@ -1,4 +1,4 @@
1
- import type { CmsNavItem } from "@evenicanpm/storefront-core/src/cms/interfaces/navigation";
1
+ import type { CmsNavItem } from "@evenicanpm/cms/src/content/navigation/types";
2
2
 
3
3
  export const createLink = (name: string, path: string): CmsNavItem => {
4
4
  return {
package/tsconfig.json CHANGED
@@ -11,9 +11,8 @@
11
11
  "resolveJsonModule": true,
12
12
  "outDir": "./dist",
13
13
  "jsx": "preserve",
14
- "typeRoots": ["../../../../storefront/node_modules/@types"],
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/*"],
@@ -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
- };