@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,6 +7,9 @@ import { Heading } from "@/cms/blocks/components/footer/styles/index";
7
7
  interface Props {
8
8
  isDark?: boolean;
9
9
  title?: string;
10
+ address?: string;
11
+ email?: string;
12
+ phoneNumber?: string;
10
13
  social?: SocialLink[];
11
14
  }
12
15
 
@@ -15,17 +18,23 @@ const AboutLinks = (props: Props) => {
15
18
  <Grid size={{ lg: 3, md: 6, sm: 6, xs: 12 }}>
16
19
  <Heading>Contact Us</Heading>
17
20
 
18
- <Paragraph py={0.6} color="grey.500">
19
- 70 Washington Square South, New York, NY 10012, United States
20
- </Paragraph>
21
+ {props.address && (
22
+ <Paragraph py={0.6} color="grey.500">
23
+ {props.address}
24
+ </Paragraph>
25
+ )}
21
26
 
22
- <Paragraph py={0.6} color="grey.500">
23
- Email: evenica@example.org
24
- </Paragraph>
27
+ {props.email && (
28
+ <Paragraph py={0.6} color="grey.500">
29
+ Email: {props.email}
30
+ </Paragraph>
31
+ )}
25
32
 
26
- <Paragraph py={0.6} mb={2} color="grey.500">
27
- Phone: +1 1123 456 780
28
- </Paragraph>
33
+ {props.phoneNumber && (
34
+ <Paragraph py={0.6} mb={2} color="grey.500">
35
+ Phone: {props.phoneNumber}
36
+ </Paragraph>
37
+ )}
29
38
 
30
39
  <SocialLinks socials={props.social} />
31
40
  </Grid>
@@ -2,20 +2,35 @@
2
2
 
3
3
  import SiteLogo from "@evenicanpm/storefront-core/src/components/site-logo";
4
4
  import { Paragraph } from "@evenicanpm/storefront-core/src/components/Typography";
5
+ import { Box } from "@mui/material";
5
6
  import Grid from "@mui/material/Grid2";
6
7
  import useTheme from "@mui/material/styles/useTheme";
7
8
  import useMediaQuery from "@mui/material/useMediaQuery";
9
+ import cmsEndPoints from "@/cms/endpoints";
8
10
 
9
11
  interface Props {
12
+ logoImage?: { url: string; alternativeText?: string };
10
13
  description: string;
11
14
  }
12
15
 
13
16
  export default function LogoSection(props: Readonly<Props>) {
14
17
  const theme = useTheme();
15
- const downMd = useMediaQuery(theme.breakpoints.down(1150)); // same like in header
18
+ const downMd = useMediaQuery(theme.breakpoints.down(1150));
19
+ const logoHeight = downMd ? "2.125rem" : "3rem";
20
+
16
21
  return (
17
22
  <Grid size={{ lg: 3, md: 6, sm: 6, xs: 12 }}>
18
- <SiteLogo {...(downMd ? { height: "2.125rem" } : {})} />
23
+ {props.logoImage?.url ? (
24
+ <Box mb={1}>
25
+ <img
26
+ src={cmsEndPoints.buildCmsMediaUrl(props.logoImage.url)}
27
+ alt={props.logoImage.alternativeText || "Logo"}
28
+ style={{ height: logoHeight, width: "auto" }}
29
+ />
30
+ </Box>
31
+ ) : (
32
+ <SiteLogo {...(downMd ? { height: "2.125rem" } : {})} />
33
+ )}
19
34
 
20
35
  <Paragraph mb={2.5} color="grey.500">
21
36
  {props.description}
@@ -1,5 +1,3 @@
1
- import type { Formats } from "@/cms/interfaces/product-data";
2
-
3
1
  const apiKeys = {
4
2
  pages: "/cms/pages",
5
3
  blogs: "/cms/blogs",
@@ -42,6 +40,13 @@ export const extendCmsEndpoints = (
42
40
 
43
41
  export type sizes = "small" | "medium" | "large" | "thumbnail";
44
42
 
43
+ interface Formats {
44
+ small?: { url: string };
45
+ medium?: { url: string };
46
+ large?: { url: string };
47
+ thumbnail?: { url: string };
48
+ }
49
+
45
50
  /**
46
51
  * @param formats formats array from CMS data
47
52
  * @param size size of the image
@@ -34,7 +34,7 @@ export default function BazaarMenu({
34
34
  {handler ? handler(handleClick) : null}
35
35
 
36
36
  <Menu
37
- elevation={0}
37
+ slotProps={{ paper: { elevation: 0 } }}
38
38
  anchorEl={anchorEl}
39
39
  onClose={handleClose}
40
40
  open={open ?? Boolean(anchorEl)}
@@ -6,9 +6,11 @@ import clsx from "clsx";
6
6
 
7
7
  // ==============================================================
8
8
  type Ellipsis = { ellipsis: number };
9
- export interface EllipsisProps extends BoxProps {
9
+ export type EllipsisProps = BoxProps & {
10
10
  ellipsis?: boolean;
11
- }
11
+ children?: React.ReactNode;
12
+ className?: string;
13
+ };
12
14
  // ==============================================================
13
15
 
14
16
  const StyledBox = styled(Box, {
@@ -15,7 +15,7 @@ vi.mock("next/link", () => ({
15
15
  describe("SiteLogo", () => {
16
16
  it("renders logo image", () => {
17
17
  render(<SiteLogo />);
18
- const logo = screen.getByAltText("E4 Logo");
18
+ const logo = screen.getByAltText("e4 Logo");
19
19
  expect(logo).toBeInTheDocument();
20
20
  // next/image rewrites src to an optimized URL (e.g. /_next/image?url=...)
21
21
  expect(logo.getAttribute("src")).toMatch(/logo-blue\.png/);
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from "@storybook/react";
1
+ import type { Meta, StoryObj } from "@storybook/nextjs";
2
2
  import CarouselCard1 from "./index";
3
3
 
4
4
  const meta: Meta<typeof CarouselCard1> = {
@@ -24,7 +24,7 @@ const Wrapper = styled("div", {
24
24
  type Props = {
25
25
  render: (
26
26
  handler: (e: React.MouseEvent<HTMLButtonElement>) => void,
27
- ) => JSX.Element;
27
+ ) => React.ReactElement;
28
28
  };
29
29
 
30
30
  // CategoryMenu is acting purely as a configuration/wiring component,
@@ -57,6 +57,10 @@ export default function CountryInput({
57
57
  fullWidth
58
58
  value={selectedCountry}
59
59
  options={countries}
60
+ getOptionKey={(option) => option.CountryRegionId}
61
+ isOptionEqualToValue={(option, value) =>
62
+ option.CountryRegionId === value?.CountryRegionId
63
+ }
60
64
  onChange={(_event, newValue) => {
61
65
  setSelectedCountry(newValue);
62
66
  }}
@@ -16,7 +16,7 @@ import T from "@evenicanpm/storefront-core/src/components/T";
16
16
  import {
17
17
  type AccountRoute,
18
18
  type AccountRouteOverrides,
19
- createAccountRoutes,
19
+ useCreateAccountRoutes,
20
20
  } from "@evenicanpm/storefront-core/src/pages/account/account-routes";
21
21
  // UI Components
22
22
  import {
@@ -61,7 +61,7 @@ const routeToMenuItem = (route: AccountRoute) => ({
61
61
  });
62
62
 
63
63
  const buildMenuItems = (overrides?: AccountRouteOverrides) => {
64
- const routes = createAccountRoutes(overrides);
64
+ const routes = useCreateAccountRoutes(overrides);
65
65
  return [
66
66
  routeToMenuItem(routes.profile),
67
67
  routeToMenuItem(routes.orders),
@@ -89,10 +89,9 @@ const UserContext = createContext<UserContextValue | undefined>(undefined);
89
89
 
90
90
  interface Props {
91
91
  children?: ReactNode;
92
- routeOverrides?: AccountRouteOverrides;
93
92
  }
94
93
 
95
- const User = ({ children, routeOverrides }: Props) => {
94
+ const User = ({ children }: Props) => {
96
95
  const { data: session, isPending } = useBetterAuthSession();
97
96
  const status = isPending
98
97
  ? "loading"
@@ -166,7 +165,7 @@ const User = ({ children, routeOverrides }: Props) => {
166
165
  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
167
166
  const open = Boolean(anchorEl);
168
167
 
169
- const menuItems = buildMenuItems(routeOverrides);
168
+ const menuItems = buildMenuItems();
170
169
 
171
170
  const value: UserContextValue = {
172
171
  isAuthenticated,
@@ -1,9 +1,9 @@
1
+ import type { CmsNavItem } from "@evenicanpm/cms/src/content/navigation/types";
1
2
  import MobileMenu from "@evenicanpm/storefront-core/src/components/navbar/mobile-menu/mobile-menu";
2
3
  import { transformCmsNav } from "@evenicanpm/storefront-core/src/components/navbar/utils/transform-nav";
3
4
  import { fireEvent, render, screen } from "@testing-library/react";
4
5
  import type React from "react";
5
6
  import { beforeEach, describe, it, vi } from "vitest";
6
- import type { CmsNavItem } from "@/cms/interfaces/navigation";
7
7
 
8
8
  const useContext = vi.fn();
9
9
 
@@ -1,10 +1,10 @@
1
+ import type { CmsNavItem } from "@evenicanpm/cms/src/content/navigation/types";
1
2
  import {
2
3
  transformCategoryNav,
3
4
  transformCmsNav,
4
5
  } from "@evenicanpm/storefront-core/src/components/navbar/utils/transform-nav";
5
6
  import type { CategoryHierarchy } from "@evenicanpm/storefront-core/src/lib/category-helpers";
6
7
  import { describe, expect, it } from "vitest";
7
- import type { CmsNavItem } from "@/cms/interfaces/navigation";
8
8
 
9
9
  describe("transform nav items", () => {
10
10
  it("should recursively transform deeply nested categories and its children with correct href, title, and items", () => {
@@ -1,6 +1,6 @@
1
+ import type { CmsNavItem } from "@evenicanpm/cms/src/content/navigation/types";
1
2
  import type { CategoryHierarchy } from "@evenicanpm/storefront-core/src/lib/category-helpers";
2
3
  import type { SvgIconComponent } from "@mui/icons-material";
3
- import type { CmsNavItem } from "@/cms/interfaces/navigation";
4
4
 
5
5
  type MenuType = "default" | "mega-menu" | "full-screen-mega-menu";
6
6
  export interface TransformedNavItem {
@@ -60,8 +60,11 @@ function findChild(
60
60
  if (!React.isValidElement(child)) return;
61
61
  if (child.type === component) {
62
62
  found = true;
63
- } else if (child.props?.children) {
64
- found ||= findChild(child.props.children, component);
63
+ } else if ((child.props as { children?: ReactNode })?.children) {
64
+ found ||= findChild(
65
+ (child.props as { children?: ReactNode }).children,
66
+ component,
67
+ );
65
68
  }
66
69
  });
67
70
 
@@ -1,7 +1,6 @@
1
1
  // GLOBAL CUSTOM COMPONENT
2
2
  import BazaarCard from "@evenicanpm/storefront-core/src/components/BazaarCard";
3
3
  import styled from "@mui/material/styles/styled";
4
- import type { StyledComponent } from "@mui/styled-engine";
5
4
 
6
5
  export const StyledBazaarCard = styled(BazaarCard)({
7
6
  height: "100%",
@@ -46,7 +45,4 @@ export const ContentWrapper = styled("div")({
46
45
  },
47
46
  });
48
47
 
49
- const StyledBazaarCardType: StyledComponent<typeof BazaarCard, object, "div"> =
50
- StyledBazaarCard as StyledComponent<typeof BazaarCard, object, "div">;
51
-
52
- export default StyledBazaarCardType;
48
+ export default StyledBazaarCard;
@@ -1,7 +1,4 @@
1
- import {
2
- FlexBetween,
3
- FlexBox,
4
- } from "@evenicanpm/storefront-core/src/components/flex-box/index";
1
+ import { FlexBetween } from "@evenicanpm/storefront-core/src/components/flex-box/index";
5
2
  import ImageWithFallback from "@evenicanpm/storefront-core/src/components/ImageWithFallback";
6
3
  // LOCAL CUSTOM COMPONENTS
7
4
  import DiscountChip from "@evenicanpm/storefront-core/src/components/product-cards/discount-chip";
@@ -9,6 +6,7 @@ import FavoriteButton from "@evenicanpm/storefront-core/src/components/product-c
9
6
  // STYLED COMPONENTS
10
7
  import {
11
8
  HoverIconWrapper,
9
+ PlpImageWrapper,
12
10
  PriceText,
13
11
  } from "@evenicanpm/storefront-core/src/components/product-cards/product-card-plp/styles/index";
14
12
  import QuantityButtons from "@evenicanpm/storefront-core/src/components/product-cards/quantity-buttons";
@@ -86,15 +84,9 @@ ProductCardPlp.ImageWrapper = ({ children }: { children: React.ReactNode }) => {
86
84
 
87
85
  return (
88
86
  <Link href={buildDetailUrl(title, id)}>
89
- <FlexBox
90
- className="hover-box-container"
91
- position="relative"
92
- bgcolor="grey.50"
93
- borderRadius={3}
94
- mb={2}
95
- >
87
+ <PlpImageWrapper className="hover-box-container">
96
88
  {children}
97
- </FlexBox>
89
+ </PlpImageWrapper>
98
90
  </Link>
99
91
  );
100
92
  };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { FlexBox } from "@evenicanpm/storefront-core/src/components/flex-box/index";
3
4
  import styled from "@mui/material/styles/styled";
4
5
 
5
6
  export const PriceText = styled("p")(({ theme }) => ({
@@ -17,6 +18,20 @@ export const PriceText = styled("p")(({ theme }) => ({
17
18
  },
18
19
  }));
19
20
 
21
+ export const PlpImageWrapper = styled(FlexBox)(({ theme }) => ({
22
+ position: "relative",
23
+ backgroundColor: theme.palette.grey[50],
24
+ borderRadius: theme.shape.borderRadius * 3,
25
+ marginBottom: theme.spacing(2),
26
+
27
+ "& img": {
28
+ maxHeight: 300,
29
+ objectFit: "contain",
30
+ width: "100%",
31
+ borderRadius: 12,
32
+ },
33
+ }));
34
+
20
35
  export const HoverIconWrapper = styled("div")({
21
36
  zIndex: 2,
22
37
  top: "7px",
@@ -52,7 +52,11 @@ export default function ProductsGridView(props: Readonly<ProductsViewProps>) {
52
52
  * @category Sub-Component
53
53
  *
54
54
  */
55
- ProductsGridView.Grid = ({ children }: { children: React.ReactElement }) => {
55
+ ProductsGridView.Grid = ({
56
+ children,
57
+ }: {
58
+ children: React.ReactElement<Record<string, unknown>>;
59
+ }) => {
56
60
  const { products, productPrices } = useProductsView();
57
61
 
58
62
  return (
@@ -38,7 +38,11 @@ export default function ProductsListView(props: Readonly<ProductsViewProps>) {
38
38
  * @category Sub-Component
39
39
  *
40
40
  */
41
- ProductsListView.List = ({ children }: { children: React.ReactElement }) => {
41
+ ProductsListView.List = ({
42
+ children,
43
+ }: {
44
+ children: React.ReactElement<Record<string, unknown>>;
45
+ }) => {
42
46
  const { products, productPrices } = useProductsView();
43
47
  return (
44
48
  <>
@@ -12,7 +12,7 @@ const SiteLogo: React.FC<SiteLogoProps> = () => {
12
12
  <Link href="/">
13
13
  <Image
14
14
  src="/logo-blue.png"
15
- alt="E4 Logo"
15
+ alt="e4 Logo"
16
16
  width={180}
17
17
  height={54}
18
18
  style={{ width: "auto", height: 54 }}
@@ -9,6 +9,7 @@ import {
9
9
  } from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/add-to-wishlist/compound/context";
10
10
  import UseWishlist from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/add-to-wishlist/hooks/use-add-to-wishlist";
11
11
  import { StyledTableRow } from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/styles";
12
+ import { useCreateAccountRoutes } from "@evenicanpm/storefront-core/src/pages/account/account-routes";
12
13
  import { Add, Delete } from "@mui/icons-material";
13
14
  import { IconButton } from "@mui/material";
14
15
  import { format } from "date-fns";
@@ -25,6 +26,7 @@ export default function WishlistDialogItem() {
25
26
 
26
27
  const itemCtx = useContext(WishListItemContext);
27
28
  const dialogCtx = useContext(AddToWishListDialogContext);
29
+ const { wishlists: wishlistsRoute } = useCreateAccountRoutes();
28
30
 
29
31
  if (!itemCtx || !dialogCtx)
30
32
  throw new Error("WishlistModalItem must be used within a wishlist context");
@@ -59,7 +61,7 @@ export default function WishlistDialogItem() {
59
61
  }}
60
62
  >
61
63
  <Paragraph ellipsis sx={{ fontWeight: "bold" }}>
62
- <Link href={`/account/wish-lists/${wishlist?.Id}`}>
64
+ <Link href={`${wishlistsRoute.href}/${wishlist?.Id}`}>
63
65
  {wishlist?.Name ?? ""}
64
66
  </Link>
65
67
  </Paragraph>
@@ -0,0 +1,3 @@
1
+ declare module "*.css";
2
+ declare module "*.scss";
3
+ declare module "*.sass";
@@ -12,7 +12,7 @@ import {
12
12
  import {
13
13
  type AccountRoute,
14
14
  type AccountRouteOverrides,
15
- createAccountRoutes,
15
+ useCreateAccountRoutes,
16
16
  } from "@evenicanpm/storefront-core/src/pages/account/account-routes";
17
17
  // STYLED COMPONENTS
18
18
  import {
@@ -85,7 +85,7 @@ const routeToNavItem = (route: AccountRoute): NavItem => {
85
85
  export const DEFAULT_MENU_ITEMS = (
86
86
  overrides?: AccountRouteOverrides,
87
87
  ): NavSection[] => {
88
- const routes = createAccountRoutes(overrides);
88
+ const routes = useCreateAccountRoutes(overrides);
89
89
  return [
90
90
  {
91
91
  title: "DASHBOARD",
@@ -1,3 +1,4 @@
1
+ import { createContext, useContext } from "react";
1
2
  import type { IconType } from "react-icons";
2
3
  import {
3
4
  MdFavoriteBorder as FavoriteBorder,
@@ -44,9 +45,42 @@ const defaults = {
44
45
  },
45
46
  } as const satisfies Record<string, AccountRoute>;
46
47
 
48
+ /**
49
+ * Context for overriding account route configuration.
50
+ * Allows parent providers to customize route paths and title keys
51
+ * without modifying the default route definitions.
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * <RoutesOverrideContext.Provider value={{ wishlists: { href: "/account/order-templates", titleKey: "orderTemplates" } }}>
56
+ * {children}
57
+ * </RoutesOverrideContext.Provider>
58
+ * ```
59
+ */
60
+ export const RoutesOverrideContext = createContext<
61
+ AccountRouteOverrides | undefined
62
+ >(undefined);
63
+
47
64
  export type AccountRouteKey = keyof typeof defaults;
48
65
 
49
- export const createAccountRoutes = (overrides?: AccountRouteOverrides) => ({
50
- ...defaults,
51
- wishlists: { ...defaults.wishlists, ...overrides?.wishlists },
52
- });
66
+ /**
67
+ * Hook that builds the account routes map, merging defaults with context-level
68
+ * and call-site overrides. Override priority (highest wins):
69
+ * 1. `overrides` parameter passed directly to the hook
70
+ * 2. Values from {@link RoutesOverrideContext}
71
+ * 3. Built-in defaults
72
+ *
73
+ * @param overrides - Optional route overrides applied at the call site
74
+ * @returns The full set of account routes with any overrides merged in
75
+ */
76
+ export const useCreateAccountRoutes = (overrides?: AccountRouteOverrides) => {
77
+ const ctxOverrides = useContext(RoutesOverrideContext);
78
+ return {
79
+ ...defaults,
80
+ wishlists: {
81
+ ...defaults.wishlists,
82
+ ...ctxOverrides?.wishlists,
83
+ ...overrides?.wishlists,
84
+ },
85
+ };
86
+ };
@@ -11,6 +11,7 @@ import type { CountryRegionInfo } from "@msdyn365-commerce/retail-proxy";
11
11
  import {
12
12
  Box,
13
13
  Button,
14
+ CircularProgress,
14
15
  FormControl,
15
16
  InputLabel,
16
17
  MenuItem,
@@ -25,7 +26,7 @@ import { withZodSchema } from "formik-validator-zod";
25
26
  import _ from "lodash";
26
27
  import { useRouter } from "next/navigation";
27
28
  import { useLocale, useTranslations } from "next-intl";
28
- import React, { createContext, useContext } from "react";
29
+ import React, { createContext, useContext, useRef } from "react";
29
30
  import { z } from "zod";
30
31
 
31
32
  interface AddressFormValues {
@@ -71,6 +72,7 @@ const AddressForm = ({ address, children }: RootProps) => {
71
72
 
72
73
  const { mutateAsync: createAddress } = useCreateAddress();
73
74
  const { mutateAsync: updateAddress } = useUpdateAddress();
75
+ const isSubmittingRef = useRef(false);
74
76
 
75
77
  const FormSchema = z.object({
76
78
  Name: z.string().nonempty(t("Validation.required", { field: t("name") })),
@@ -99,6 +101,9 @@ const AddressForm = ({ address, children }: RootProps) => {
99
101
  };
100
102
 
101
103
  const handleSubmit = async (values: AddressFormValues) => {
104
+ if (isSubmittingRef.current) return;
105
+ isSubmittingRef.current = true;
106
+
102
107
  let newAddress: Address = {
103
108
  Name: values.Name,
104
109
  Street: values.Street,
@@ -128,6 +133,7 @@ const AddressForm = ({ address, children }: RootProps) => {
128
133
  severity: "error",
129
134
  });
130
135
  console.log(error);
136
+ isSubmittingRef.current = false;
131
137
  }
132
138
  };
133
139
 
@@ -291,8 +297,20 @@ AddressForm.Actions = ({ children }: { children?: React.ReactNode }) => {
291
297
 
292
298
  AddressForm.SubmitButton = () => {
293
299
  const t = useTranslations("Address");
300
+ const ctx = useContext(FormContext);
301
+ const isSubmitting = ctx?.formik.isSubmitting ?? false;
294
302
  return (
295
- <Button type="submit" variant="contained" color="primary">
303
+ <Button
304
+ type="submit"
305
+ variant="contained"
306
+ color="primary"
307
+ disabled={isSubmitting}
308
+ startIcon={
309
+ isSubmitting ? (
310
+ <CircularProgress size={20} color="inherit" />
311
+ ) : undefined
312
+ }
313
+ >
296
314
  {t("save")}
297
315
  </Button>
298
316
  );
@@ -0,0 +1,31 @@
1
+ import TableRow from "@evenicanpm/storefront-core/src/pages/account/table-row";
2
+ import Box from "@mui/material/Box";
3
+ import Skeleton from "@mui/material/Skeleton";
4
+ import type { SxProps, Theme } from "@mui/material/styles";
5
+
6
+ interface TableRowSkeletonProps {
7
+ sx?: SxProps<Theme>;
8
+ }
9
+
10
+ export default function TableRowSkeleton({
11
+ sx = { gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr" },
12
+ }: TableRowSkeletonProps) {
13
+ return (
14
+ <TableRow sx={sx}>
15
+ <Skeleton variant="text" width="60%" />
16
+ <Box textAlign="center">
17
+ <Skeleton
18
+ variant="rounded"
19
+ width={70}
20
+ height={24}
21
+ sx={{ mx: "auto" }}
22
+ />
23
+ </Box>
24
+ <Skeleton variant="text" width="70%" sx={{ mx: { sm: "auto" } }} />
25
+ <Skeleton variant="text" width="50%" sx={{ mx: { sm: "auto" } }} />
26
+ <Box display={{ sm: "inline-flex", xs: "none" }} justifyContent="end">
27
+ <Skeleton variant="circular" width={32} height={32} />
28
+ </Box>
29
+ </TableRow>
30
+ );
31
+ }
@@ -16,6 +16,7 @@ import { format } from "date-fns";
16
16
  import { useTranslations } from "next-intl";
17
17
  import { createContext, type ReactNode, useContext } from "react";
18
18
  import { MdDelete as Delete } from "react-icons/md";
19
+ import { useCreateAccountRoutes } from "../account-routes";
19
20
 
20
21
  interface WishlistItemContextValue {
21
22
  wishlist: Wishlist;
@@ -73,13 +74,14 @@ const WishlistItem = ({ wishlist, children, sx }: RootProps) => {
73
74
  };
74
75
 
75
76
  WishlistItem.Name = () => {
77
+ const { wishlists: wishlistsRoute } = useCreateAccountRoutes();
76
78
  const ctx = useContext(WishlistItemContext);
77
79
  if (!ctx) return null;
78
80
  const { wishlist } = ctx;
79
81
  return (
80
82
  <Paragraph ellipsis>
81
83
  <NavLink3
82
- href={`/account/order-templates/${wishlist?.Id}`}
84
+ href={`${wishlistsRoute.href}/${wishlist?.Id}`}
83
85
  text={wishlist?.Name ?? ""}
84
86
  />
85
87
  </Paragraph>
@@ -7,7 +7,7 @@ import { useSuspenseQuery } from "@tanstack/react-query";
7
7
  import Link from "next/link";
8
8
  import { cmsQueryOptions } from "../../cms/queries";
9
9
  import { BorderShadowCard } from "../../components/BoxShadowCard";
10
- import { Display, Subtitle } from "../../components/Typography";
10
+ import { Display, Paragraph, Subtitle } from "../../components/Typography";
11
11
 
12
12
  interface Props {
13
13
  id: string;
@@ -29,6 +29,7 @@ export const BlogDetailView = ({ id, locale, cmsLocale }: Props) => {
29
29
  const title = blogData?.title;
30
30
  const shortDescription = blogData?.shortDescription;
31
31
  const content = blogData?.content;
32
+ const author = blogData?.author;
32
33
 
33
34
  return (
34
35
  <Container className="mt-2 mb-2" maxWidth="xl">
@@ -128,6 +129,7 @@ export const BlogDetailView = ({ id, locale, cmsLocale }: Props) => {
128
129
  </Grid>
129
130
  <Grid size={{ xs: 12, md: 3 }} order={{ xs: 1, md: 2 }}>
130
131
  <Box sx={{ paddingBottom: "15px" }}>
132
+ {author && <Paragraph sx={noSpaceSx}>{author}</Paragraph>}
131
133
  <Subtitle sx={noSpaceSx}>Published on: {publishDate}</Subtitle>
132
134
  </Box>
133
135
  </Grid>
@@ -416,13 +416,6 @@ CartItemRoot.Comment = () => {
416
416
  </FlexBox>
417
417
  </>
418
418
  );
419
- } else if (isLoading) {
420
- content = (
421
- <CircularProgress
422
- size={30}
423
- sx={{ marginTop: "auto", marginLeft: "auto" }}
424
- />
425
- );
426
419
  } else {
427
420
  content = (
428
421
  <>
@@ -448,6 +441,7 @@ CartItemRoot.Comment = () => {
448
441
  <Button
449
442
  color="primary"
450
443
  variant="outlined"
444
+ disabled={isLoading}
451
445
  sx={{ width: 150, mt: "auto", ml: "auto" }}
452
446
  onClick={() => setIsWritingComment(true)}
453
447
  >