@evenicanpm/storefront-core 2.2.0 → 2.3.1

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/package.json +7 -3
  2. package/src/api-manager/README.md +52 -27
  3. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
  4. package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
  6. package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
  7. package/src/api-manager/datasources/d365/d365-user.datasource.ts +125 -36
  8. package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
  9. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +10 -3
  10. package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
  11. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
  12. package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
  13. package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
  14. package/src/api-manager/index.ts +54 -3
  15. package/src/api-manager/lib/get-graphql-client.ts +11 -5
  16. package/src/api-manager/schemas/cart.schema.ts +10 -1
  17. package/src/api-manager/schemas/invoice.schema.ts +55 -0
  18. package/src/api-manager/schemas/order.schema.ts +13 -1
  19. package/src/api-manager/schemas/user.schema.ts +8 -0
  20. package/src/api-manager/services/create-query.ts +4 -10
  21. package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
  22. package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
  23. package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
  24. package/src/api-manager/services/order/queries/get-orders.ts +6 -3
  25. package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
  26. package/src/api-manager/types/Datasource.ts +21 -2
  27. package/src/auth/better-auth.ts +1 -1
  28. package/src/cms/blocks/block-manager.tsx +18 -3
  29. package/src/cms/blocks/components/cta-banner.tsx +65 -0
  30. package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
  31. package/src/cms/blocks/components/footer/index.tsx +9 -71
  32. package/src/cms/blocks/components/footer/interfaces.ts +4 -0
  33. package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
  34. package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
  35. package/src/cms/blocks/components/hero-image.tsx +83 -0
  36. package/src/cms/blocks/index.tsx +2 -0
  37. package/src/cms/blocks/interfaces.ts +20 -0
  38. package/src/cms/draft-mode-badge.tsx +26 -0
  39. package/src/cms/queries.ts +81 -0
  40. package/src/components/_tests_/site-logo.test.tsx +1 -1
  41. package/src/components/flex-box/flex-box.tsx +2 -0
  42. package/src/components/header/components/user.tsx +35 -11
  43. package/src/components/header/sticky-header.tsx +1 -0
  44. package/src/components/mini-cart/mini-cart.tsx +3 -3
  45. package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
  46. package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
  47. package/src/components/site-logo.tsx +1 -1
  48. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +1 -0
  49. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
  50. package/src/pages/account/account-navigation.tsx +88 -60
  51. package/src/pages/account/account-routes.ts +86 -0
  52. package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
  53. package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
  54. package/src/pages/account/orders/order-history-header.tsx +54 -0
  55. package/src/pages/account/orders/order-history-pagination.tsx +55 -0
  56. package/src/pages/account/orders/order-history-root.tsx +125 -0
  57. package/src/pages/account/orders/order-history-row.tsx +110 -0
  58. package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
  59. package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
  60. package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
  61. package/src/pages/account/orders/order-history-sort.tsx +90 -0
  62. package/src/pages/account/orders/order-history-table.tsx +54 -0
  63. package/src/pages/account/orders/order-row.tsx +2 -2
  64. package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
  65. package/src/pages/account/table-header.tsx +20 -0
  66. package/src/pages/account/table-row-skeleton.tsx +31 -0
  67. package/src/pages/account/wishlist/wishlist-item.tsx +4 -1
  68. package/src/pages/blog/blog-card.tsx +9 -3
  69. package/src/pages/blog/blog-detail-view.tsx +152 -0
  70. package/src/pages/blog/blog-list-view.tsx +59 -0
  71. package/src/pages/cart/estimate-shipping.tsx +6 -5
  72. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
  73. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
  74. package/src/pages/cms-page-view.tsx +53 -0
  75. package/src/pages/product-details/product-description.tsx +25 -13
  76. package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
  77. package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
  78. package/src/pages/product-details/product-tabs.tsx +20 -4
  79. package/src/pages/quickorder/order-upload.tsx +12 -6
  80. package/src/pages/quickorder/quick-order.tsx +25 -20
@@ -0,0 +1,81 @@
1
+ /**
2
+ * THIS IS A REACT-QUERY WRAPPER LAYER AROUND THE CMS SDK.
3
+ * IT PROVIDES TYPING AND STRUCTURE TO THE QUERIES USED THROUGHOUT THE APP, AND
4
+ * IT ALSO PROVIDES A SINGLE PLACE TO CONFIGURE QUERY OPTIONS FOR CMS DATA.
5
+ */
6
+ import { cms } from "@evenicanpm/cms";
7
+ import type { Blog } from "@evenicanpm/cms/src/content/blogs/types";
8
+ import type { Page } from "@evenicanpm/cms/src/content/pages/types";
9
+ import { queryOptions } from "@tanstack/react-query";
10
+ import type { StrapiResponse } from "strapi-sdk-js";
11
+
12
+ // strapi-sdk-js's StrapiResponseMetaPagination is missing pageCount, which Strapi
13
+ // includes when queries use withCount:true. This extends the meta to include it.
14
+ type StrapiPagedResponse<T> = StrapiResponse<T> & {
15
+ meta: {
16
+ pagination: {
17
+ page: number;
18
+ pageSize: number;
19
+ pageCount: number;
20
+ total: number;
21
+ };
22
+ };
23
+ };
24
+
25
+ export const cmsQueryKeys = {
26
+ navigation: () => ["cms", "navigation"] as const,
27
+ page: (
28
+ slug: string,
29
+ status: "draft" | "published" = "published",
30
+ locale?: string,
31
+ ) => ["cms", "pages", slug, status, locale] as const,
32
+ blogs: (page: number, pageSize: number, locale?: string) =>
33
+ ["cms", "blogs", page, pageSize, locale] as const,
34
+ blog: (id: string, locale?: string) => ["cms", "blog", id, locale] as const,
35
+ };
36
+
37
+ export const cmsQueryOptions = {
38
+ navigation: () =>
39
+ queryOptions({
40
+ queryKey: cmsQueryKeys.navigation(),
41
+ queryFn: (): Promise<unknown> => cms.navigation.getMainNavigation(),
42
+ }),
43
+ page: (
44
+ slug: string,
45
+ status: "draft" | "published" = "published",
46
+ locale?: string,
47
+ ) =>
48
+ queryOptions({
49
+ queryKey: cmsQueryKeys.page(slug, status, locale),
50
+ queryFn: (): Promise<StrapiResponse<Page[]>> =>
51
+ (
52
+ cms.pages.getPage as (opts: {
53
+ slug: string;
54
+ status?: "draft" | "published";
55
+ locale?: string;
56
+ }) => Promise<StrapiResponse<Page[]>>
57
+ )({ slug, status, locale }),
58
+ }),
59
+ blogs: (page: number, pageSize: number, locale?: string) =>
60
+ queryOptions({
61
+ queryKey: cmsQueryKeys.blogs(page, pageSize, locale),
62
+ queryFn: (): Promise<StrapiPagedResponse<Blog[]>> =>
63
+ (
64
+ cms.blogs.getBlogs as (opts: {
65
+ pagination: { page: number; pageSize: number };
66
+ locale?: string;
67
+ }) => Promise<StrapiPagedResponse<Blog[]>>
68
+ )({ pagination: { page, pageSize }, locale }),
69
+ }),
70
+ blog: (id: string, locale?: string) =>
71
+ queryOptions({
72
+ queryKey: cmsQueryKeys.blog(id, locale),
73
+ queryFn: (): Promise<StrapiResponse<Blog>> =>
74
+ (
75
+ cms.blogs.getBlog as (opts: {
76
+ id: string;
77
+ locale?: string;
78
+ }) => Promise<StrapiResponse<Blog>>
79
+ )({ id, locale }),
80
+ }),
81
+ };
@@ -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,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import Box, { type BoxProps } from "@mui/material/Box";
2
4
 
3
5
  /**
@@ -4,6 +4,7 @@ import useSessionInit from "@evenicanpm/storefront-core/src/api-manager/services
4
4
  import useSessionLogout from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-logout";
5
5
  // API hooks
6
6
  import useUpdateUser from "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user";
7
+ import getUser from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-user";
7
8
  import {
8
9
  getMsalInstance,
9
10
  initializeMsal,
@@ -12,6 +13,11 @@ import signOutCustom from "@evenicanpm/storefront-core/src/auth/signout";
12
13
  import MobileUserIcon from "@evenicanpm/storefront-core/src/components/icons/user";
13
14
  import { NavLink } from "@evenicanpm/storefront-core/src/components/nav-link";
14
15
  import T from "@evenicanpm/storefront-core/src/components/T";
16
+ import {
17
+ type AccountRoute,
18
+ type AccountRouteOverrides,
19
+ useCreateAccountRoutes,
20
+ } from "@evenicanpm/storefront-core/src/pages/account/account-routes";
15
21
  // UI Components
16
22
  import {
17
23
  Box,
@@ -21,15 +27,16 @@ import {
21
27
  ListItemButton,
22
28
  Popover,
23
29
  } from "@mui/material";
24
- import { useRouter } from "next/navigation";
30
+ import { genericOAuthClient } from "better-auth/client/plugins";
25
31
  // Auth components
26
32
  import { createAuthClient } from "better-auth/react";
27
- import { genericOAuthClient } from "better-auth/client/plugins";
33
+ import { useRouter } from "next/navigation";
28
34
 
29
35
  const authClient = createAuthClient({
30
36
  plugins: [genericOAuthClient()],
31
37
  });
32
38
  const { useSession: useBetterAuthSession } = authClient;
39
+
33
40
  import type React from "react";
34
41
  import {
35
42
  createContext,
@@ -46,17 +53,30 @@ interface IdTokenClaims {
46
53
  family_name?: string;
47
54
  }
48
55
 
49
- const MENU_ITEMS = [
50
- { href: "/account/profile", label: "Account.Menu.profile" },
51
- { href: "/account/addresses", label: "Account.Menu.addresses" },
52
- { href: "/account/orders", label: "Account.Menu.orders" },
53
- { href: "/account/wish-lists", label: "Account.Menu.wishlists" },
54
- ];
56
+ const routeToMenuItem = (route: AccountRoute) => ({
57
+ href: route.href,
58
+ label: `Account.Menu.${route.titleKey}`,
59
+ Icon: route.Icon,
60
+ count: route.count,
61
+ });
62
+
63
+ const buildMenuItems = (overrides?: AccountRouteOverrides) => {
64
+ const routes = useCreateAccountRoutes(overrides);
65
+ return [
66
+ routeToMenuItem(routes.profile),
67
+ routeToMenuItem(routes.orders),
68
+ routeToMenuItem(routes.wishlists),
69
+ routeToMenuItem(routes.invoices),
70
+ routeToMenuItem(routes.addresses),
71
+ ];
72
+ };
73
+
74
+ type MenuItem = { href: string; label: string };
55
75
 
56
76
  interface UserContextValue {
57
77
  isAuthenticated: boolean;
58
78
  status: string;
59
- menuItems: typeof MENU_ITEMS;
79
+ menuItems: MenuItem[];
60
80
  handleLogout: () => Promise<void>;
61
81
  handleSignIn: () => void;
62
82
  signOut: () => Promise<void>;
@@ -87,6 +107,8 @@ const User = ({ children }: Props) => {
87
107
  const { mutateAsync: sessionInit } = useSessionInit();
88
108
  const { mutateAsync: updateUser } = useUpdateUser();
89
109
  const { mutateAsync: sessionLogout } = useSessionLogout();
110
+ // Register the "me" query on the client so the hydrated cache entry has a queryFn for refetches
111
+ getUser.useData();
90
112
 
91
113
  // session init
92
114
  useEffect(() => {
@@ -143,10 +165,12 @@ const User = ({ children }: Props) => {
143
165
  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
144
166
  const open = Boolean(anchorEl);
145
167
 
168
+ const menuItems = buildMenuItems();
169
+
146
170
  const value: UserContextValue = {
147
171
  isAuthenticated,
148
172
  status,
149
- menuItems: MENU_ITEMS,
173
+ menuItems,
150
174
  handleLogout,
151
175
  handleSignIn,
152
176
  signOut: signOutCustom,
@@ -198,7 +222,7 @@ User.DesktopTrigger = function DesktopTrigger() {
198
222
  User.Menu = function Menu({
199
223
  children,
200
224
  }: {
201
- children?: (items: typeof MENU_ITEMS) => React.ReactNode;
225
+ children?: (items: MenuItem[]) => React.ReactNode;
202
226
  }) {
203
227
  const ctx = useContext(UserContext);
204
228
  if (!ctx) return null;
@@ -8,6 +8,7 @@ import { Sticky } from "@evenicanpm/ui";
8
8
  import useTheme from "@mui/material/styles/useTheme";
9
9
  import useMediaQuery from "@mui/material/useMediaQuery";
10
10
  import { useCallback, useState } from "react";
11
+ import { AccountRouteOverrides } from "../../pages/account/account-routes";
11
12
 
12
13
  /**
13
14
  * @category Implementation
@@ -121,8 +121,8 @@ MiniCart.Items = () => {
121
121
  } = ctx;
122
122
 
123
123
  return (
124
- <Box height={`calc(100vh - ${cart?.CartLines.length ? "207px" : "75px"})`}>
125
- {cart.CartLines.length > 0 ? (
124
+ <Box height={`calc(100vh - ${cart?.CartLines?.length ? "207px" : "75px"})`}>
125
+ {cart?.CartLines?.length > 0 ? (
126
126
  <Scrollbar>
127
127
  {cart?.CartLines?.map((item) => (
128
128
  <MiniCartItem
@@ -153,7 +153,7 @@ MiniCart.Actions = () => {
153
153
  const ctx = useContext(MiniCartContext);
154
154
  if (!ctx) return null;
155
155
  const { cart, currency, handleNavigate } = ctx;
156
- return cart?.CartLines.length > 0 ? (
156
+ return cart?.CartLines?.length > 0 ? (
157
157
  <BottomActions
158
158
  total={currency(cart?.TotalAmount || 0)}
159
159
  handleNavigate={handleNavigate}
@@ -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",
@@ -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 }}
@@ -13,6 +13,7 @@ import { createAuthClient } from "better-auth/react";
13
13
  import { useTranslations } from "next-intl";
14
14
 
15
15
  const { useSession } = createAuthClient();
16
+
16
17
  // ICONS
17
18
  import { MdFavorite } from "react-icons/md";
18
19
 
@@ -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>
@@ -9,6 +9,11 @@ import {
9
9
  Paragraph,
10
10
  Span,
11
11
  } from "@evenicanpm/storefront-core/src/components/Typography";
12
+ import {
13
+ type AccountRoute,
14
+ type AccountRouteOverrides,
15
+ useCreateAccountRoutes,
16
+ } from "@evenicanpm/storefront-core/src/pages/account/account-routes";
12
17
  // STYLED COMPONENTS
13
18
  import {
14
19
  MainContainer,
@@ -17,88 +22,114 @@ import {
17
22
  import { usePathname } from "next/navigation";
18
23
  import { useTranslations } from "next-intl";
19
24
  import { createContext, Fragment, type ReactNode } from "react";
20
- // ICON COMPONENTS
21
- import {
22
- MdFavoriteBorder as FavoriteBorder,
23
- MdPerson as Person,
24
- MdPlace as Place,
25
- MdOutlineShoppingBag as ShoppingBagOutlined,
26
- } from "react-icons/md";
25
+ import type { IconType } from "react-icons";
27
26
 
28
27
  /**
29
28
  *
30
29
  *** Extend menu:
31
30
  <AccountNavigation
32
- items={[
33
- ...DEFAULT_MENU_ITEMS,
34
- {
35
- title: "EXTRAS",
36
- list: [
37
- {
38
- href: "/account/rewards",
39
- title: "rewards",
40
- Icon: RewardsIcon,
41
- count: 5,
42
- },
43
- ],
44
- },
45
- ]}
31
+ items={[
32
+ ...DEFAULT_MENU_ITEMS,
33
+ {
34
+ title: "EXTRAS",
35
+ list: [
36
+ {
37
+ href: "/account/rewards",
38
+ title: "rewards",
39
+ Icon: RewardsIcon,
40
+ count: 5,
41
+ },
42
+ ],
43
+ },
44
+ ]}
46
45
  />;
47
46
 
48
47
  *** Override completely:
49
48
  <AccountNavigation>
50
- <AccountNavigation.Section title="Custom Section">
51
- <AccountNavigation.Item
52
- href="/custom"
53
- Icon={CustomIcon}
54
- label="Custom Page"
55
- />
56
- </AccountNavigation.Section>
49
+ <AccountNavigation.Section title="Custom Section">
50
+ <AccountNavigation.Item
51
+ href="/custom"
52
+ Icon={CustomIcon}
53
+ label="Custom Page"
54
+ />
55
+ </AccountNavigation.Section>
57
56
  </AccountNavigation>;
58
57
  */
59
58
 
60
- export const DEFAULT_MENU_ITEMS = [
61
- {
62
- title: "DASHBOARD",
63
- list: [
64
- {
65
- href: "/account/orders",
66
- title: "orders",
67
- Icon: ShoppingBagOutlined,
68
- count: 0,
69
- },
70
- { href: "/account/wish-lists", title: "wishlists", Icon: FavoriteBorder },
71
- ],
72
- },
73
- {
74
- title: "ACCOUNT SETTINGS",
75
- list: [
76
- { href: "/account/profile", title: "profile", Icon: Person },
77
- { href: "/account/addresses", title: "addresses", Icon: Place, count: 0 },
78
- ],
79
- },
80
- ];
59
+ interface NavItem {
60
+ href: string;
61
+ title: string;
62
+ Icon: IconType;
63
+ count?: number;
64
+ }
65
+
66
+ interface NavSection {
67
+ title: string;
68
+ list: NavItem[];
69
+ }
70
+
71
+ /**
72
+ *
73
+ * @param route
74
+ * @returns
75
+ */
76
+ const routeToNavItem = (route: AccountRoute): NavItem => {
77
+ return {
78
+ href: route.href,
79
+ title: route.titleKey,
80
+ Icon: route.Icon,
81
+ count: route.count,
82
+ };
83
+ };
84
+
85
+ export const DEFAULT_MENU_ITEMS = (
86
+ overrides?: AccountRouteOverrides,
87
+ ): NavSection[] => {
88
+ const routes = useCreateAccountRoutes(overrides);
89
+ return [
90
+ {
91
+ title: "DASHBOARD",
92
+ list: [
93
+ routeToNavItem(routes.orders),
94
+ routeToNavItem(routes.wishlists),
95
+ routeToNavItem(routes.invoices),
96
+ ],
97
+ },
98
+ {
99
+ title: "ACCOUNT SETTINGS",
100
+ list: [routeToNavItem(routes.profile), routeToNavItem(routes.addresses)],
101
+ },
102
+ ];
103
+ };
81
104
 
82
105
  interface AccountNavigationContextValue {
83
- items: typeof DEFAULT_MENU_ITEMS;
106
+ items: NavSection[];
84
107
  }
85
108
 
86
109
  const AccountNavigationContext =
87
110
  createContext<AccountNavigationContextValue | null>(null);
111
+
88
112
  interface AccountNavigationProps {
89
- items?: typeof DEFAULT_MENU_ITEMS;
113
+ items?: NavSection[];
90
114
  children?: ReactNode;
115
+ routeOverrides?: AccountRouteOverrides;
91
116
  }
92
117
 
93
- const AccountNavigation = ({ items, children }: AccountNavigationProps) => {
118
+ const AccountNavigation = ({
119
+ items,
120
+ children,
121
+ routeOverrides,
122
+ }: AccountNavigationProps) => {
94
123
  const pathname = usePathname();
95
124
  const t = useTranslations("Account.Menu");
96
125
 
97
126
  const { data: user } = getUser.useData();
98
127
  const { data: counts } = getUserCounts.useData(String(user?.RecordId) || "");
99
128
 
100
- // Merge default items with provided overrides
101
- const mergedItems = (items || DEFAULT_MENU_ITEMS).map((section) => ({
129
+ const defaultItems = DEFAULT_MENU_ITEMS(routeOverrides);
130
+
131
+ // Render either the props items or the default items
132
+ const itemsWCounts = (items || defaultItems).map((section) => ({
102
133
  ...section,
103
134
  list: section.list.map((item) => {
104
135
  if (item.href === "/account/orders") {
@@ -112,10 +143,10 @@ const AccountNavigation = ({ items, children }: AccountNavigationProps) => {
112
143
  }));
113
144
 
114
145
  return (
115
- <AccountNavigationContext.Provider value={{ items: mergedItems }}>
146
+ <AccountNavigationContext.Provider value={{ items: itemsWCounts }}>
116
147
  <MainContainer>
117
148
  {children ||
118
- mergedItems.map((section) => (
149
+ itemsWCounts.map((section) => (
119
150
  <AccountNavigation.Section
120
151
  key={section.title}
121
152
  title={section.title}
@@ -151,10 +182,7 @@ AccountNavigation.Section = ({ title, children }: SectionProps) => {
151
182
  );
152
183
  };
153
184
 
154
- interface ItemProps {
155
- href: string;
156
- Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
157
- count?: number;
185
+ interface ItemProps extends Pick<NavItem, "href" | "Icon" | "count"> {
158
186
  label: string;
159
187
  isActive?: boolean;
160
188
  }
@@ -0,0 +1,86 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { IconType } from "react-icons";
3
+ import {
4
+ MdFavoriteBorder as FavoriteBorder,
5
+ MdDocumentScanner as InvoicesIcon,
6
+ MdPerson as Person,
7
+ MdPlace as Place,
8
+ MdOutlineShoppingBag as ShoppingBagOutlined,
9
+ } from "react-icons/md";
10
+
11
+ export interface AccountRoute {
12
+ href: string;
13
+ titleKey: string;
14
+ Icon: IconType;
15
+ count?: number;
16
+ }
17
+
18
+ export interface AccountRouteOverrides {
19
+ wishlists?: { href?: string; titleKey?: string };
20
+ }
21
+
22
+ const defaults = {
23
+ orders: {
24
+ href: "/account/orders",
25
+ titleKey: "orders",
26
+ Icon: ShoppingBagOutlined,
27
+ count: 0,
28
+ },
29
+ wishlists: {
30
+ href: "/account/wish-lists",
31
+ titleKey: "wishlists",
32
+ Icon: FavoriteBorder,
33
+ },
34
+ invoices: {
35
+ href: "/account/invoices",
36
+ titleKey: "invoices",
37
+ Icon: InvoicesIcon,
38
+ },
39
+ profile: { href: "/account/profile", titleKey: "profile", Icon: Person },
40
+ addresses: {
41
+ href: "/account/addresses",
42
+ titleKey: "addresses",
43
+ Icon: Place,
44
+ count: 0,
45
+ },
46
+ } as const satisfies Record<string, AccountRoute>;
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
+
64
+ export type AccountRouteKey = keyof typeof defaults;
65
+
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
+ };
@@ -0,0 +1,26 @@
1
+ export enum FilterSourceType {
2
+ Origin = "Origin",
3
+ CreatedDate = "CreatedDate",
4
+ OrderStatus = "OrderStatus",
5
+ }
6
+
7
+ export interface OriginFilter {
8
+ sourceType: FilterSourceType.Origin;
9
+ label: string;
10
+ value: number;
11
+ }
12
+
13
+ export interface CreatedDateFilter {
14
+ sourceType: FilterSourceType.CreatedDate;
15
+ label: string;
16
+ startValue?: string;
17
+ endValue?: string;
18
+ }
19
+
20
+ export interface OrderStatusFilter {
21
+ sourceType: FilterSourceType.OrderStatus;
22
+ label: string;
23
+ value: number;
24
+ }
25
+
26
+ export type FilterOption = OriginFilter | CreatedDateFilter | OrderStatusFilter;