@evenicanpm/storefront-core 1.0.0 → 1.0.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 (133) hide show
  1. package/package.json +3 -2
  2. package/src/api-manager/datasources/d365/d365-address.datasource.ts +1 -1
  3. package/src/api-manager/datasources/d365/d365-user.datasource.ts +0 -19
  4. package/src/api-manager/datasources/e4/e4-cart.datasource.ts +2 -2
  5. package/src/api-manager/datasources/e4/e4-product.datasource.ts +19 -4
  6. package/src/api-manager/datasources/e4/e4.datasource.ts +1 -2
  7. package/src/api-manager/datasources/e4/e4.remaps.ts +24 -0
  8. package/src/api-manager/datasources/e4/e4.translator.ts +17 -0
  9. package/src/api-manager/datasources/e4/{e4-user.datasource.ts → user/e4-user.datasource.ts} +5 -7
  10. package/src/api-manager/datasources/e4/user/e4-user.remaps.ts +4 -0
  11. package/src/api-manager/datasources/e4/user/e4-user.translator.ts +10 -0
  12. package/src/api-manager/index.ts +21 -6
  13. package/src/api-manager/schemas/product.schema.ts +1 -1
  14. package/src/api-manager/services/address/queries/get-states.ts +1 -1
  15. package/src/api-manager/services/create-query.ts +10 -3
  16. package/src/api-manager/types/Datasource.ts +9 -9
  17. package/src/components/Blocks/Components/ProductCarousel/index.tsx +6 -6
  18. package/src/components/_components/navbar/category-based-menu/components/categories.tsx +1 -1
  19. package/src/components/_components/navbar/mega-menu/mega-menu.tsx +1 -1
  20. package/src/components/_components/navbar/nav-list/nav-item-child.tsx +2 -2
  21. package/src/components/_components/product-dimensions/product-dimensions.tsx +114 -0
  22. package/src/components/_components/product-quantity-buttons/product-quantity-buttons.tsx +88 -0
  23. package/src/components/_components/section-header/section-header.tsx +1 -1
  24. package/src/components/_components/settings/setting.tsx +1 -1
  25. package/src/components/_components/wishlist-modal/TODO.md +10 -0
  26. package/src/components/_components/wishlist-modal/add-to-wishlist-button.tsx +68 -0
  27. package/src/components/_components/wishlist-modal/add-to-wishlist-dialog.tsx +161 -0
  28. package/src/components/_components/wishlist-modal/remove-from-wishlist-dialog.tsx +174 -0
  29. package/src/components/_components/wishlist-modal/wishlist-modal-item.tsx +65 -0
  30. package/src/components/countries-input.tsx +1 -0
  31. package/src/hooks/use-cart.tsx +82 -0
  32. package/src/hooks/use-variants.tsx +85 -0
  33. package/src/pages/account/addresses/address-form.tsx +289 -0
  34. package/src/pages/account/addresses/address-item.tsx +104 -0
  35. package/src/pages/account/dashboard-header.tsx +123 -0
  36. package/src/pages/account/navigation.tsx +84 -0
  37. package/src/pages/account/no-records.tsx +20 -0
  38. package/src/pages/account/orders/icons/delivery.tsx +14 -0
  39. package/src/pages/account/orders/icons/package-box.tsx +13 -0
  40. package/src/pages/account/orders/icons/truck-filled.tsx +14 -0
  41. package/src/pages/account/orders/order-progress.tsx +111 -0
  42. package/src/pages/account/orders/order-row.tsx +61 -0
  43. package/src/pages/account/orders/order-summary.tsx +83 -0
  44. package/src/pages/account/orders/ordered-products.tsx +114 -0
  45. package/src/pages/account/profile/profile-button.test.tsx +59 -0
  46. package/src/pages/account/profile/profile-button.tsx +50 -0
  47. package/src/pages/account/profile/user-info.tsx +59 -0
  48. package/src/pages/account/styles.ts +32 -0
  49. package/src/pages/account/table-row.tsx +19 -0
  50. package/src/pages/account/wishlist/create-new-list.tsx +167 -0
  51. package/src/pages/account/wishlist/create-wishlist-button.tsx +40 -0
  52. package/src/pages/account/wishlist/wishlist-item.tsx +82 -0
  53. package/src/pages/blog/pagination.tsx +38 -0
  54. package/src/pages/cart/cart-item.tsx +312 -0
  55. package/src/pages/cart/checkout-form.tsx +122 -0
  56. package/src/pages/cart/coupon-entry.tsx +90 -0
  57. package/src/pages/cart/estimate-shipping.tsx +183 -0
  58. package/src/pages/cart/wrapper.tsx +30 -0
  59. package/src/pages/checkout/checkout-alt-form/address-card.tsx +73 -0
  60. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +133 -0
  61. package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +120 -0
  62. package/src/pages/checkout/checkout-alt-form/customer-information.tsx +94 -0
  63. package/src/pages/checkout/checkout-alt-form/delivery-address.tsx +170 -0
  64. package/src/pages/checkout/checkout-alt-form/delivery-date.tsx +114 -0
  65. package/src/pages/checkout/checkout-alt-form/delivery-options.tsx +186 -0
  66. package/src/pages/checkout/checkout-alt-form/edit-address-form.tsx +130 -0
  67. package/src/pages/checkout/checkout-alt-form/heading.tsx +50 -0
  68. package/src/pages/checkout/checkout-alt-form/index.ts +1 -0
  69. package/src/pages/checkout/checkout-alt-form/new-address-form.tsx +294 -0
  70. package/src/pages/checkout/checkout-alt-form/node.tsx +9 -0
  71. package/src/pages/checkout/checkout-alt-form/payment-details.tsx +344 -0
  72. package/src/pages/checkout/checkout-alt-form/safe-card-preview-data.tsx +36 -0
  73. package/src/pages/checkout/checkout-alt-form/types.ts +20 -0
  74. package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +39 -0
  75. package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +40 -0
  76. package/src/pages/checkout/checkout-alt-summary/index.ts +1 -0
  77. package/src/pages/checkout/checkout-alt-summary/list-item.tsx +31 -0
  78. package/src/pages/confirmation/address.tsx +22 -0
  79. package/src/pages/confirmation/confirmation-summary.tsx +52 -0
  80. package/src/pages/confirmation/ordered-products.tsx +108 -0
  81. package/src/pages/product-details/available-shops.tsx +48 -0
  82. package/src/pages/product-details/bopis/find-in-store-button.tsx +63 -0
  83. package/src/pages/product-details/bopis/find-in-store-modal.tsx +257 -0
  84. package/src/pages/product-details/bopis/pickup-option-select.tsx +127 -0
  85. package/src/pages/product-details/bopis/search-header.tsx +69 -0
  86. package/src/pages/product-details/checkbox-label.tsx +20 -0
  87. package/src/pages/product-details/currency.ts +64 -0
  88. package/src/pages/product-details/frequently-bought.tsx +90 -0
  89. package/src/pages/product-details/frequently-product-card.tsx +60 -0
  90. package/src/pages/product-details/product-comment.tsx +44 -0
  91. package/src/pages/product-details/product-description.tsx +22 -0
  92. package/src/pages/product-details/product-filter-card.tsx +257 -0
  93. package/src/pages/product-details/product-intro/product-intro-images.tsx +87 -0
  94. package/src/pages/product-details/product-intro/product-intro.tsx +250 -0
  95. package/src/pages/product-details/product-review.tsx +131 -0
  96. package/src/pages/product-details/product-tabs.tsx +51 -0
  97. package/src/pages/product-details/related-products.tsx +42 -0
  98. package/src/pages/product-details/types.ts +11 -0
  99. package/src/pages/product-list/breadcrumbs.tsx +39 -0
  100. package/src/pages/product-list/checkbox-label.tsx +20 -0
  101. package/src/pages/product-list/facet-group.tsx +125 -0
  102. package/src/pages/product-list/facet.tsx +36 -0
  103. package/src/pages/product-list/list-filter.tsx +40 -0
  104. package/src/pages/product-list/pagination.tsx +80 -0
  105. package/src/pages/product-list/product-list-context.tsx +302 -0
  106. package/src/pages/product-list/product-list-state.ts +187 -0
  107. package/src/pages/product-list/product-rating.tsx +16 -0
  108. package/src/pages/product-list/quick-view-dialog-content.tsx +205 -0
  109. package/src/pages/product-list/quick-view-dialog.tsx +54 -0
  110. package/src/pages/product-list/range-filter.tsx +125 -0
  111. package/src/pages/product-list/scrollbar.tsx +38 -0
  112. package/src/pages/product-list/search-bar.tsx +162 -0
  113. package/src/pages/product-list/selected-facets.tsx +80 -0
  114. package/src/pages/product-list/side-nav.tsx +49 -0
  115. package/src/pages/product-list/swatch.tsx +80 -0
  116. package/src/pages/product-list/types.ts +18 -0
  117. package/src/pages/product-list/use-product-list.ts +12 -0
  118. package/src/pages/product-list/useCategoryFilter.tsx +26 -0
  119. package/src/pages/product-list/utils/facet-helpers.ts +5 -0
  120. package/src/pages/product-list/utils/generate-breadcrumbs.ts +30 -0
  121. package/src/pages/product-list/utils/getCategoryFilterFromSlug.ts +153 -0
  122. package/src/pages/product-list/utils/product-list-helper.ts +111 -0
  123. package/src/pages/product-list/utils/product-list-types.ts +41 -0
  124. package/src/pages/product-list/utils/search-for-category.ts +76 -0
  125. package/src/pages/product-list/utils/sort-options.ts +44 -0
  126. package/src/pages/product-list/utils/use-previous-refiners.ts +14 -0
  127. package/src/pages/quickorder/order-upload.tsx +150 -0
  128. package/src/pages/quickorder/quick-order-form.tsx +343 -0
  129. package/src/pages/quickorder/quick-order-row.tsx +144 -0
  130. package/tsconfig.json +1 -0
  131. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +0 -10464
  132. /package/src/{components/_components/hooks/useOverflowDetect.ts → hooks/use-overflow-detect.ts} +0 -0
  133. /package/src/{components/_components/hooks/useSettings.ts → hooks/use-settings.ts} +0 -0
@@ -0,0 +1,49 @@
1
+ import { useEffect, useState, ReactNode } from "react";
2
+ import Drawer from "@mui/material/Drawer";
3
+ // GLOBAL CUSTOM COMPONENT
4
+ import Scrollbar from "./scrollbar";
5
+
6
+ // ================================================================
7
+ interface Props {
8
+ open?: boolean;
9
+ width?: number;
10
+ children: ReactNode;
11
+ toggle?: () => void;
12
+ position?: "left" | "right";
13
+ handler: (event: () => void) => ReactNode;
14
+ }
15
+ // ================================================================
16
+
17
+ export default function SideNav(props: Props) {
18
+ const {
19
+ position = "left",
20
+ open = false,
21
+ width = 280,
22
+ children,
23
+ handler,
24
+ toggle,
25
+ } = props;
26
+
27
+ const [sideNavOpen, setSideNavOpen] = useState(open);
28
+ const handleToggleSideNav = () => setSideNavOpen(!sideNavOpen);
29
+
30
+ useEffect(() => setSideNavOpen(open), [open]);
31
+
32
+ const handleClose = toggle || handleToggleSideNav;
33
+
34
+ return (
35
+ <div>
36
+ <Drawer
37
+ anchor={position}
38
+ open={sideNavOpen}
39
+ onClose={handleClose}
40
+ SlideProps={{ style: { width } }}
41
+ sx={{ zIndex: 15001 }}
42
+ >
43
+ <Scrollbar autoHide={false}>{children}</Scrollbar>
44
+ </Drawer>
45
+
46
+ {handler(handleClose)}
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { Box } from "@mui/material";
4
+ import { ProductRefinerValue } from "@msdyn365-commerce/retail-proxy";
5
+ import { Chip, useTheme } from "@mui/material";
6
+ import { RenderedFacetProps } from "./facet-group";
7
+
8
+ type Props = RenderedFacetProps & NonNullable<unknown>;
9
+
10
+ const cssColorName = (color: string | undefined) => {
11
+ if (!color) return false;
12
+ const cssColorName = new Option().style;
13
+ cssColorName.color = color;
14
+ return cssColorName.color !== "";
15
+ };
16
+
17
+ const checkWhite = (color: string | undefined) => {
18
+ return color?.trim().toLowerCase() === "white";
19
+ };
20
+
21
+ export default function Swatch({
22
+ facet,
23
+ handleRefinerSelect,
24
+ isRefinerSelected,
25
+ }: Props) {
26
+ const theme = useTheme();
27
+
28
+ return (
29
+ <Box mb={3}>
30
+ {facet.Values?.map((colorItem: ProductRefinerValue) => {
31
+ return (
32
+ <Box
33
+ key={colorItem.LeftValueBoundString}
34
+ display="inline-block"
35
+ mr={3}
36
+ sx={{ textAlign: "center" }}
37
+ >
38
+ <Chip
39
+ clickable={true}
40
+ key={colorItem.LeftValueBoundString}
41
+ onClick={() => handleRefinerSelect(colorItem)}
42
+ sx={{
43
+ color: "black",
44
+ borderRadius: "100%",
45
+ backgroundColor:
46
+ colorItem.SwatchColorHexCode ||
47
+ (cssColorName(colorItem.LeftValueBoundString)
48
+ ? colorItem.LeftValueBoundString?.toLowerCase()
49
+ : theme.palette.grey[300]),
50
+ width: 32,
51
+ height: 32,
52
+ border: checkWhite(colorItem.LeftValueBoundString)
53
+ ? "1px solid black"
54
+ : isRefinerSelected(colorItem)
55
+ ? "2px solid"
56
+ : "2px solid transparent",
57
+ "&:hover": {
58
+ backgroundColor:
59
+ colorItem.SwatchColorHexCode ||
60
+ (cssColorName(colorItem.LeftValueBoundString)
61
+ ? colorItem.LeftValueBoundString?.toLowerCase()
62
+ : theme.palette.grey[300]),
63
+ },
64
+ ".MuiChip-label": {
65
+ overflow: "visible",
66
+ },
67
+ }}
68
+ label={
69
+ !colorItem.SwatchColorHexCode &&
70
+ (colorItem.LeftValueBoundString || "0")[0].toUpperCase()
71
+ }
72
+ title={colorItem.LeftValueBoundString}
73
+ />
74
+ <p>{colorItem.LeftValueBoundString}</p>
75
+ </Box>
76
+ );
77
+ })}
78
+ </Box>
79
+ );
80
+ }
@@ -0,0 +1,18 @@
1
+ export interface ProductFilters {
2
+ brand: string[];
3
+ color: string[];
4
+ sales: string[];
5
+ price: number[];
6
+ rating: number;
7
+ }
8
+
9
+ export type ProductFilterKeys = keyof ProductFilters;
10
+ export type ProductFilterValues = ProductFilters[ProductFilterKeys];
11
+
12
+ export type SortOption = {
13
+ field: string;
14
+ isDefault?: boolean;
15
+ isDescending: boolean;
16
+ displayName: string;
17
+ defaultForSearch?: boolean;
18
+ };
@@ -0,0 +1,12 @@
1
+ import { useContext } from "react";
2
+ import { ProductListContext } from "./product-list-context";
3
+
4
+ const useProductList = () => {
5
+ const context = useContext(ProductListContext);
6
+ if (!context) {
7
+ throw new Error("Context must be used within a ProductListProvider");
8
+ }
9
+ return context;
10
+ };
11
+
12
+ export default useProductList;
@@ -0,0 +1,26 @@
1
+ import { notFound } from "next/navigation";
2
+
3
+ export type Category = {
4
+ id: string;
5
+ parentId: string | null;
6
+ seoCrumb?: string | null;
7
+ };
8
+
9
+ export function getE4CategoryIdFromSlug(
10
+ slug: string[],
11
+ categories: Category[],
12
+ ): string {
13
+ if (!slug || slug.length === 0) {
14
+ const home = categories.find(
15
+ (cat) => cat.seoCrumb?.toLowerCase() === "home",
16
+ );
17
+ return home ? home.id : "1";
18
+ }
19
+ const slugStr = slug.join("/").toLowerCase();
20
+ const found = categories.find(
21
+ (cat) => cat.seoCrumb?.toLowerCase() === slugStr,
22
+ );
23
+ if (found) return found.id;
24
+ notFound();
25
+ return "";
26
+ }
@@ -0,0 +1,5 @@
1
+ const isRefinerSelected = () => {
2
+ return false;
3
+ };
4
+
5
+ export { isRefinerSelected };
@@ -0,0 +1,30 @@
1
+ import { CategoryHierarchy } from "@/lib/category-helpers";
2
+ import { searchCategory } from "./search-for-category";
3
+
4
+ const generateBreadcrumbs = (
5
+ selectedCategory: CategoryHierarchy | undefined,
6
+ categoryHierarchy: CategoryHierarchy[],
7
+ locale: string,
8
+ ) => {
9
+ const breadcrumbs: CategoryHierarchy[] = [];
10
+ if (selectedCategory) {
11
+ breadcrumbs.push(selectedCategory);
12
+ }
13
+ let nextCategory: CategoryHierarchy | undefined =
14
+ selectedCategory ?? undefined;
15
+ while (nextCategory) {
16
+ nextCategory = searchCategory(
17
+ locale,
18
+ categoryHierarchy,
19
+ nextCategory?.ParentCategory,
20
+ );
21
+ if (nextCategory) {
22
+ breadcrumbs.push(nextCategory);
23
+ } else {
24
+ break;
25
+ }
26
+ }
27
+ return breadcrumbs.reverse();
28
+ };
29
+
30
+ export default generateBreadcrumbs;
@@ -0,0 +1,153 @@
1
+ export type Category = {
2
+ id?: string;
3
+ RecordId?: number;
4
+ parentId?: string | null;
5
+ ParentCategory?: number;
6
+ seoCrumb?: string | null;
7
+ Url?: string;
8
+ Name?: string;
9
+ NeutralizedNAME?: string;
10
+ Children?: Category[];
11
+ };
12
+
13
+ export function getCategoryIdFromSlug(
14
+ slug: string[],
15
+ categories: Category[],
16
+ isD365: boolean,
17
+ ): string | undefined {
18
+ if (!slug || slug.length === 0 || !categories || categories.length === 0) {
19
+ return undefined;
20
+ }
21
+
22
+ if (slug.length > 1) {
23
+ const parentSlug = slug[0]?.toLowerCase();
24
+ const childSlug = slug[1]?.toLowerCase();
25
+
26
+ const parentCategory = categories.find((cat) => {
27
+ const url = cat?.Url?.toLowerCase() || "";
28
+ const name = cat?.Name?.toLowerCase() || "";
29
+ const neutralName = cat?.NeutralizedNAME?.toLowerCase() || "";
30
+ const seoCrumb = cat?.seoCrumb?.toLowerCase() || "";
31
+ return (
32
+ url.includes(parentSlug) ||
33
+ name === parentSlug ||
34
+ neutralName === parentSlug ||
35
+ seoCrumb === parentSlug
36
+ );
37
+ });
38
+
39
+ if (!parentCategory) {
40
+ console.warn(
41
+ "getCategoryIdFromSlug: No parent category found for slug:",
42
+ parentSlug,
43
+ );
44
+ return undefined;
45
+ }
46
+
47
+ const childMatch = parentCategory.Children?.find((cat) => {
48
+ const url = cat?.Url?.toLowerCase() || "";
49
+ const name = cat?.Name?.toLowerCase() || "";
50
+ const neutralName = cat?.NeutralizedNAME?.toLowerCase() || "";
51
+ const seoCrumb = cat?.seoCrumb?.toLowerCase() || "";
52
+ return (
53
+ url.includes(childSlug) ||
54
+ name === childSlug ||
55
+ neutralName === childSlug ||
56
+ seoCrumb === childSlug
57
+ );
58
+ });
59
+ if (childMatch) {
60
+ return isD365
61
+ ? childMatch.RecordId?.toString()
62
+ : childMatch.id?.toString();
63
+ }
64
+
65
+ return undefined;
66
+ }
67
+
68
+ const slugValue = slug[0]?.toLowerCase();
69
+
70
+ const topLevelMatch = categories.find((cat) => {
71
+ const url = cat?.Url?.toLowerCase() || "";
72
+ const name = cat?.Name?.toLowerCase() || "";
73
+ const neutralName = cat?.NeutralizedNAME?.toLowerCase() || "";
74
+ const seoCrumb = cat?.seoCrumb?.toLowerCase() || "";
75
+ return (
76
+ url.includes(slugValue) ||
77
+ name === slugValue ||
78
+ neutralName === slugValue ||
79
+ seoCrumb === slugValue
80
+ );
81
+ });
82
+
83
+ if (topLevelMatch) {
84
+ return isD365
85
+ ? topLevelMatch.RecordId?.toString()
86
+ : topLevelMatch.id?.toString();
87
+ }
88
+
89
+ let childMatch: Category | undefined;
90
+ for (const topLevelCat of categories) {
91
+ if (topLevelCat.Children && topLevelCat.Children.length > 0) {
92
+ const child = topLevelCat.Children.find((cat) => {
93
+ const url = cat?.Url?.toLowerCase() || "";
94
+ const name = cat?.Name?.toLowerCase() || "";
95
+ const neutralName = cat?.NeutralizedNAME?.toLowerCase() || "";
96
+ const seoCrumb = cat?.seoCrumb?.toLowerCase() || "";
97
+ return (
98
+ url.includes(slugValue) ||
99
+ name === slugValue ||
100
+ neutralName === slugValue ||
101
+ seoCrumb === slugValue
102
+ );
103
+ });
104
+ if (child) {
105
+ childMatch = child;
106
+ break;
107
+ }
108
+ }
109
+ }
110
+
111
+ if (childMatch) {
112
+ return isD365 ? childMatch.RecordId?.toString() : childMatch.id?.toString();
113
+ }
114
+
115
+ return undefined;
116
+ }
117
+
118
+ export function getCategoryFilterFromSlug(
119
+ slug: string[],
120
+ categories: Category[],
121
+ ): string {
122
+ if (!slug || slug.length === 0) {
123
+ return "";
124
+ }
125
+
126
+ const topLevelSlugs = ["men", "women", "accessories"];
127
+
128
+ if (slug.length === 1 && topLevelSlugs.includes(slug[0].toLowerCase())) {
129
+ const topCategory = categories.find((cat) => {
130
+ const seoCrumb =
131
+ cat?.seoCrumb?.toLowerCase() ||
132
+ cat?.Url?.toLowerCase() ||
133
+ cat?.Name?.toLowerCase();
134
+ return seoCrumb === slug[0].toLowerCase();
135
+ });
136
+ if (topCategory) {
137
+ const children = categories.filter((cat) => {
138
+ const parentIdMatch =
139
+ cat.parentId === topCategory.id ||
140
+ cat.ParentCategory === topCategory.RecordId;
141
+ return parentIdMatch && (cat.seoCrumb || cat.Url || cat.Name);
142
+ });
143
+ if (children.length > 0) {
144
+ return children
145
+ .map((child) => (child.seoCrumb || child.Url || child.Name) as string)
146
+ .join("|");
147
+ }
148
+ }
149
+ return slug[0];
150
+ } else {
151
+ return slug.join("/");
152
+ }
153
+ }
@@ -0,0 +1,111 @@
1
+ import { IQueryResultSettings } from "@msdyn365-commerce/retail-proxy";
2
+ import { PageKey, SortOption } from "./product-list-types";
3
+ import getSortOptions from "./sort-options";
4
+ import { convertParameterToRefiner } from "@/lib/refiner-utils";
5
+ import { SearchByCriteriaInput } from "@/api-manager/schemas/product.schema";
6
+
7
+ const getDefaultValueByPageKey = (pageKey: PageKey) => {
8
+ switch (pageKey) {
9
+ case PageKey.TOP:
10
+ return 20;
11
+ case PageKey.PAGE:
12
+ return 1;
13
+ case PageKey.REFINEMENT:
14
+ case PageKey.SEARCH:
15
+ return "";
16
+ case PageKey.VIEW:
17
+ return "grid";
18
+ default:
19
+ return undefined;
20
+ }
21
+ };
22
+
23
+ const getSelectedSortOption = (
24
+ sortOptions: SortOption[],
25
+ isSearch: boolean,
26
+ sortCode?: string,
27
+ ) => {
28
+ let sortOption: SortOption | undefined;
29
+ if (sortCode) {
30
+ sortOption = sortOptions.find((s) => s.code === sortCode);
31
+ } else {
32
+ sortOption = sortOptions.find((s) =>
33
+ isSearch ? s.defaultForSearch : s.isDefault,
34
+ );
35
+ }
36
+ if (!sortOption) {
37
+ sortOption = sortOptions[0];
38
+ }
39
+ return sortOption;
40
+ };
41
+
42
+ const getCategoryFromSlug = (slug: string[]) => {
43
+ if (Array.isArray(slug)) {
44
+ const categoryId = Number(slug.pop());
45
+ if (!isNaN(categoryId)) {
46
+ return categoryId;
47
+ }
48
+ }
49
+ return undefined;
50
+ };
51
+
52
+ const calculatePage = (searchState: IQueryResultSettings): number => {
53
+ const skip = searchState?.Paging?.Skip ?? 0;
54
+ const top = searchState?.Paging?.Top ?? 20;
55
+
56
+ return skip === 0 ? 1 : skip / top + 1;
57
+ };
58
+
59
+ const calculateSkip = (page: number, top: number) => (page - 1) * top;
60
+
61
+ const buildInitialState = (
62
+ params: string[],
63
+ top: number,
64
+ page: number,
65
+ sort: string,
66
+ refiner: string,
67
+ searchTerm: string | undefined,
68
+ ) => {
69
+ const categoryId = getCategoryFromSlug(params);
70
+
71
+ const selectedSort = getSelectedSortOption(
72
+ getSortOptions(),
73
+ !!searchTerm,
74
+ sort || undefined,
75
+ );
76
+ const state: SearchByCriteriaInput = {
77
+ searchState: {
78
+ queryResultSettings: {
79
+ Paging: {
80
+ Top: top,
81
+ Skip: calculateSkip(page, top),
82
+ },
83
+ Sorting: {
84
+ Columns:
85
+ (selectedSort && [
86
+ {
87
+ ColumnName: selectedSort?.field,
88
+ IsDescending: selectedSort?.isDescending,
89
+ },
90
+ ]) ||
91
+ [],
92
+ },
93
+ },
94
+ refinement: refiner
95
+ ? refiner.split("||").map((r) => convertParameterToRefiner(r))
96
+ : [],
97
+ searchTerm: searchTerm,
98
+ },
99
+ categoryId: categoryId,
100
+ };
101
+ return state;
102
+ };
103
+
104
+ export {
105
+ getDefaultValueByPageKey,
106
+ getSelectedSortOption,
107
+ getCategoryFromSlug,
108
+ calculatePage,
109
+ calculateSkip,
110
+ buildInitialState,
111
+ };
@@ -0,0 +1,41 @@
1
+ import { ProductRefinerValue } from "@msdyn365-commerce/retail-proxy";
2
+
3
+ export const PageKey = {
4
+ TOP: "perPage",
5
+ PAGE: "page",
6
+ REFINEMENT: "refiner",
7
+ VIEW: "view",
8
+ SORT: "sort",
9
+ SEARCH: "search",
10
+ } as const;
11
+
12
+ export type ProductListSearchParams = {
13
+ perPage?: typeof PageKey.TOP;
14
+ page?: typeof PageKey.PAGE;
15
+ refiner: typeof PageKey.REFINEMENT;
16
+ search: typeof PageKey.SEARCH;
17
+ sort: typeof PageKey.SORT;
18
+ };
19
+
20
+ export type PageKey = (typeof PageKey)[keyof typeof PageKey];
21
+
22
+ export type ViewMode = "grid" | "list";
23
+
24
+ export type ProductListParamHandlers = {
25
+ handlePageChange: (page: number) => void;
26
+ handlePerPageChange: (perPage: number) => void;
27
+ handleRefinerChange: (refiner: ProductRefinerValue) => void;
28
+ handleSortChange: (sort: SortOption) => void;
29
+ handleViewChange: (view: ViewMode) => void;
30
+ handleSearchChange: (searchTerm: string) => void;
31
+ handleRefinersClear?: () => void;
32
+ };
33
+
34
+ export type SortOption = {
35
+ field: string;
36
+ code: string;
37
+ isDefault?: boolean;
38
+ isDescending: boolean;
39
+ displayName: string;
40
+ defaultForSearch?: boolean;
41
+ };
@@ -0,0 +1,76 @@
1
+ import { CategoryHierarchy } from "@/lib/category-helpers";
2
+ import { PageSlug } from "@/lib/page-slugs";
3
+ import { TextValueTranslation } from "@msdyn365-commerce/retail-proxy";
4
+
5
+ const searchCategory = (
6
+ locale: string,
7
+ categoryHierarchy: CategoryHierarchy[],
8
+ categoryId?: number,
9
+ categoryName?: string,
10
+ ): CategoryHierarchy | undefined => {
11
+ let foundCategory;
12
+ if (!Array.isArray(categoryHierarchy)) {
13
+ return foundCategory;
14
+ }
15
+ for (const cat of categoryHierarchy) {
16
+ if (categoryId === cat.RecordId) {
17
+ foundCategory = cat;
18
+ break;
19
+ }
20
+ let nameToCheck = getCategoryTranslatedName(locale, cat.NameTranslations);
21
+ if (!nameToCheck) {
22
+ nameToCheck = cat.Name;
23
+ }
24
+ if (
25
+ categoryName &&
26
+ nameToCheck &&
27
+ normalizeCategoryName(categoryName) === normalizeCategoryName(nameToCheck)
28
+ ) {
29
+ foundCategory = cat;
30
+ }
31
+ if (cat.Children && cat.Children.length > 0) {
32
+ const matchingChild = searchCategory(
33
+ locale,
34
+ cat.Children,
35
+ categoryId,
36
+ categoryName,
37
+ );
38
+ if (matchingChild) {
39
+ foundCategory = matchingChild;
40
+ }
41
+ }
42
+ }
43
+ return foundCategory;
44
+ };
45
+
46
+ const getCategoryTranslatedName = (
47
+ locale?: string,
48
+ nameTranslations?: TextValueTranslation[],
49
+ ): string | undefined => {
50
+ let nameTranslation: TextValueTranslation | undefined;
51
+ if (locale && nameTranslations && nameTranslations.length > 0) {
52
+ nameTranslation = nameTranslations.find(
53
+ (item) => item.Language?.toLowerCase() === locale.toLowerCase(),
54
+ );
55
+ }
56
+ return nameTranslation && nameTranslation.Text;
57
+ };
58
+
59
+ const normalizeCategoryName = (name: string) =>
60
+ name.toLowerCase().replace(/ /g, "-");
61
+
62
+ const getCategoryUrl = (category: CategoryHierarchy) => {
63
+ const contentPageKey = PageSlug.Content;
64
+ const productListPageKey = PageSlug.ProductList;
65
+ if (category.IsCMSPage) {
66
+ return `/${contentPageKey}/${category.Url}`;
67
+ }
68
+ return `/${productListPageKey}/${category.Url}`;
69
+ };
70
+
71
+ export {
72
+ searchCategory,
73
+ getCategoryTranslatedName,
74
+ normalizeCategoryName,
75
+ getCategoryUrl,
76
+ };
@@ -0,0 +1,44 @@
1
+ import { SortOption } from "./product-list-types";
2
+
3
+ const sortOptions: SortOption[] = [
4
+ {
5
+ field: "Name",
6
+ code: "name-asc",
7
+ isDefault: true,
8
+ displayName: "Name A-Z",
9
+ isDescending: false,
10
+ },
11
+ {
12
+ field: "Name",
13
+ code: "name-desc",
14
+ isDefault: false,
15
+ displayName: "Name Z-A",
16
+ isDescending: true,
17
+ },
18
+ {
19
+ field: "Price",
20
+ code: "price-desc",
21
+ isDefault: false,
22
+ displayName: "Price High to Low",
23
+ isDescending: true,
24
+ },
25
+ {
26
+ field: "Price",
27
+ code: "price-asc",
28
+ isDefault: false,
29
+ displayName: "Price Low to High",
30
+ isDescending: false,
31
+ },
32
+ {
33
+ field: "Relevance",
34
+ code: "relevance",
35
+ isDefault: false,
36
+ displayName: "Relevance",
37
+ isDescending: true,
38
+ defaultForSearch: true,
39
+ },
40
+ ];
41
+
42
+ const getSortOptions = () => sortOptions;
43
+
44
+ export default getSortOptions;
@@ -0,0 +1,14 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ export const usePreviousRefiners = <T>(
4
+ value: T,
5
+ isValid: (val: T) => boolean,
6
+ ): T => {
7
+ const ref = useRef<T>(value);
8
+ useEffect(() => {
9
+ if (isValid(value)) {
10
+ ref.current = value;
11
+ }
12
+ }, [value, isValid]);
13
+ return ref.current;
14
+ };