@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
@@ -1,5 +1,6 @@
1
1
  import * as cartSchemas from "@/api-manager/schemas/cart.schema";
2
2
  import * as productSchemas from "@/api-manager/schemas/product.schema";
3
+ import * as userSchemas from "@/api-manager/schemas/user.schema";
3
4
  import { User, UserCounts } from "@/api-manager/schemas/user.schema";
4
5
  import {
5
6
  Session,
@@ -31,6 +32,7 @@ import {
31
32
  SimpleProduct,
32
33
  StateProvinceInfo,
33
34
  ProductWarehouseInventoryAvailability,
35
+ ProductWarehouseInventoryInformation,
34
36
  } from "@msdyn365-commerce/retail-proxy";
35
37
  import {
36
38
  CategoriesQuery,
@@ -89,14 +91,34 @@ export interface OrganizationApi {
89
91
  * User Data Actions
90
92
  */
91
93
  export interface UserApi {
94
+ updateUser(user: userSchemas.User): Promise<userSchemas.User>;
95
+ getUser(): Promise<userSchemas.User | null>;
96
+ getUserCounts(userId: string): Promise<UserCounts>;
97
+ // wishlists
98
+ getWishlists(): Promise<userSchemas.Wishlist[]>;
99
+ getWishlistDetails(
100
+ input: userSchemas.WishlistDetailInput,
101
+ ): Promise<userSchemas.ExtendedWishlist>;
102
+ createWishlist(
103
+ input: userSchemas.WishlistCreateInput,
104
+ ): Promise<userSchemas.Wishlist>;
105
+ deleteWishlist(input: userSchemas.WishlistDetailInput): Promise<void>;
106
+ updateWishlist(
107
+ // input: userSchemas.UpdateWishlistInput, // used to change wishlist name
108
+ input: userSchemas.WishlistLinesInput,
109
+ ): Promise<userSchemas.Wishlist>;
110
+ addProductsToWishlist(
111
+ input: userSchemas.WishlistLinesInput,
112
+ ): Promise<userSchemas.Wishlist>;
113
+ removeProductsFromWishlist(
114
+ input: userSchemas.WishlistLinesInput,
115
+ ): Promise<void>;
116
+ copyCartToWishlist(
117
+ input: userSchemas.CopyCartToWishlistInput,
118
+ ): Promise<userSchemas.Wishlist>;
92
119
  updateUser(user: User): Promise<User>;
93
120
  getUser(): Promise<User | null>;
94
121
  getUserCounts(userId: string): Promise<UserCounts>;
95
- // wishlists
96
- getWishlists(): Promise<WishList[]>;
97
- getWishlistDetails(id: string): Promise<WishList>;
98
- createWishlist(wishlist: CreateWishListInput): Promise<WishList>;
99
- deleteWishlist(id: string): Promise<string>;
100
122
  }
101
123
 
102
124
  /**
@@ -142,6 +164,9 @@ export interface CartApi {
142
164
  input: cartSchemas.RetrieveCardPaymentAcceptResultInput,
143
165
  ): Promise<CardPaymentAcceptResult>;
144
166
  checkout(input: cartSchemas.CheckoutInput): Promise<SalesOrder>;
167
+ getCartLinesInventory(
168
+ input: cartSchemas.InventorySearchCartLinesCriteriaInput,
169
+ ): Promise<ProductWarehouseInventoryInformation>;
145
170
  }
146
171
 
147
172
  /**
@@ -167,6 +192,9 @@ export interface ProductApi {
167
192
  getInventory(
168
193
  input: productSchemas.InventorySearchCriteriaInput,
169
194
  ): Promise<ProductWarehouseInventoryAvailability>;
195
+ getProductPrices(
196
+ input: productSchemas.ProductPriceInput,
197
+ ): Promise<productSchemas.ProductPrice[]>;
170
198
  }
171
199
 
172
200
  /**
@@ -8,7 +8,7 @@ import {
8
8
  } from "next";
9
9
  import { getProviders } from "./providers";
10
10
  import { deleteCartCookie } from "@/lib/cart-cookie-handler";
11
- import { getConfig } from "@config";
11
+ import { getSecret } from "@evenicanpm/common-core/keyvault";
12
12
 
13
13
  // Split providers into separate array so we can iterate them and match the provider ID
14
14
  export type ExtendedJwtToken = JWT & {
@@ -20,7 +20,7 @@ export type ExtendedSession = Session & { token?: JWT };
20
20
 
21
21
  const createAuthOptions = (): NextAuthOptions => {
22
22
  const providers = getProviders();
23
- const secret = getConfig().NEXTAUTH_SECRET;
23
+ const secret = getSecret("NEXTAUTH_SECRET");
24
24
  return {
25
25
  providers,
26
26
  secret,
package/src/auth/msal.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { PublicClientApplication, Configuration } from "@azure/msal-browser";
2
- import { getConfig } from "@config";
3
2
 
4
3
  const createMsalConfig = (): Configuration => {
5
4
  return {
6
5
  auth: {
7
- clientId: getConfig().AZURE_AD_B2C_CLIENTID, //process.env.NEXT_PUBLIC_AZURE_AD_B2C_CLIENTID || "",
8
- authority: getConfig().AZURE_AD_B2C_LOGIN_AUTHORITY, //process.env.NEXT_PUBLIC_AZURE_AD_B2C_LOGIN_AUTHORITY || "",
9
- redirectUri: "/", //process.env.NEXT_PUBLIC_AZURE_AD_B2C_REDIRECT || "/",
10
- postLogoutRedirectUri: getConfig().AZURE_AD_B2C_POST_LOGOUT_REDIRECT, //process.env.NEXT_PUBLIC_AZURE_AD_B2C_POST_LOGOUT_REDIRECT || "/",
6
+ clientId: process.env.NEXT_PUBLIC_AZURE_AD_B2C_CLIENTID || "",
7
+ authority: process.env.NEXT_PUBLIC_AZURE_AD_B2C_LOGIN_AUTHORITY || "",
8
+ redirectUri: process.env.NEXT_PUBLIC_AZURE_AD_B2C_REDIRECT || "/",
9
+ postLogoutRedirectUri:
10
+ process.env.NEXT_PUBLIC_AZURE_AD_B2C_POST_LOGOUT_REDIRECT || "/",
11
11
  knownAuthorities: [
12
- getConfig().AZURE_AD_B2C_KNOWN_AUTHORITY, //process.env.NEXT_PUBLIC_AZURE_AD_B2C_KNOWN_AUTHORITY || "",
12
+ process.env.NEXT_PUBLIC_AZURE_AD_B2C_KNOWN_AUTHORITY || "",
13
13
  ],
14
14
  navigateToLoginRequestUrl: true,
15
15
  },
@@ -44,11 +44,11 @@ const getMsalInstance = () => {
44
44
  };
45
45
 
46
46
  const getRequestConfig = (requestType?: string) => {
47
- const editAuthority = getConfig().AZURE_AD_B2C_EDIT_AUTHORITY;
48
- const resetAuthority = getConfig().AZURE_AD_B2C_RESET_AUTHORITY;
47
+ const editAuthority = process.env.NEXT_PUBLIC_AZURE_AD_B2C_EDIT_AUTHORITY;
48
+ const resetAuthority = process.env.NEXT_PUBLIC_AZURE_AD_B2C_RESET_AUTHORITY;
49
49
 
50
50
  const baseRequest = {
51
- scopes: [getConfig().AZURE_AD_B2C_API_PERMISSION],
51
+ scopes: [process.env.NEXT_PUBLIC_AZURE_AD_B2C_API_PERMISSION],
52
52
  authority: createMsalConfig().auth.authority,
53
53
  };
54
54
 
@@ -6,6 +6,8 @@ import { ExtendedJwtToken } from "@/auth/auth-options";
6
6
  const NEXT_AUTH_COOKIE_NAME = "next-auth.session-token";
7
7
  const SECURE_COOKIE_PREFIX = "__Secure-";
8
8
 
9
+ import { getSecret } from "@evenicanpm/common-core/keyvault";
10
+
9
11
  /**
10
12
  * Retrieves and decodes the NextAuth session cookie from the request.
11
13
  *
@@ -28,7 +30,7 @@ const manuallyRetrieveSessionCookie = async () => {
28
30
  if (cookie) {
29
31
  return decode({
30
32
  token: cookie.value,
31
- secret: process.env.NEXTAUTH_SECRET || "",
33
+ secret: getSecret("NEXTAUTH_SECRET") || "",
32
34
  });
33
35
  }
34
36
  //If the cookie is not found, we need to see if there is a chunked cookie
@@ -49,7 +51,7 @@ const manuallyRetrieveSessionCookie = async () => {
49
51
  if (cookieValues.length) {
50
52
  return decode({
51
53
  token: cookieValues.join(""),
52
- secret: process.env.NEXTAUTH_SECRET || "",
54
+ secret: getSecret("NEXTAUTH_SECRET") || "",
53
55
  });
54
56
  }
55
57
 
@@ -73,7 +75,7 @@ const manuallySetSessionCookie = async (token: JWT | ExtendedJwtToken) => {
73
75
  // Set the cookie with the name __Secure-next-auth.session-token
74
76
  const value = await encode({
75
77
  token,
76
- secret: process.env.NEXTAUTH_SECRET || "",
78
+ secret: getSecret("NEXTAUTH_SECRET") || "",
77
79
  });
78
80
  cookieStore.set({
79
81
  name: `${SECURE_COOKIE_PREFIX}${NEXT_AUTH_COOKIE_NAME}`,
@@ -1,4 +1,3 @@
1
- import { getConfig } from "@config";
2
1
  import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
3
2
 
4
3
  // By default, NextAuth tries to use either PKCE or Client Secret-based authentication, depending on how the provider is configured.
@@ -6,9 +5,9 @@ import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
6
5
  // If clientSecret is missing, it assumes PKCE.
7
6
  // But Azure AD B2C sometimes requires PKCE explicitly, so NextAuth needs to be told to always use PKCE.
8
7
  const azureADB2CProvider = () => {
9
- const CLIENT_ID = getConfig().AZURE_AD_B2C_CLIENTID;
10
- const SCOPE = `${getConfig().AZURE_AD_B2C_API_PERMISSION} offline_access openid`;
11
- const LOGIN_AUTHORITY = getConfig().AZURE_AD_B2C_LOGIN_AUTHORITY;
8
+ const CLIENT_ID = process.env.NEXT_PUBLIC_AZURE_AD_B2C_CLIENTID || "";
9
+ const SCOPE = `${process.env.NEXT_PUBLIC_AZURE_AD_B2C_API_PERMISSION} offline_access openid`;
10
+ const LOGIN_AUTHORITY = process.env.NEXT_PUBLIC_AZURE_AD_B2C_LOGIN_AUTHORITY;
12
11
 
13
12
  return AzureADB2CProvider({
14
13
  clientId: CLIENT_ID,
@@ -1,5 +1,4 @@
1
1
  import { Formats } from "./interfaces/productData";
2
- import { getConfig } from "@config";
3
2
 
4
3
  const apiKeys = {
5
4
  pages: "/cms/pages",
@@ -17,12 +16,12 @@ type ApiKeysType = typeof apiKeys;
17
16
  type ApiKey = keyof ApiKeysType;
18
17
 
19
18
  const getCmsApiUrl = (apiKey: ApiKey) => {
20
- const cmsApiEndpoint = getConfig().E4_API_ENDPOINT;
19
+ const cmsApiEndpoint = process.env.NEXT_PUBLIC_E4_API_ENDPOINT;
21
20
  return `${cmsApiEndpoint}${apiKeys[apiKey]}`;
22
21
  };
23
22
 
24
23
  const buildCmsMediaUrl = (mediaUrl: string) => {
25
- const cmsEndpoint = getConfig().CMS_ENDPOINT;
24
+ const cmsEndpoint = process.env.NEXT_PUBLIC_CMS_ENDPOINT;
26
25
 
27
26
  if (mediaUrl === undefined || mediaUrl === null) {
28
27
  return "";
@@ -1,9 +1,12 @@
1
1
  "use client";
2
2
 
3
- import NextImage from "next/image";
3
+ import NextImage, { ImageProps } from "next/image";
4
4
  import styled from "@mui/material/styles/styled";
5
+ import { ComponentType } from "react";
5
6
 
6
- export default styled(NextImage)({
7
+ const LazyImage: ComponentType<ImageProps> = styled(NextImage)({
7
8
  width: "100%",
8
9
  height: "auto", //If we use CSS to change the size of image, we should include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio.
9
10
  });
11
+
12
+ export default LazyImage;
@@ -42,7 +42,6 @@ const MENU_ITEMS = [
42
42
  interface Props {
43
43
  mobile?: boolean;
44
44
  }
45
-
46
45
  const User = ({ mobile = false }: Props) => {
47
46
  const { data: session, status } = useSession();
48
47
 
@@ -115,7 +114,7 @@ const User = ({ mobile = false }: Props) => {
115
114
 
116
115
  const setAnchorElNull = () => setAnchorEl(null);
117
116
 
118
- if (status === "loading") return <>Loading...</>;
117
+ if (status === "loading") return <></>;
119
118
 
120
119
  if (mobile) {
121
120
  return (
@@ -6,51 +6,60 @@ import Remove from "@mui/icons-material/Remove";
6
6
  // GLOBAL CUSTOM COMPONENTS
7
7
  import { FlexBox } from "@/components/_components/flex-box/index";
8
8
  import { Paragraph } from "@/components/_components/Typography";
9
+ import { CircularProgress } from "@mui/material";
9
10
 
10
11
  // ==============================================================
11
12
  interface Props {
12
13
  quantity: number;
13
14
  handleDecrement: () => void;
14
15
  handleIncrement: () => void;
16
+ isLineUpdating: boolean;
15
17
  }
16
18
  // ==============================================================
17
19
 
18
20
  export default function QuantityButtons(props: Props) {
19
- const { quantity, handleDecrement, handleIncrement } = props || {};
21
+ const { quantity, handleDecrement, handleIncrement, isLineUpdating } =
22
+ props || {};
20
23
 
21
24
  return (
22
25
  <FlexBox
23
- width="30px"
24
26
  alignItems="center"
25
27
  className="add-cart"
26
- flexDirection="column-reverse"
28
+ flexDirection="row-reverse"
27
29
  justifyContent={quantity ? "space-between" : "flex-start"}
30
+ gap={1}
28
31
  >
29
32
  <Button
30
33
  color="primary"
31
34
  variant="outlined"
32
35
  onClick={handleIncrement}
33
36
  sx={{ padding: "3px" }}
37
+ disabled={isLineUpdating}
34
38
  >
35
39
  <Add fontSize="small" />
36
40
  </Button>
37
41
 
38
- {quantity ? (
42
+ {quantity && (
39
43
  <Fragment>
40
- <Paragraph color="text.primary" fontWeight="600">
41
- {quantity}
42
- </Paragraph>
44
+ {isLineUpdating ? (
45
+ <CircularProgress size={"10px"} color={"primary"} />
46
+ ) : (
47
+ <Paragraph color="text.primary" fontWeight="600">
48
+ {quantity}
49
+ </Paragraph>
50
+ )}
43
51
 
44
52
  <Button
45
53
  color="primary"
46
54
  variant="outlined"
47
55
  onClick={handleDecrement}
48
56
  sx={{ padding: "3px" }}
57
+ disabled={isLineUpdating}
49
58
  >
50
59
  <Remove fontSize="small" />
51
60
  </Button>
52
61
  </Fragment>
53
- ) : null}
62
+ )}
54
63
  </FlexBox>
55
64
  );
56
65
  }
@@ -0,0 +1,304 @@
1
+ "use client";
2
+
3
+ import { IconButton } from "@mui/material";
4
+ import ProductCard1 from "./product-card";
5
+ import { Delete, ShoppingCart } from "@mui/icons-material";
6
+ import {
7
+ ExtendedProductListLine,
8
+ ExtendedWishlist,
9
+ WishlistLinesInput,
10
+ } from "@/api-manager/schemas/user.schema";
11
+ import { useTranslations } from "next-intl";
12
+ import useRemoveFromWishlist from "@/api-manager/services/user/mutations/remove-from-wishlist";
13
+ import HoverActions from "./components/hover-actions";
14
+ import { useState } from "react";
15
+ import useUpdateWishlist from "@/api-manager/services/user/mutations/update-wishlist";
16
+ import useAddToCart from "@/api-manager/services/cart/mutations/add-to-cart";
17
+ import { Cart } from "@/api-manager/schemas/cart.schema";
18
+ import { FlexBox } from "@/_components/flex-box";
19
+ import AddToWishlistDialog from "@/_components/wishlist/add-to-wishlist-dialog";
20
+ import RemoveFromWishlistDialog from "@/_components/wishlist/remove-from-wishlist-dialog";
21
+ import ProductViewDialog from "@/_components/products-view/product-view-dialog";
22
+ import { useNotification } from "@/components/Notifications/use-notification";
23
+
24
+ type Props = {
25
+ title: string;
26
+ slug: string;
27
+ price: number;
28
+ imgUrl: string;
29
+ rating?: number;
30
+ discount?: number;
31
+ id: string | number;
32
+ hideRating?: boolean;
33
+ hoverEffect?: boolean;
34
+ showProductSize?: boolean;
35
+ mode: "PLP" | "WishlistDetail";
36
+ isFavorite?: boolean; // when called from PLP, whether the product is in a wishlist
37
+ wishlist?: ExtendedWishlist; // when called from wishlist detail page
38
+ cart?: Cart; // for adding wishlist item to cart in wishlist detail page
39
+ };
40
+ export default function ProductCard1Wrapper({
41
+ id,
42
+ slug,
43
+ title,
44
+ price,
45
+ imgUrl,
46
+ rating = 5,
47
+ hideRating,
48
+ hoverEffect,
49
+ discount = 5,
50
+ showProductSize,
51
+ mode,
52
+ isFavorite,
53
+ wishlist,
54
+ cart,
55
+ }: Props) {
56
+ const t = useTranslations("Account.Wishlists");
57
+ const { setNotification } = useNotification();
58
+ const { mutateAsync: removeFromWishlist } = useRemoveFromWishlist();
59
+ const { mutateAsync: updateWishlist } = useUpdateWishlist();
60
+ const [favorite, setFavorite] = useState(isFavorite || false);
61
+ const [openAddToWishlistDialog, setOpenAddToWishlistDialog] = useState(false);
62
+ const [openRemoveFromWishlistDialog, setOpenRemoveFromWishlistDialog] =
63
+ useState(false);
64
+ const [openProductModal, setOpenProductModal] = useState(false);
65
+ const [quantity, setQuantity] = useState(
66
+ wishlist?.ProductListLines?.find(
67
+ (item: ExtendedProductListLine) => item.ProductId === id,
68
+ )?.Quantity || 0,
69
+ );
70
+ const [isLineUpdating, setIsLineUpdating] = useState(false);
71
+ const { mutateAsync: addToCart } = useAddToCart();
72
+
73
+ const handleRemoveFromWishlist = async (productId: string | number) => {
74
+ try {
75
+ const extendedWishlistLine = wishlist?.ProductListLines?.find(
76
+ (item: ExtendedProductListLine) => item.ProductId === productId,
77
+ );
78
+
79
+ if (extendedWishlistLine && wishlist) {
80
+ const { ...wishlistLine } = extendedWishlistLine;
81
+ delete wishlistLine.Product;
82
+
83
+ const wishlistLinesInput: WishlistLinesInput = {
84
+ Id: wishlist.Id,
85
+ ProductListLines: [wishlistLine],
86
+ };
87
+
88
+ await removeFromWishlist(wishlistLinesInput);
89
+ setNotification({
90
+ message: t("wishlistLineRemoveSuccess"),
91
+ severity: "success",
92
+ });
93
+ }
94
+ } catch (error) {
95
+ setNotification({
96
+ message: t("wishlistLineRemoveFailed"),
97
+ severity: "error",
98
+ });
99
+ }
100
+ };
101
+
102
+ const handleToggleFavorite = () => {
103
+ if (favorite) {
104
+ setOpenRemoveFromWishlistDialog(true);
105
+ } else {
106
+ setOpenAddToWishlistDialog(true);
107
+ }
108
+ };
109
+
110
+ const handleToggleProductModal = () => {
111
+ setOpenProductModal(!openProductModal);
112
+ };
113
+
114
+ const handleCloseDialog = () => {
115
+ setOpenAddToWishlistDialog(false);
116
+ setOpenRemoveFromWishlistDialog(false);
117
+ };
118
+
119
+ const handleAddRemoveSuccess = () => {
120
+ setFavorite(!favorite);
121
+ };
122
+
123
+ const handleWishlistIncrementQuantity = async () => {
124
+ setIsLineUpdating(true);
125
+ // update wishlist product list lines
126
+ if (mode === "WishlistDetail" && wishlist && wishlist.ProductListLines) {
127
+ try {
128
+ const updatedProductListLines = wishlist.ProductListLines.map(
129
+ (line) => {
130
+ const { ...wishlistLine } = line;
131
+ delete wishlistLine.Product;
132
+ if (line.ProductId === Number(id)) {
133
+ return {
134
+ ...wishlistLine,
135
+ Quantity: quantity + 1,
136
+ };
137
+ }
138
+ return wishlistLine;
139
+ },
140
+ );
141
+ await updateWishlist({
142
+ Id: wishlist.Id,
143
+ ProductListLines: updatedProductListLines,
144
+ });
145
+ setQuantity(quantity + 1);
146
+ } catch (error) {
147
+ // Do nothing
148
+ }
149
+ } else {
150
+ // PLP mode
151
+ setQuantity(quantity + 1);
152
+ }
153
+ setIsLineUpdating(false);
154
+ };
155
+
156
+ const handleWishlistDecrementQuantity = async () => {
157
+ setIsLineUpdating(true);
158
+ // update wishlist product list lines
159
+ if (
160
+ mode === "WishlistDetail" &&
161
+ wishlist &&
162
+ wishlist.ProductListLines &&
163
+ quantity > 0
164
+ ) {
165
+ try {
166
+ const updatedProductListLines = wishlist.ProductListLines.map(
167
+ (line) => {
168
+ const { ...wishlistLine } = line;
169
+ delete wishlistLine.Product;
170
+ if (line.ProductId === Number(id)) {
171
+ return {
172
+ ...wishlistLine,
173
+ Quantity: quantity - 1,
174
+ };
175
+ }
176
+ return wishlistLine;
177
+ },
178
+ );
179
+ await updateWishlist({
180
+ Id: wishlist.Id,
181
+ ProductListLines: updatedProductListLines,
182
+ });
183
+ setQuantity(quantity - 1);
184
+ } catch (error) {
185
+ // Do nothing
186
+ }
187
+ } else {
188
+ // PLP mode
189
+ setQuantity(quantity - 1);
190
+ }
191
+ setIsLineUpdating(false);
192
+ };
193
+
194
+ const handleAddItemToCart = async () => {
195
+ if (!cart) return;
196
+ try {
197
+ await addToCart({
198
+ cartId: cart.Id,
199
+ version: cart.Version,
200
+ cartLine: [
201
+ {
202
+ ProductId: Number(id),
203
+ Quantity: quantity,
204
+ },
205
+ ],
206
+ });
207
+ setNotification({
208
+ message: "Successfully added item to cart",
209
+ severity: "success",
210
+ });
211
+ } catch (error) {
212
+ setNotification({
213
+ message: "Failed to add item to cart",
214
+ severity: "error",
215
+ });
216
+ }
217
+ };
218
+
219
+ // render wishlist controls depending on mode
220
+ const wishlistControls =
221
+ mode === "WishlistDetail" ? (
222
+ // render delete button
223
+ <>
224
+ <FlexBox
225
+ width="25px"
226
+ alignItems="center"
227
+ flexDirection="row-reverse"
228
+ justifyContent="space-between"
229
+ >
230
+ <IconButton
231
+ sx={{ padding: "3px" }}
232
+ onClick={() => handleAddItemToCart()}
233
+ >
234
+ <ShoppingCart fontSize="small" color="inherit" />
235
+ </IconButton>
236
+ </FlexBox>
237
+ <FlexBox
238
+ width="25px"
239
+ alignItems="center"
240
+ flexDirection="row-reverse"
241
+ justifyContent="space-between"
242
+ >
243
+ <IconButton
244
+ sx={{ padding: "3px" }}
245
+ onClick={() => handleRemoveFromWishlist(id)}
246
+ >
247
+ <Delete fontSize="small" color="inherit" />
248
+ </IconButton>
249
+ </FlexBox>
250
+ </>
251
+ ) : (
252
+ // render hover actions for PLP
253
+ <HoverActions
254
+ isFavorite={favorite}
255
+ toggleView={handleToggleProductModal}
256
+ toggleFavorite={handleToggleFavorite}
257
+ />
258
+ );
259
+
260
+ return (
261
+ <>
262
+ <ProductCard1
263
+ id={id}
264
+ slug={slug}
265
+ title={title}
266
+ price={price}
267
+ imgUrl={imgUrl}
268
+ rating={rating}
269
+ hideRating={hideRating}
270
+ hoverEffect={hoverEffect}
271
+ discount={discount}
272
+ showProductSize={showProductSize}
273
+ wishlistControls={wishlistControls}
274
+ quantity={quantity}
275
+ isLineUpdating={isLineUpdating}
276
+ handleIncrementQuantity={handleWishlistIncrementQuantity}
277
+ handleDecrementQuantity={handleWishlistDecrementQuantity}
278
+ />
279
+ {openAddToWishlistDialog && (
280
+ <AddToWishlistDialog
281
+ open={openAddToWishlistDialog}
282
+ handleClose={handleCloseDialog}
283
+ productId={Number(id)}
284
+ onSuccess={handleAddRemoveSuccess}
285
+ />
286
+ )}
287
+ {openRemoveFromWishlistDialog && (
288
+ <RemoveFromWishlistDialog
289
+ open={openRemoveFromWishlistDialog}
290
+ handleClose={handleCloseDialog}
291
+ productId={Number(id)}
292
+ onSuccess={handleAddRemoveSuccess}
293
+ />
294
+ )}
295
+ {openProductModal && (
296
+ <ProductViewDialog
297
+ openDialog={openProductModal || false}
298
+ handleCloseDialog={handleToggleProductModal}
299
+ product={{ title, price, id, slug, imgGroup: [imgUrl, imgUrl] }}
300
+ />
301
+ )}
302
+ </>
303
+ );
304
+ }