@evenicanpm/storefront-core 1.0.0-alpha → 1.0.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 (78) hide show
  1. package/package.json +1 -1
  2. package/src/api-manager/api-manager.config.json +1 -1
  3. package/src/api-manager/datasources/d365/d365-address.datasource.ts +4 -4
  4. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +100 -7
  5. package/src/api-manager/datasources/d365/d365-product.datasource.ts +39 -17
  6. package/src/api-manager/datasources/d365/d365-user.datasource.ts +220 -49
  7. package/src/api-manager/datasources/d365/utils/decode-jwt.ts +2 -2
  8. package/src/api-manager/datasources/e4/e4-cart.datasource.ts +6 -0
  9. package/src/api-manager/datasources/e4/e4-product.datasource.ts +5 -0
  10. package/src/api-manager/datasources/e4/e4-user.datasource.ts +39 -15
  11. package/src/api-manager/datasources/e4/e4.datasource.ts +1 -0
  12. package/src/api-manager/datasources/e4/e4.translator.ts +2 -1
  13. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +5231 -38282
  14. package/src/api-manager/index.ts +58 -11
  15. package/src/api-manager/lib/get-graphql-client.ts +7 -7
  16. package/src/api-manager/schemas/address.schema.ts +1 -19
  17. package/src/api-manager/schemas/cart.schema.ts +18 -0
  18. package/src/api-manager/schemas/product.schema.ts +7 -0
  19. package/src/api-manager/schemas/user.schema.ts +52 -1
  20. package/src/api-manager/services/address/queries/get-states.ts +1 -1
  21. package/src/api-manager/services/cart/mutations/checkout.ts +16 -1
  22. package/src/api-manager/services/cart/queries/get-cart-lines-inventory.ts +15 -0
  23. package/src/api-manager/services/create-extension-query.ts +26 -0
  24. package/src/api-manager/services/create-mutation.ts +5 -1
  25. package/src/api-manager/services/create-query.ts +0 -1
  26. package/src/api-manager/services/organization/queries/get-channel-configuration.ts +1 -1
  27. package/src/api-manager/services/product/queries/get-product-prices.ts +16 -0
  28. package/src/api-manager/services/product/queries/search-products.ts +1 -0
  29. package/src/api-manager/services/user/mutations/add-to-wishlist.ts +24 -0
  30. package/src/api-manager/services/user/mutations/copy-cart-to-wishlist.ts +23 -0
  31. package/src/api-manager/services/user/mutations/create-wishlist.ts +4 -4
  32. package/src/api-manager/services/user/mutations/delete-wishlist.ts +4 -3
  33. package/src/api-manager/services/user/mutations/remove-from-wishlist.ts +21 -0
  34. package/src/api-manager/services/user/mutations/update-wishlist.ts +23 -0
  35. package/src/api-manager/services/user/queries/get-wishlist-details.ts +11 -4
  36. package/src/api-manager/services/user/queries/get-wishlists.ts +4 -3
  37. package/src/api-manager/types/Datasource.ts +33 -5
  38. package/src/auth/auth-options.ts +2 -2
  39. package/src/auth/msal.ts +9 -9
  40. package/src/auth/next-auth-cookie-manager.ts +5 -3
  41. package/src/auth/providers/aadb2c-provider.ts +3 -4
  42. package/src/cms/endpoints.ts +2 -3
  43. package/src/components/_components/LazyImage.tsx +5 -2
  44. package/src/components/_components/header/components/user.tsx +1 -2
  45. package/src/components/_components/product-cards/product-card-1/components/quantity-buttons.tsx +17 -8
  46. package/src/components/_components/product-cards/product-card-1/product-card-1-wrapper.tsx +304 -0
  47. package/src/components/_components/product-cards/product-card-1/product-card.tsx +16 -55
  48. package/src/components/_components/product-cards/product-card-8/product-card.tsx +80 -60
  49. package/src/components/_components/product-cards/product-card-8/styles/index.ts +10 -0
  50. package/src/components/_components/product-cards/product-card-wishlist/components/hover-actions.tsx +37 -0
  51. package/src/components/_components/product-cards/product-card-wishlist/index.ts +1 -0
  52. package/src/components/_components/product-cards/product-card-wishlist/product-card.tsx +118 -0
  53. package/src/components/_components/product-cards/product-card-wishlist/styles/index.ts +51 -0
  54. package/src/components/_components/products-view/product-view-dialog.tsx +48 -10
  55. package/src/components/_components/products-view/products-grid-view.tsx +31 -17
  56. package/src/components/_components/products-view/products-list-view.tsx +30 -16
  57. package/src/components/_components/search-box/__test__/search-input-category.test.tsx +110 -0
  58. package/src/components/_components/search-box/__test__/search-input.test.tsx +137 -0
  59. package/src/components/_components/search-box/__test__/search-results.json +165 -0
  60. package/src/components/_components/search-box/components/search-result.tsx +66 -12
  61. package/src/components/_components/search-box/search-input-category.tsx +135 -32
  62. package/src/components/_components/search-box/styles/index.ts +2 -0
  63. package/src/components/_components/section-header/section-creator.tsx +1 -0
  64. package/src/components/_components/wishlist/add-to-wishlist-dialog.tsx +161 -0
  65. package/src/components/_components/wishlist/components/create-new-list.tsx +167 -0
  66. package/src/components/_components/wishlist/components/create-wishlist-button.tsx +40 -0
  67. package/src/components/_components/wishlist/components/dashboard-header.tsx +123 -0
  68. package/src/components/_components/wishlist/components/navigation.tsx +87 -0
  69. package/src/components/_components/wishlist/components/no-records.tsx +20 -0
  70. package/src/components/_components/wishlist/components/wishlist-modal-item.tsx +66 -0
  71. package/src/components/_components/wishlist/remove-from-wishlist-dialog.tsx +174 -0
  72. package/src/components/_components/wishlist/styles.ts +61 -0
  73. package/src/components/countries-input.tsx +6 -12
  74. package/src/lib/cart-cookie-handler-constants.ts +8 -0
  75. package/src/lib/cart-cookie-handler.ts +27 -5
  76. package/src/lib/create-graphql-client.ts +1 -2
  77. package/src/lib/product-list-types.ts +41 -0
  78. package/src/config.ts +0 -47
@@ -0,0 +1,123 @@
1
+ import React, { ReactNode } from "react";
2
+ import Link from "next/link";
3
+ import { Box, Button, IconButton, Theme, styled } from "@mui/material";
4
+ import useMediaQuery from "@mui/material/useMediaQuery";
5
+ import { useTranslations } from "next-intl";
6
+ // ICON COMPONENTS
7
+ import { MdMenu as Menu } from "react-icons/md";
8
+ // GLOBAL CUSTOM COMPONENTS
9
+ import SideNav from "@evenicanpm/storefront-core/src/components/_components/side-nav/index";
10
+ import { H2 } from "@evenicanpm/storefront-core/src/components/_components/Typography";
11
+ import FlexBox from "@evenicanpm/storefront-core/src/components/_components/flex-box/flex-box";
12
+ // LOCAL CUSTOM COMPONENTS
13
+ import Navigation from "./navigation";
14
+ import { IconType } from "react-icons";
15
+
16
+ // STYLED COMPONENT
17
+ const StyledBox = styled("div")(({ theme }) => ({
18
+ display: "flex",
19
+ marginTop: theme.spacing(-2),
20
+ marginBottom: theme.spacing(3),
21
+ "& .headerHold": {
22
+ flexGrow: 1,
23
+ alignItems: "center",
24
+ justifyContent: "space-between",
25
+ },
26
+ [theme.breakpoints.up("md")]: {
27
+ "& .sidenav": { display: "none" },
28
+ },
29
+ [theme.breakpoints.down("md")]: {
30
+ flexDirection: "column",
31
+ },
32
+ }));
33
+
34
+ // ==============================================================
35
+
36
+ type WithButtons = {
37
+ title: string;
38
+ titleMessage?: string;
39
+ buttons: ReactNode;
40
+ Icon: IconType;
41
+ href?: never;
42
+ buttonText?: never;
43
+ };
44
+
45
+ type WithGeneratedButton = {
46
+ title: string;
47
+ titleMessage?: string;
48
+ href?: string;
49
+ buttonText?: string;
50
+ Icon: IconType;
51
+ buttons?: ReactNode;
52
+ };
53
+
54
+ type Props = WithButtons | WithGeneratedButton;
55
+
56
+ // ==============================================================
57
+
58
+ export default function DashboardHeader({
59
+ title,
60
+ titleMessage,
61
+ buttons,
62
+ href,
63
+ buttonText,
64
+ Icon,
65
+ }: Props) {
66
+ const t = useTranslations();
67
+ const isTablet = useMediaQuery((theme: Theme) =>
68
+ theme.breakpoints.down(1025),
69
+ );
70
+
71
+ const GENERATED_BUTTON = href ? (
72
+ <Button
73
+ href={href}
74
+ color="primary"
75
+ LinkComponent={Link}
76
+ sx={{ bgcolor: "primary.light", px: 4 }}
77
+ >
78
+ {t(buttonText)}
79
+ </Button>
80
+ ) : null;
81
+
82
+ const HEADER_LINKS = (
83
+ <FlexBox gap={1} justifyContent="flex-end">
84
+ {buttons || GENERATED_BUTTON}
85
+ </FlexBox>
86
+ );
87
+
88
+ return (
89
+ <StyledBox>
90
+ <FlexBox mt={2} className="headerHold">
91
+ <FlexBox alignItems="center" gap={0.5}>
92
+ {Icon && (
93
+ <IconButton color="primary">
94
+ <Icon />
95
+ </IconButton>
96
+ )}
97
+ <H2 my={0} lineHeight={1} ellipsis>
98
+ {titleMessage ? titleMessage : t(title)}
99
+ </H2>
100
+ </FlexBox>
101
+
102
+ <FlexBox className="button-container" justifyContent="flex-end">
103
+ {!isTablet && HEADER_LINKS}
104
+ </FlexBox>
105
+
106
+ <div className="sidenav">
107
+ <SideNav
108
+ position="left"
109
+ handler={(close) => (
110
+ <IconButton onClick={close}>
111
+ <Menu fontSize="small" />
112
+ </IconButton>
113
+ )}
114
+ >
115
+ <Navigation />
116
+ </SideNav>
117
+ </div>
118
+ </FlexBox>
119
+
120
+ {isTablet ? <Box mt={2}>{HEADER_LINKS}</Box> : null}
121
+ </StyledBox>
122
+ );
123
+ }
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import { Fragment } from "react";
5
+ import { useTranslations } from "next-intl";
6
+
7
+ // ICON COMPONENTS
8
+ import {
9
+ MdOutlineShoppingBag as ShoppingBagOutlined,
10
+ MdFavoriteBorder as FavoriteBorder,
11
+ MdPerson as Person,
12
+ MdPlace as Place,
13
+ } from "react-icons/md";
14
+
15
+ // GLOBAL CUSTOM COMPONENTS
16
+ import FlexBox from "@evenicanpm/storefront-core/src/components/_components/flex-box/flex-box";
17
+ import {
18
+ Paragraph,
19
+ Span,
20
+ } from "@evenicanpm/storefront-core/src/components/_components/Typography";
21
+
22
+ // API
23
+ import getUser from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-user";
24
+ import getUserCounts from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-user-counts";
25
+
26
+ // STYLED COMPONENTS
27
+ import { MainContainer, StyledNavLink } from "../styles";
28
+
29
+ const MENU_ITEMS = [
30
+ {
31
+ title: "DASHBOARD",
32
+ list: [
33
+ {
34
+ href: "/account/orders",
35
+ title: "orders",
36
+ Icon: ShoppingBagOutlined,
37
+ count: 0,
38
+ },
39
+ { href: "/account/wish-lists", title: "wishlists", Icon: FavoriteBorder },
40
+ ],
41
+ },
42
+ {
43
+ title: "ACCOUNT SETTINGS",
44
+ list: [
45
+ { href: "/account/profile", title: "profile", Icon: Person },
46
+ { href: "/account/addresses", title: "addresses", Icon: Place, count: 0 },
47
+ ],
48
+ },
49
+ ];
50
+
51
+ export default function Navigation() {
52
+ const pathname = usePathname();
53
+ const t = useTranslations("Account.Menu");
54
+
55
+ const { data: user } = getUser.useData();
56
+ const { data: counts } = getUserCounts.useData(String(user?.RecordId) || "");
57
+
58
+ MENU_ITEMS[0].list[0].count = counts?.orderCount;
59
+ MENU_ITEMS[1].list[1].count = counts?.addressCount;
60
+
61
+ return (
62
+ <MainContainer>
63
+ {MENU_ITEMS.map((item) => (
64
+ <Fragment key={item.title}>
65
+ <Paragraph p="26px 30px 1rem" color="grey.600" fontSize={12}>
66
+ {item.title}
67
+ </Paragraph>
68
+
69
+ {item.list.map(({ Icon, count, href, title }) => (
70
+ <StyledNavLink
71
+ href={href}
72
+ key={title}
73
+ isCurrentPath={pathname.includes(href)}
74
+ >
75
+ <FlexBox alignItems="center" gap={1}>
76
+ <Icon color="inherit" fontSize="large" className="nav-icon" />
77
+ <Span>{t(title)}</Span>
78
+ </FlexBox>
79
+
80
+ <Span>{count}</Span>
81
+ </StyledNavLink>
82
+ ))}
83
+ </Fragment>
84
+ ))}
85
+ </MainContainer>
86
+ );
87
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { Box } from "@mui/material";
3
+ import { H4 } from "@evenicanpm/storefront-core/src/components/_components/Typography";
4
+ import { useTranslations } from "next-intl";
5
+
6
+ type Props = { message: string };
7
+
8
+ export default function NoRecords({ message }: Props) {
9
+ const t = useTranslations();
10
+ return (
11
+ <Box
12
+ display="flex"
13
+ justifyContent="center"
14
+ alignItems="center"
15
+ height="50vh"
16
+ >
17
+ <H4 color="grey.500">{t(message)}</H4>
18
+ </Box>
19
+ );
20
+ }
@@ -0,0 +1,66 @@
1
+ import { Button } from "@mui/material";
2
+ import { format } from "date-fns";
3
+ // MUI ICON COMPONENTS
4
+ // GLOBAL CUSTOM COMPONENTS
5
+ import { Paragraph } from "@evenicanpm/storefront-core/src/components/_components/Typography";
6
+ // LOCAL CUSTOM COMPONENT
7
+
8
+ // API
9
+ import { Wishlist } from "@/api-manager/schemas/user.schema";
10
+ import { StyledTableRow } from "../styles";
11
+
12
+ interface Props {
13
+ wishlist: Wishlist;
14
+ addToWishlist?: (wishlistId: string) => void;
15
+ removeFromWishlist?: (wishlistId: string) => void;
16
+ }
17
+
18
+ export default function WishlistModalItem({
19
+ wishlist,
20
+ addToWishlist,
21
+ removeFromWishlist,
22
+ }: Props) {
23
+ const handleClick = () => {
24
+ if (addToWishlist) {
25
+ addToWishlist(wishlist.Id);
26
+ } else if (removeFromWishlist) {
27
+ removeFromWishlist(wishlist.Id);
28
+ }
29
+ };
30
+ return (
31
+ <StyledTableRow
32
+ sx={{
33
+ display: "grid",
34
+ gridTemplateColumns: "2fr 1fr 1fr 1fr", // Define equal column structure
35
+ alignItems: "center",
36
+ gap: 2,
37
+ padding: "10px 15px",
38
+ width: "500px",
39
+ cursor: "default",
40
+ }}
41
+ >
42
+ <Paragraph ellipsis sx={{ fontWeight: "bold" }}>
43
+ {wishlist?.Name ?? ""}
44
+ </Paragraph>
45
+ <Paragraph ellipsis>
46
+ {" "}
47
+ {format(
48
+ new Date(wishlist?.CreatedDateTime ?? wishlist?.LastUpdated ?? ""),
49
+ "MMM dd, yyyy",
50
+ )}
51
+ </Paragraph>
52
+ <Paragraph ellipsis>
53
+ {" "}
54
+ {`${wishlist?.ProductListLines?.length} products`}
55
+ </Paragraph>
56
+
57
+ <Button
58
+ onClick={handleClick}
59
+ color="primary"
60
+ sx={{ bgcolor: "primary.light", px: 4 }}
61
+ >
62
+ {addToWishlist ? "Add" : "Remove"}
63
+ </Button>
64
+ </StyledTableRow>
65
+ );
66
+ }
@@ -0,0 +1,174 @@
1
+ import React, { Fragment, useCallback, useEffect } from "react";
2
+ import isFunction from "lodash/isFunction";
3
+ import { useTranslations } from "next-intl";
4
+ import { Grid, Button, Dialog, DialogContent } from "@mui/material";
5
+ import { MdFavorite } from "react-icons/md";
6
+ // Global Custom components
7
+ import DashboardHeader from "./components/dashboard-header";
8
+ import WishlistModalItem from "./components/wishlist-modal-item";
9
+ import NoRecords from "./components/no-records";
10
+ // API
11
+ import getWishlists from "@/api-manager/services/user/queries/get-wishlists";
12
+ import {
13
+ ExtendedProductListLine,
14
+ Wishlist,
15
+ WishlistLinesInput,
16
+ } from "@evenicanpm/storefront-core/src/api-manager/schemas/user.schema";
17
+ import { useNotification } from "@evenicanpm/storefront-core/src/components/Notifications/use-notification";
18
+ import { useSession } from "next-auth/react";
19
+ import useRemoveFromWishlist from "@/api-manager/services/user/mutations/remove-from-wishlist";
20
+
21
+ interface Props {
22
+ open: boolean;
23
+ // product?: ProductDetails;
24
+ productId?: number;
25
+ onSuccess?: () => void;
26
+ handleClose: () => void;
27
+ }
28
+
29
+ export default function RemoveFromWishlistDialog({
30
+ open,
31
+ productId,
32
+ handleClose,
33
+ onSuccess,
34
+ }: Props) {
35
+ const { data: wishlists, isLoading } = getWishlists.useSuspenseData();
36
+ const t = useTranslations("Account.Wishlists");
37
+ const { setNotification } = useNotification();
38
+
39
+ const { mutateAsync: removeFromWishlist } = useRemoveFromWishlist();
40
+
41
+ const { data: session } = useSession();
42
+ const isAuthenticated = !!session;
43
+
44
+ const [relevantWishlists, setRelevantWishlists] = React.useState<Wishlist[]>(
45
+ [],
46
+ );
47
+
48
+ const handleCloseDialog = () => {
49
+ if (isFunction(handleClose)) {
50
+ handleClose();
51
+ }
52
+ };
53
+
54
+ // get wishlists that the product is in
55
+ const getRelevantWishlists = useCallback(
56
+ (wishlists: Wishlist[]) => {
57
+ return wishlists.filter((wishlist) =>
58
+ wishlist.ProductListLines?.some((line) => line.ProductId === productId),
59
+ );
60
+ },
61
+ [productId],
62
+ );
63
+
64
+ const handleRemoveFromWishlist = useCallback(
65
+ async (wishlistId: string) => {
66
+ // get the selected wishlist
67
+ const wishlist = relevantWishlists.find(
68
+ (wishlist) => wishlist.Id === wishlistId,
69
+ );
70
+ if (wishlist) {
71
+ try {
72
+ const extendedWishlistLine = wishlist.ProductListLines?.find(
73
+ (item: ExtendedProductListLine) => item.ProductId === productId,
74
+ );
75
+
76
+ if (extendedWishlistLine) {
77
+ const wishlistLinesInput: WishlistLinesInput = {
78
+ Id: wishlist.Id,
79
+ ProductListLines: [extendedWishlistLine],
80
+ };
81
+
82
+ await removeFromWishlist(wishlistLinesInput);
83
+
84
+ setNotification({
85
+ message: t("wishlistLineRemoveSuccess"),
86
+ severity: "success",
87
+ });
88
+ if (isFunction(onSuccess)) {
89
+ onSuccess();
90
+ }
91
+ }
92
+ } catch (error) {
93
+ setNotification({
94
+ message: t("wishlistLineRemoveFailed"),
95
+ severity: "error",
96
+ });
97
+ }
98
+ }
99
+ },
100
+ [
101
+ relevantWishlists,
102
+ removeFromWishlist,
103
+ setNotification,
104
+ t,
105
+ onSuccess,
106
+ productId,
107
+ ],
108
+ );
109
+
110
+ useEffect(() => {
111
+ const withlistsWithProduct = getRelevantWishlists(wishlists);
112
+ setRelevantWishlists(withlistsWithProduct);
113
+ if (open && withlistsWithProduct.length === 1) {
114
+ // delete right away
115
+ handleRemoveFromWishlist(withlistsWithProduct[0].Id);
116
+ }
117
+ }, [open, getRelevantWishlists, handleRemoveFromWishlist, wishlists]);
118
+
119
+ return (
120
+ <Dialog
121
+ open={open}
122
+ onClose={handleCloseDialog}
123
+ aria-labelledby="form-dialog-title"
124
+ >
125
+ <DialogContent>
126
+ <Fragment>
127
+ <DashboardHeader
128
+ title="Account.Wishlists.heading"
129
+ Icon={MdFavorite}
130
+ href=""
131
+ buttonText=""
132
+ />
133
+ {isAuthenticated && !isLoading ? (
134
+ relevantWishlists && relevantWishlists.length > 0 ? (
135
+ relevantWishlists?.map((wishlist: Wishlist) => (
136
+ <WishlistModalItem
137
+ key={wishlist.Id}
138
+ wishlist={wishlist}
139
+ removeFromWishlist={handleRemoveFromWishlist}
140
+ />
141
+ ))
142
+ ) : (
143
+ <NoRecords message="Account.Wishlists.noWishlists" />
144
+ )
145
+ ) : (
146
+ <NoRecords message="Account.Wishlists.wishlistAddUnauthenticated" />
147
+ )}
148
+ </Fragment>
149
+ <Grid container spacing={3}>
150
+ <Grid
151
+ item
152
+ sm={12}
153
+ xs={12}
154
+ sx={{
155
+ display: "flex",
156
+ justifyContent: "flex-start",
157
+ gap: 2,
158
+ }}
159
+ >
160
+ <Button
161
+ color="primary"
162
+ variant="outlined"
163
+ size="small"
164
+ onClick={handleCloseDialog}
165
+ sx={{ height: 30.75 }}
166
+ >
167
+ {t("btnCancel")}
168
+ </Button>
169
+ </Grid>
170
+ </Grid>
171
+ </DialogContent>
172
+ </Dialog>
173
+ );
174
+ }
@@ -0,0 +1,61 @@
1
+ import Card, { CardProps } from "@mui/material/Card";
2
+ import styled from "@mui/material/styles/styled";
3
+ import NavLink, {
4
+ NavLinkProps,
5
+ } from "@evenicanpm/storefront-core/src/components/_components/nav-link/nav-link";
6
+ import { ComponentType } from "react";
7
+
8
+ type ExtendedNavLinkProps = NavLinkProps & {
9
+ isCurrentPath?: boolean;
10
+ };
11
+
12
+ export const MainContainer: ComponentType<CardProps> = styled(Card)(
13
+ ({ theme }) => ({
14
+ paddingBottom: "1.5rem",
15
+ [theme.breakpoints.down("md")]: {
16
+ boxShadow: "none",
17
+ overflowY: "auto",
18
+ height: "calc(100vh - 64px)",
19
+ },
20
+ }),
21
+ ) as typeof Card;
22
+
23
+ export const StyledNavLink: ComponentType<ExtendedNavLinkProps> = styled(
24
+ NavLink,
25
+ {
26
+ shouldForwardProp: (prop) => prop !== "isCurrentPath",
27
+ },
28
+ )<ExtendedNavLinkProps>(({ theme, isCurrentPath }) => ({
29
+ display: "flex",
30
+ alignItems: "center",
31
+ borderLeft: "4px solid",
32
+ paddingLeft: "1.5rem",
33
+ paddingRight: "1.5rem",
34
+ marginBottom: "1.25rem",
35
+ justifyContent: "space-between",
36
+ borderColor: isCurrentPath ? theme.palette.primary.main : "transparent",
37
+ "& .nav-icon": {
38
+ color: isCurrentPath ? theme.palette.primary.main : theme.palette.grey[600],
39
+ },
40
+ "&:hover": {
41
+ borderColor: theme.palette.primary.main,
42
+ "& .nav-icon": { color: theme.palette.primary.main },
43
+ },
44
+ }));
45
+
46
+ export const StyledTableRow: ComponentType<CardProps> = styled(Card)(
47
+ ({ theme }) => ({
48
+ gap: 16,
49
+ marginBlock: 16,
50
+ display: "grid",
51
+ borderRadius: 10,
52
+ cursor: "pointer",
53
+ alignItems: "center",
54
+ padding: ".6rem 1.2rem",
55
+ gridTemplateColumns: "1.5fr 2fr 1.5fr auto",
56
+ [theme.breakpoints.down("sm")]: {
57
+ gap: 8,
58
+ gridTemplateColumns: "repeat(2, 1fr)",
59
+ },
60
+ }),
61
+ );
@@ -3,7 +3,6 @@ import {
3
3
  Autocomplete,
4
4
  AutocompleteRenderInputParams,
5
5
  Box,
6
- InputProps,
7
6
  TextField,
8
7
  } from "@mui/material";
9
8
  import React from "react";
@@ -28,12 +27,14 @@ export default function CountryInput({
28
27
  ) => setCountryInputValue(value);
29
28
 
30
29
  const renderOption = (
31
- optionProps: React.HTMLAttributes<HTMLLIElement>,
30
+ props: React.HTMLAttributes<HTMLLIElement> & {
31
+ key: any;
32
+ },
32
33
  option: CountryRegionInfo,
33
- index: number,
34
34
  ) => {
35
+ const { key, ...optionProps } = props;
35
36
  return (
36
- <Box key={index} component="li" {...optionProps}>
37
+ <Box key={key} component="li" {...optionProps}>
37
38
  {option.ShortName}
38
39
  </Box>
39
40
  );
@@ -45,11 +46,6 @@ export default function CountryInput({
45
46
  label={label}
46
47
  variant="outlined"
47
48
  placeholder="Select Country"
48
- slotProps={{
49
- input: {
50
- "data-testid": "country-input",
51
- } as InputProps,
52
- }}
53
49
  />
54
50
  );
55
51
 
@@ -66,9 +62,7 @@ export default function CountryInput({
66
62
  data-testid="country-autocomplete"
67
63
  inputValue={countryInputValue}
68
64
  onInputChange={handleAutoCompleteChange}
69
- renderOption={(props, option, state) =>
70
- renderOption(props, option, state?.index)
71
- }
65
+ renderOption={(props, option) => renderOption(props, option)}
72
66
  renderInput={renderInput}
73
67
  getOptionLabel={getOptionLabel}
74
68
  />
@@ -0,0 +1,8 @@
1
+ export const CART_COOKIE_NAME = "cart_cookie";
2
+ export const CartCookieType = {
3
+ Auth: "AU",
4
+ Anon: "AN",
5
+ } as const;
6
+
7
+ export type CartCookieType =
8
+ (typeof CartCookieType)[keyof typeof CartCookieType];
@@ -1,14 +1,36 @@
1
1
  "use server";
2
2
  import { cookies } from "next/headers";
3
+ import {
4
+ CART_COOKIE_NAME,
5
+ CartCookieType,
6
+ } from "./cart-cookie-handler-constants";
3
7
 
4
- const getCartCookie = async () => (await cookies()).get("cart_cookie")?.value;
8
+ const getCartCookie = async (): Promise<string | null> =>
9
+ (await getCartCookieWithAuthStatus())?.id ?? null;
5
10
 
6
- const setCartCookie = async (cartId: string) => {
7
- (await cookies()).set("cart_cookie", cartId);
11
+ const setCartCookie = async (cartId: string, isAuthenticated?: boolean) => {
12
+ const cookieValue = `${isAuthenticated ? CartCookieType.Auth : CartCookieType.Anon}:${cartId}`;
13
+ (await cookies()).set(CART_COOKIE_NAME, cookieValue);
8
14
  };
9
15
 
10
16
  const deleteCartCookie = async () => {
11
- (await cookies()).delete("cart_cookie");
17
+ (await cookies()).delete(CART_COOKIE_NAME);
12
18
  };
13
19
 
14
- export { getCartCookie, setCartCookie, deleteCartCookie };
20
+ const getCartCookieWithAuthStatus = async () => {
21
+ const cartCookie = (await cookies()).get(CART_COOKIE_NAME)?.value;
22
+ if (!cartCookie) return null;
23
+ const parts = cartCookie.split(":");
24
+ if (parts.length !== 2) return null;
25
+ return {
26
+ type: parts[0] as CartCookieType,
27
+ id: parts[1] as string,
28
+ };
29
+ };
30
+
31
+ export {
32
+ getCartCookie,
33
+ setCartCookie,
34
+ deleteCartCookie,
35
+ getCartCookieWithAuthStatus,
36
+ };
@@ -5,7 +5,6 @@ import { GraphQLClient } from "graphql-request";
5
5
  // and one here to isntantiate because graphql-request library must be same
6
6
  // for this file and the sdk
7
7
  import { getSdk } from "@/lib/graphqlRequestSdk";
8
- import { getConfig } from "@config";
9
8
 
10
9
  // typescript can't infer type without reference
11
10
  export type E4Sdk = ReturnType<typeof getSdk>;
@@ -15,7 +14,7 @@ export type E4Sdk = ReturnType<typeof getSdk>;
15
14
  * @returns a new graphql request client with all of E4 queries and mutations
16
15
  */
17
16
  const createGraphQLClient = async (): Promise<E4Sdk> => {
18
- const API_URL = getConfig().E4_API_ENDPOINT;
17
+ const API_URL = process.env.NEXT_PUBLIC_E4_API_ENDPOINT || "";
19
18
  const client = new GraphQLClient(API_URL, {
20
19
  credentials: "include", // Ensures cookies are included
21
20
  headers: {
@@ -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
+ };