@evenicanpm/storefront-core 2.4.2 → 2.5.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 (142) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +90 -90
  3. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +4 -3
  4. package/src/api-manager/datasources/d365/d365-user.datasource.ts +3 -3
  5. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +1 -0
  6. package/src/api-manager/datasources/e4/address/e4-address.datasource.ts +5 -14
  7. package/src/api-manager/datasources/e4/address/e4-address.translator.ts +4 -3
  8. package/src/api-manager/datasources/e4/categories/e4-categories.translator.ts +1 -1
  9. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +29 -3
  10. package/src/api-manager/datasources/e4/product/e4-product.translator.ts +1 -1
  11. package/src/api-manager/index.ts +1 -1
  12. package/src/api-manager/lib/get-graphql-client.ts +1 -1
  13. package/src/api-manager/services/cart/mutations/add-discount-code.ts +0 -11
  14. package/src/api-manager/services/create-query.ts +13 -7
  15. package/src/api-manager/services/user/mutations/add-to-wishlist.ts +1 -1
  16. package/src/api-manager/services/user/mutations/remove-from-wishlist.ts +1 -1
  17. package/src/auth/AUTH GUIDE.md +880 -0
  18. package/src/cms/blocks/components/featured-categories.tsx +18 -15
  19. package/src/cms/blocks/components/featured-products.tsx +3 -2
  20. package/src/cms/blocks/components/footer/data/index.ts +0 -0
  21. package/src/cms/blocks/components/footer/index.tsx +0 -0
  22. package/src/cms/blocks/components/footer/sections/footer-app-store.tsx +0 -0
  23. package/src/cms/blocks/components/footer/sections/footer-contact.tsx +0 -2
  24. package/src/cms/blocks/components/footer/sections/footer-links.tsx +0 -0
  25. package/src/cms/blocks/components/footer/sections/footer-logo.tsx +2 -1
  26. package/src/cms/blocks/components/footer/sections/footer-social-links.tsx +0 -0
  27. package/src/cms/blocks/components/footer/styles/index.ts +0 -0
  28. package/src/cms/blocks/components/hero-carousel/index.tsx +0 -0
  29. package/src/cms/blocks/components/product-carousel/index.tsx +0 -0
  30. package/src/cms/blocks/components/shared/featured-product-card.tsx +0 -0
  31. package/src/cms/blocks/components/shared/product-category-item.tsx +0 -0
  32. package/src/cms/blocks/components/shared/product-grid.tsx +0 -0
  33. package/src/cms/blocks/components/shared/top-categories-card.tsx +0 -0
  34. package/src/cms/blocks/components/shared/top-rating-product-card.tsx +0 -0
  35. package/src/cms/blocks/icons/components/category.tsx +0 -0
  36. package/src/cms/blocks/icons/components/dotted-star.tsx +0 -0
  37. package/src/cms/blocks/icons/components/gift-box.tsx +0 -0
  38. package/src/cms/blocks/icons/components/light.tsx +0 -0
  39. package/src/cms/blocks/icons/components/new-arrival.tsx +0 -0
  40. package/src/cms/blocks/icons/components/rank-badge.tsx +0 -0
  41. package/src/components/SearchInput.stories.tsx +12 -10
  42. package/src/components/base-layout.tsx +42 -0
  43. package/src/components/categories/category-list/category-list.tsx +32 -12
  44. package/src/components/header/__tests__/header.test.tsx +0 -24
  45. package/src/components/header/components/mobile-header.tsx +16 -8
  46. package/src/components/header/header.tsx +6 -3
  47. package/src/components/mini-cart/components/bottom-actions.tsx +7 -2
  48. package/src/components/mini-cart/components/cart-item.tsx +16 -13
  49. package/src/components/mini-cart/components/empty-view.tsx +12 -6
  50. package/src/components/mini-cart/components/top-header.tsx +9 -8
  51. package/src/components/mini-cart/mini-cart-trigger.tsx +11 -5
  52. package/src/components/mini-cart/mini-cart.tsx +30 -15
  53. package/src/components/mobile-navigation/mobile-navigation-bar.tsx +6 -2
  54. package/src/components/navbar/categories.tsx +8 -2
  55. package/src/components/navbar/category-based-menu/category-based-menu.tsx +6 -2
  56. package/src/components/navbar/mega-menu/mega-menu.tsx +22 -14
  57. package/src/components/navbar/mobile-menu/mobile-menu.test.tsx +8 -11
  58. package/src/components/navbar/mobile-menu/mobile-menu.tsx +13 -11
  59. package/src/components/navbar/nav-list/nav-list.tsx +3 -2
  60. package/src/components/navbar/navbar.tsx +4 -2
  61. package/src/components/product-cards/product-card/product-card.tsx +50 -26
  62. package/src/components/product-cards/product-card-plp/product-card.tsx +37 -16
  63. package/src/components/product-cards/product-card-plp-list/product-card.tsx +38 -16
  64. package/src/components/product-cards/product-card-search/product-card.tsx +34 -13
  65. package/src/components/product-dialog/ProductDialog.stories.tsx +20 -18
  66. package/src/components/product-dialog/compound/product-dialog.tsx +7 -4
  67. package/src/components/product-dimensions/compound/dimensions-group.tsx +4 -2
  68. package/src/components/product-dimensions/compound/dimensions.tsx +15 -6
  69. package/src/components/product-quantity-buttons/compound/quantity-buttons-root.tsx +21 -13
  70. package/src/components/products-view/compound/products-grid-view.tsx +11 -4
  71. package/src/components/products-view/compound/products-list-view.tsx +11 -4
  72. package/src/components/search-bar/compound/search-bar-root.tsx +6 -4
  73. package/src/components/search-bar/compound/textfield-adornment-category.tsx +25 -8
  74. package/src/components/search-bar/hooks/use-click-outside.ts +7 -1
  75. package/src/components/search-bar/hooks/use-search.ts +3 -3
  76. package/src/components/user/index.ts +3 -0
  77. package/src/components/user/use-user-auth.ts +163 -0
  78. package/src/components/user/user.tsx +215 -0
  79. package/src/components/wishlist-dialogs/add-to-wishlist/AddToWishlist.stories.tsx +20 -15
  80. package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +11 -8
  81. package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-list.tsx +18 -3
  82. package/src/components/wishlist-dialogs/add-to-wishlist/index.tsx +0 -1
  83. package/src/components/wishlist-dialogs/create-wishlist/compound/create-wishlist-dialog.tsx +6 -2
  84. package/src/hooks/use-overflow-detect.ts +4 -4
  85. package/src/lib/graphqlRequestSdk.ts +0 -0
  86. package/src/lib/page-slugs.ts +16 -0
  87. package/src/pages/account/account-navigation.tsx +7 -14
  88. package/src/pages/account/account-routes.ts +4 -9
  89. package/src/pages/account/addresses/address-form.tsx +38 -16
  90. package/src/pages/account/addresses/address-item.tsx +18 -8
  91. package/src/pages/account/dashboard-header.tsx +20 -13
  92. package/src/pages/account/orders/order-history-filters-panel.tsx +21 -20
  93. package/src/pages/account/orders/order-history-header.tsx +14 -4
  94. package/src/pages/account/orders/order-history-root.tsx +3 -1
  95. package/src/pages/account/orders/order-history-row.tsx +12 -4
  96. package/src/pages/account/orders/order-history-search-and-filter.tsx +88 -59
  97. package/src/pages/account/orders/order-history-search-dropdown.tsx +26 -9
  98. package/src/pages/account/orders/order-history-sort.tsx +3 -1
  99. package/src/pages/account/orders/order-history-table.tsx +27 -5
  100. package/src/pages/account/orders/order-progress.tsx +12 -6
  101. package/src/pages/account/orders/order-row.tsx +18 -6
  102. package/src/pages/account/orders/order-summary.tsx +24 -8
  103. package/src/pages/account/orders/ordered-products.tsx +15 -5
  104. package/src/pages/account/profile/user-info.tsx +11 -3
  105. package/src/pages/account/table-row-skeleton.tsx +1 -1
  106. package/src/pages/account/wishlist/create-new-list.tsx +33 -9
  107. package/src/pages/account/wishlist/wishlist-item.tsx +18 -6
  108. package/src/pages/cart/cart-item.tsx +21 -7
  109. package/src/pages/cart/checkout-form.tsx +55 -22
  110. package/src/pages/cart/coupon-entry.tsx +143 -36
  111. package/src/pages/cart/estimate-shipping.tsx +45 -21
  112. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +22 -14
  113. package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +37 -16
  114. package/src/pages/checkout/checkout-alt-form/steps/address/address-card.tsx +9 -3
  115. package/src/pages/checkout/checkout-alt-form/steps/address/delivery-address.tsx +84 -51
  116. package/src/pages/checkout/checkout-alt-form/steps/address/new-address-form.tsx +41 -19
  117. package/src/pages/checkout/checkout-alt-form/steps/customer-info/customer-information.tsx +17 -10
  118. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +15 -6
  119. package/src/pages/checkout/checkout-alt-form/steps/shipping/delivery-options.tsx +6 -2
  120. package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +12 -4
  121. package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +12 -5
  122. package/src/pages/checkout/checkout-alt-summary/list-item.tsx +17 -10
  123. package/src/pages/confirmation/address.tsx +6 -2
  124. package/src/pages/confirmation/confirmation-summary.tsx +12 -4
  125. package/src/pages/confirmation/ordered-products.tsx +12 -4
  126. package/src/pages/product-details/bopis/store-card/shop-card.tsx +2 -2
  127. package/src/pages/product-details/bopis/store-card/utils/getDaysResources.ts +1 -1
  128. package/src/pages/product-details/product-intro/compound/product-info.tsx +41 -18
  129. package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +18 -12
  130. package/src/pages/product-details/product-intro/compound/product-intro.tsx +20 -10
  131. package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +1 -1
  132. package/src/pages/product-details/product-tabs.tsx +20 -12
  133. package/src/pages/product-list/product-list-view.tsx +4 -2
  134. package/src/pages/product-list/search-bar.tsx +7 -7
  135. package/src/pages/product-list/side-nav.tsx +5 -3
  136. package/src/pages/quickorder/order-upload.tsx +1 -1
  137. package/src/pages/quickorder/provider.tsx +26 -14
  138. package/src/pages/quickorder/quick-order.tsx +49 -22
  139. package/src/providers/nav-provider/index.tsx +16 -11
  140. package/tsconfig.json +0 -5
  141. package/src/components/header/components/user.tsx +0 -322
  142. /package/src/components/{header → user}/__tests__/user.test.tsx +0 -0
@@ -18,7 +18,13 @@ import { withZodSchema } from "formik-validator-zod";
18
18
  import isFunction from "lodash/isFunction";
19
19
  import { useTranslations } from "next-intl";
20
20
  import type React from "react";
21
- import { createContext, type ReactNode, useContext, useEffect } from "react";
21
+ import {
22
+ createContext,
23
+ type ReactNode,
24
+ useContext,
25
+ useEffect,
26
+ useMemo,
27
+ } from "react";
22
28
  import { z } from "zod";
23
29
 
24
30
  interface CreateNewListContextValue {
@@ -74,9 +80,13 @@ const CreateNewList = ({
74
80
  const handleClose = () => {
75
81
  if (isFunction(onClose)) onClose();
76
82
  };
83
+ const contextValue = useMemo(
84
+ () => ({ formik, handleClose, t }),
85
+ [formik, handleClose, t],
86
+ );
77
87
 
78
88
  return (
79
- <CreateNewListContext.Provider value={{ formik, handleClose, t }}>
89
+ <CreateNewListContext.Provider value={contextValue}>
80
90
  <Dialog
81
91
  open={open}
82
92
  onClose={handleClose}
@@ -88,14 +98,16 @@ const CreateNewList = ({
88
98
  );
89
99
  };
90
100
 
91
- CreateNewList.Title = () => {
101
+ const CreateNewListTitle = () => {
92
102
  const ctx = useContext(CreateNewListContext);
93
103
  if (!ctx) return null;
94
104
  const { t } = ctx;
95
105
  return <H5 mb={4}>{t("formTitle")}</H5>;
96
106
  };
97
107
 
98
- CreateNewList.Form = ({ children }: { children: React.ReactNode }) => {
108
+ CreateNewList.Title = CreateNewListTitle;
109
+
110
+ const CreateNewListForm = ({ children }: { children: React.ReactNode }) => {
99
111
  const ctx = useContext(CreateNewListContext);
100
112
  if (!ctx) return null;
101
113
  const { formik } = ctx;
@@ -108,7 +120,9 @@ CreateNewList.Form = ({ children }: { children: React.ReactNode }) => {
108
120
  );
109
121
  };
110
122
 
111
- CreateNewList.NameField = () => {
123
+ CreateNewList.Form = CreateNewListForm;
124
+
125
+ const CreateNewListNameField = () => {
112
126
  const ctx = useContext(CreateNewListContext);
113
127
  if (!ctx) return null;
114
128
  const { formik, t } = ctx;
@@ -129,7 +143,9 @@ CreateNewList.NameField = () => {
129
143
  );
130
144
  };
131
145
 
132
- CreateNewList.CommentField = () => {
146
+ CreateNewList.NameField = CreateNewListNameField;
147
+
148
+ const CreateNewListCommentField = () => {
133
149
  const ctx = useContext(CreateNewListContext);
134
150
  if (!ctx) return null;
135
151
  const { formik, t } = ctx;
@@ -149,7 +165,9 @@ CreateNewList.CommentField = () => {
149
165
  );
150
166
  };
151
167
 
152
- CreateNewList.PrivacyField = () => {
168
+ CreateNewList.CommentField = CreateNewListCommentField;
169
+
170
+ const CreateNewListPrivacyField = () => {
153
171
  const ctx = useContext(CreateNewListContext);
154
172
  if (!ctx) return null;
155
173
  const { formik, t } = ctx;
@@ -170,6 +188,8 @@ CreateNewList.PrivacyField = () => {
170
188
  );
171
189
  };
172
190
 
191
+ CreateNewList.PrivacyField = CreateNewListPrivacyField;
192
+
173
193
  CreateNewList.Actions = ({ children }: { children: React.ReactNode }) => {
174
194
  return (
175
195
  <Grid
@@ -186,7 +206,7 @@ CreateNewList.Actions = ({ children }: { children: React.ReactNode }) => {
186
206
  );
187
207
  };
188
208
 
189
- CreateNewList.Save = () => {
209
+ const CreateNewListSave = () => {
190
210
  const ctx = useContext(CreateNewListContext);
191
211
  if (!ctx) return null;
192
212
  const { t } = ctx;
@@ -203,7 +223,9 @@ CreateNewList.Save = () => {
203
223
  );
204
224
  };
205
225
 
206
- CreateNewList.Cancel = () => {
226
+ CreateNewList.Save = CreateNewListSave;
227
+
228
+ const CreateNewListCancel = () => {
207
229
  const ctx = useContext(CreateNewListContext);
208
230
  if (!ctx) return null;
209
231
  const { handleClose, t } = ctx;
@@ -220,4 +242,6 @@ CreateNewList.Cancel = () => {
220
242
  );
221
243
  };
222
244
 
245
+ CreateNewList.Cancel = CreateNewListCancel;
246
+
223
247
  export default CreateNewList;
@@ -14,7 +14,7 @@ import { Box, IconButton } from "@mui/material";
14
14
  import type { SxProps } from "@mui/material/styles";
15
15
  import { format } from "date-fns";
16
16
  import { useTranslations } from "next-intl";
17
- import { createContext, type ReactNode, useContext } from "react";
17
+ import { createContext, type ReactNode, useContext, useMemo } from "react";
18
18
  import { MdDelete as Delete } from "react-icons/md";
19
19
  import { useCreateAccountRoutes } from "../account-routes";
20
20
 
@@ -54,9 +54,13 @@ const WishlistItem = ({ wishlist, children, sx }: RootProps) => {
54
54
  console.log(error);
55
55
  }
56
56
  };
57
+ const contextValue = useMemo(
58
+ () => ({ wishlist, handleDelete }),
59
+ [wishlist, handleDelete],
60
+ );
57
61
 
58
62
  return (
59
- <WishlistItemContext.Provider value={{ wishlist, handleDelete }}>
63
+ <WishlistItemContext.Provider value={contextValue}>
60
64
  <TableRow
61
65
  sx={{
62
66
  display: "grid",
@@ -73,7 +77,7 @@ const WishlistItem = ({ wishlist, children, sx }: RootProps) => {
73
77
  );
74
78
  };
75
79
 
76
- WishlistItem.Name = () => {
80
+ const WishlistItemName = () => {
77
81
  const { wishlists: wishlistsRoute } = useCreateAccountRoutes();
78
82
  const ctx = useContext(WishlistItemContext);
79
83
  if (!ctx) return null;
@@ -88,7 +92,9 @@ WishlistItem.Name = () => {
88
92
  );
89
93
  };
90
94
 
91
- WishlistItem.Date = () => {
95
+ WishlistItem.Name = WishlistItemName;
96
+
97
+ const WishlistItemDate = () => {
92
98
  const ctx = useContext(WishlistItemContext);
93
99
  if (!ctx) return null;
94
100
  const { wishlist } = ctx;
@@ -102,7 +108,9 @@ WishlistItem.Date = () => {
102
108
  );
103
109
  };
104
110
 
105
- WishlistItem.ProductCount = () => {
111
+ WishlistItem.Date = WishlistItemDate;
112
+
113
+ const WishlistItemProductCount = () => {
106
114
  const ctx = useContext(WishlistItemContext);
107
115
  const t = useTranslations("Account.Wishlists");
108
116
  if (!ctx) return null;
@@ -114,6 +122,8 @@ WishlistItem.ProductCount = () => {
114
122
  );
115
123
  };
116
124
 
125
+ WishlistItem.ProductCount = WishlistItemProductCount;
126
+
117
127
  WishlistItem.Actions = ({ children }: { children?: ReactNode }) => {
118
128
  return (
119
129
  <Box display="flex" alignItems="center" gap={1} justifySelf="end">
@@ -122,7 +132,7 @@ WishlistItem.Actions = ({ children }: { children?: ReactNode }) => {
122
132
  );
123
133
  };
124
134
 
125
- WishlistItem.Delete = () => {
135
+ const WishlistItemDelete = () => {
126
136
  const ctx = useContext(WishlistItemContext);
127
137
  if (!ctx) return null;
128
138
  const { wishlist, handleDelete } = ctx;
@@ -133,4 +143,6 @@ WishlistItem.Delete = () => {
133
143
  );
134
144
  };
135
145
 
146
+ WishlistItem.Delete = WishlistItemDelete;
147
+
136
148
  export default WishlistItem;
@@ -203,7 +203,7 @@ CartItemRoot.Right = ({ children }: { children: React.ReactNode }) => {
203
203
  // --------------------
204
204
  // Functional subcomponents
205
205
  // --------------------
206
- CartItemRoot.Image = () => {
206
+ const CartItemRootImage = () => {
207
207
  const ctx = useContext(CartItemContext);
208
208
  if (!ctx) return null;
209
209
  const { cartLine } = ctx;
@@ -219,7 +219,9 @@ CartItemRoot.Image = () => {
219
219
  );
220
220
  };
221
221
 
222
- CartItemRoot.RemoveButton = () => {
222
+ CartItemRoot.Image = CartItemRootImage;
223
+
224
+ const CartItemRootRemoveButton = () => {
223
225
  const ctx = useContext(CartItemContext);
224
226
  const t = useTranslations("Cart.CartLine");
225
227
  if (!ctx) return null;
@@ -237,7 +239,9 @@ CartItemRoot.RemoveButton = () => {
237
239
  );
238
240
  };
239
241
 
240
- CartItemRoot.Title = () => {
242
+ CartItemRoot.RemoveButton = CartItemRootRemoveButton;
243
+
244
+ const CartItemRootTitle = () => {
241
245
  const ctx = useContext(CartItemContext);
242
246
  if (!ctx) return null;
243
247
  const { cartLine } = ctx;
@@ -251,7 +255,9 @@ CartItemRoot.Title = () => {
251
255
  );
252
256
  };
253
257
 
254
- CartItemRoot.Dimensions = () => {
258
+ CartItemRoot.Title = CartItemRootTitle;
259
+
260
+ const CartItemRootDimensions = () => {
255
261
  const ctx = useContext(CartItemContext);
256
262
  if (!ctx) return null;
257
263
  const { cartLine } = ctx;
@@ -271,7 +277,9 @@ CartItemRoot.Dimensions = () => {
271
277
  );
272
278
  };
273
279
 
274
- CartItemRoot.Price = () => {
280
+ CartItemRoot.Dimensions = CartItemRootDimensions;
281
+
282
+ const CartItemRootPrice = () => {
275
283
  const ctx = useContext(CartItemContext);
276
284
  const { formatCurrency } = useCurrencyFormatter();
277
285
 
@@ -321,7 +329,9 @@ CartItemRoot.Price = () => {
321
329
  );
322
330
  };
323
331
 
324
- CartItemRoot.Quantity = () => {
332
+ CartItemRoot.Price = CartItemRootPrice;
333
+
334
+ const CartItemRootQuantity = () => {
325
335
  const ctx = useContext(CartItemContext);
326
336
  const t = useTranslations("Cart.CartLine");
327
337
  if (!ctx) return null;
@@ -361,7 +371,9 @@ CartItemRoot.Quantity = () => {
361
371
  );
362
372
  };
363
373
 
364
- CartItemRoot.Comment = () => {
374
+ CartItemRoot.Quantity = CartItemRootQuantity;
375
+
376
+ const CartItemRootComment = () => {
365
377
  const ctx = useContext(CartItemContext);
366
378
  const t = useTranslations("Cart.CartLine");
367
379
  if (!ctx) return null;
@@ -454,6 +466,8 @@ CartItemRoot.Comment = () => {
454
466
  return <CartItemRoot.Right>{content}</CartItemRoot.Right>;
455
467
  };
456
468
 
469
+ CartItemRoot.Comment = CartItemRootComment;
470
+
457
471
  const CartItem = Object.assign(CartItemRoot, {
458
472
  // wrappers
459
473
  Body: CartItemRoot.Body,
@@ -20,7 +20,14 @@ import { debounce } from "lodash";
20
20
  import Link from "next/link";
21
21
  import { useFormatter, useTranslations } from "next-intl";
22
22
  import type React from "react";
23
- import { createContext, useContext, useEffect, useRef, useState } from "react";
23
+ import {
24
+ createContext,
25
+ useContext,
26
+ useEffect,
27
+ useMemo,
28
+ useRef,
29
+ useState,
30
+ } from "react";
24
31
 
25
32
  interface CheckoutFormContextValue {
26
33
  cartId: string;
@@ -76,29 +83,45 @@ const CheckoutForm = ({
76
83
  }
77
84
  }, [commentFromData]);
78
85
 
86
+ const contextValue = useMemo(
87
+ () => ({
88
+ cartId,
89
+ cartLineIds,
90
+ cartTotal,
91
+ countries,
92
+ appliedCoupons,
93
+ commentFromData,
94
+ checkoutDisabled,
95
+ selectedCountry,
96
+ setSelectedCountry,
97
+ states,
98
+ updateCartComment,
99
+ textRef,
100
+ }),
101
+ [
102
+ cartId,
103
+ cartLineIds,
104
+ cartTotal,
105
+ countries,
106
+ appliedCoupons,
107
+ commentFromData,
108
+ checkoutDisabled,
109
+ selectedCountry,
110
+ setSelectedCountry,
111
+ states,
112
+ updateCartComment,
113
+ textRef,
114
+ ],
115
+ );
116
+
79
117
  return (
80
- <CheckoutFormContext.Provider
81
- value={{
82
- cartId,
83
- cartLineIds,
84
- cartTotal,
85
- countries,
86
- appliedCoupons,
87
- commentFromData,
88
- checkoutDisabled,
89
- selectedCountry,
90
- setSelectedCountry,
91
- states,
92
- updateCartComment,
93
- textRef,
94
- }}
95
- >
118
+ <CheckoutFormContext.Provider value={contextValue}>
96
119
  <Card sx={{ padding: 3 }}>{children}</Card>
97
120
  </CheckoutFormContext.Provider>
98
121
  );
99
122
  };
100
123
 
101
- CheckoutForm.Total = () => {
124
+ const CheckoutFormTotal = () => {
102
125
  const ctx = useContext(CheckoutFormContext);
103
126
  const { number } = useFormatter();
104
127
  const t = useTranslations("Cart.Sidebar");
@@ -118,7 +141,9 @@ CheckoutForm.Total = () => {
118
141
  );
119
142
  };
120
143
 
121
- CheckoutForm.Notes = () => {
144
+ CheckoutForm.Total = CheckoutFormTotal;
145
+
146
+ const CheckoutFormNotes = () => {
122
147
  const ctx = useContext(CheckoutFormContext);
123
148
  const t = useTranslations("Cart.Sidebar");
124
149
  if (!ctx) return null;
@@ -148,7 +173,9 @@ CheckoutForm.Notes = () => {
148
173
  );
149
174
  };
150
175
 
151
- CheckoutForm.Coupons = () => {
176
+ CheckoutForm.Notes = CheckoutFormNotes;
177
+
178
+ const CheckoutFormCoupons = () => {
152
179
  const ctx = useContext(CheckoutFormContext);
153
180
  if (!ctx) return null;
154
181
  const { appliedCoupons, cartId } = ctx;
@@ -165,7 +192,9 @@ CheckoutForm.Coupons = () => {
165
192
  );
166
193
  };
167
194
 
168
- CheckoutForm.EstimateShipping = () => {
195
+ CheckoutForm.Coupons = CheckoutFormCoupons;
196
+
197
+ const CheckoutFormEstimateShipping = () => {
169
198
  const ctx = useContext(CheckoutFormContext);
170
199
  if (!ctx) return null;
171
200
  const { countries, states, setSelectedCountry, cartId, cartLineIds } = ctx;
@@ -190,7 +219,9 @@ CheckoutForm.EstimateShipping = () => {
190
219
  );
191
220
  };
192
221
 
193
- CheckoutForm.CheckoutButton = () => {
222
+ CheckoutForm.EstimateShipping = CheckoutFormEstimateShipping;
223
+
224
+ const CheckoutFormCheckoutButton = () => {
194
225
  const ctx = useContext(CheckoutFormContext);
195
226
  const t = useTranslations("Cart.Sidebar");
196
227
  if (!ctx) return null;
@@ -210,4 +241,6 @@ CheckoutForm.CheckoutButton = () => {
210
241
  );
211
242
  };
212
243
 
244
+ CheckoutForm.CheckoutButton = CheckoutFormCheckoutButton;
245
+
213
246
  export default CheckoutForm;
@@ -9,6 +9,7 @@ import Close from "@mui/icons-material/Close";
9
9
  import {
10
10
  Box,
11
11
  Button,
12
+ CircularProgress,
12
13
  IconButton,
13
14
  List,
14
15
  ListItem,
@@ -17,15 +18,19 @@ import {
17
18
  } from "@mui/material";
18
19
  import { useTranslations } from "next-intl";
19
20
  import type React from "react";
20
- import { createContext, useContext, useState } from "react";
21
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
21
22
 
22
23
  interface CouponContextValue {
23
24
  textValue: string;
24
25
  setTextValue: (v: string) => void;
26
+ couponMessage: string;
27
+ couponMessageSeverity: "success" | "error";
25
28
  appliedCoupons: Coupon[];
26
29
  handleApplyCoupon: () => void;
27
30
  handleRemoveCoupon: (coupon: string) => void;
31
+ isApplyingCoupon: boolean;
28
32
  cartId: string;
33
+ setCouponMessage: (msg: string) => void;
29
34
  }
30
35
 
31
36
  const CouponContext = createContext<CouponContextValue | null>(null);
@@ -40,60 +45,139 @@ const CouponEntry = ({
40
45
  cartId,
41
46
  children,
42
47
  }: CouponEntryProps) => {
48
+ const t = useTranslations("Cart.Coupons");
43
49
  const [textValue, setTextValue] = useState("");
44
- const { mutate: updateCoupon } = useAddDiscountCode();
50
+ const [couponMessage, setCouponMessage] = useState("");
51
+ const [couponMessageSeverity, setCouponMessageSeverity] = useState<
52
+ "success" | "error"
53
+ >("error");
54
+ const { mutate: updateCoupon, isPending: isApplyingCoupon } =
55
+ useAddDiscountCode();
45
56
  const { mutate: removeCoupon } = useRemoveDiscountcode();
46
57
 
47
58
  const handleApplyCoupon = () => {
48
- if (!textValue) return;
49
- updateCoupon({ discountCode: textValue, cartId });
50
- setTextValue("");
59
+ const enteredCode = textValue.trim();
60
+ if (!enteredCode) return;
61
+ setCouponMessage("");
62
+ updateCoupon(
63
+ { discountCode: enteredCode, cartId },
64
+ {
65
+ onSuccess: (result) => {
66
+ // Treat as success only when the entered code is present in returned coupons.
67
+ const hasAppliedCoupon = (result?.Coupons ?? []).some(
68
+ (coupon) =>
69
+ (coupon?.Code ?? "").toLowerCase() === enteredCode.toLowerCase(),
70
+ );
71
+
72
+ if (!hasAppliedCoupon) {
73
+ setCouponMessageSeverity("error");
74
+ setCouponMessage(t("invalidCoupon"));
75
+ return;
76
+ }
77
+
78
+ setCouponMessageSeverity("success");
79
+ setCouponMessage(t("couponAppliedSuccessfully"));
80
+ setTextValue("");
81
+ },
82
+ onError: () => {
83
+ setCouponMessageSeverity("error");
84
+ setCouponMessage(t("failedToApplyCoupon"));
85
+ },
86
+ },
87
+ );
51
88
  };
52
89
 
53
90
  const handleRemoveCoupon = (coupon: string) => {
54
91
  removeCoupon({ discountCode: coupon, cartId });
55
92
  };
93
+ const contextValue = useMemo(
94
+ () => ({
95
+ textValue,
96
+ setTextValue,
97
+ couponMessage,
98
+ couponMessageSeverity,
99
+ appliedCoupons,
100
+ handleApplyCoupon,
101
+ handleRemoveCoupon,
102
+ isApplyingCoupon,
103
+ cartId,
104
+ setCouponMessage,
105
+ }),
106
+ [
107
+ textValue,
108
+ setTextValue,
109
+ couponMessage,
110
+ couponMessageSeverity,
111
+ appliedCoupons,
112
+ handleApplyCoupon,
113
+ handleRemoveCoupon,
114
+ isApplyingCoupon,
115
+ cartId,
116
+ setCouponMessage,
117
+ ],
118
+ );
56
119
 
57
120
  return (
58
- <CouponContext.Provider
59
- value={{
60
- textValue,
61
- setTextValue,
62
- appliedCoupons,
63
- handleApplyCoupon,
64
- handleRemoveCoupon,
65
- cartId,
66
- }}
67
- >
121
+ <CouponContext.Provider value={contextValue}>
68
122
  <Box>{children}</Box>
69
123
  </CouponContext.Provider>
70
124
  );
71
125
  };
72
126
 
73
- CouponEntry.Input = () => {
127
+ const CouponEntryInput = () => {
74
128
  const ctx = useContext(CouponContext);
75
129
  const t = useTranslations("Cart.Coupons");
130
+
131
+ // Auto-hide message after 3.5 seconds
132
+ useEffect(() => {
133
+ if (!ctx?.couponMessage) return;
134
+
135
+ const timer = setTimeout(() => {
136
+ ctx.setCouponMessage("");
137
+ }, 3500);
138
+
139
+ return () => clearTimeout(timer);
140
+ }, [ctx?.couponMessage, ctx?.setCouponMessage]);
141
+
76
142
  if (!ctx) return null;
77
- const { textValue, setTextValue } = ctx;
143
+ const { textValue, setTextValue, couponMessage, couponMessageSeverity } = ctx;
78
144
 
79
145
  return (
80
- <TextField
81
- fullWidth
82
- size="small"
83
- label={t("coupon")}
84
- variant="outlined"
85
- placeholder={t("coupon")}
86
- value={textValue}
87
- onChange={(e) => setTextValue(e.target.value)}
88
- />
146
+ <>
147
+ <TextField
148
+ fullWidth
149
+ size="small"
150
+ label={t("coupon")}
151
+ variant="outlined"
152
+ placeholder={t("coupon")}
153
+ value={textValue}
154
+ onChange={(e) => setTextValue(e.target.value)}
155
+ />
156
+ {couponMessage ? (
157
+ <Box
158
+ sx={{
159
+ mt: 1,
160
+ fontSize: 14,
161
+ color:
162
+ couponMessageSeverity === "success"
163
+ ? "success.main"
164
+ : "error.main",
165
+ }}
166
+ >
167
+ {couponMessage}
168
+ </Box>
169
+ ) : null}
170
+ </>
89
171
  );
90
172
  };
91
173
 
92
- CouponEntry.ApplyButton = () => {
174
+ CouponEntry.Input = CouponEntryInput;
175
+
176
+ const CouponEntryApplyButton = () => {
93
177
  const ctx = useContext(CouponContext);
94
178
  const t = useTranslations("Cart.Coupons");
95
179
  if (!ctx) return null;
96
- const { handleApplyCoupon } = ctx;
180
+ const { handleApplyCoupon, isApplyingCoupon, textValue } = ctx;
97
181
 
98
182
  return (
99
183
  <Button
@@ -101,14 +185,20 @@ CouponEntry.ApplyButton = () => {
101
185
  color="primary"
102
186
  fullWidth
103
187
  sx={{ mt: 2, mb: 4 }}
188
+ disabled={isApplyingCoupon || !textValue.trim()}
104
189
  onClick={handleApplyCoupon}
190
+ startIcon={
191
+ isApplyingCoupon ? <CircularProgress size={16} color="inherit" /> : null
192
+ }
105
193
  >
106
194
  {t("applyCouponButton")}
107
195
  </Button>
108
196
  );
109
197
  };
110
198
 
111
- CouponEntry.AppliedList = () => {
199
+ CouponEntry.ApplyButton = CouponEntryApplyButton;
200
+
201
+ const CouponEntryAppliedList = () => {
112
202
  const ctx = useContext(CouponContext);
113
203
  const t = useTranslations("Cart.Coupons");
114
204
  if (!ctx) return null;
@@ -117,20 +207,22 @@ CouponEntry.AppliedList = () => {
117
207
  if (!appliedCoupons.length) return null;
118
208
 
119
209
  return (
120
- <>
210
+ <Box>
121
211
  <FlexBox alignItems="center" columnGap={1} mb={2}>
122
212
  <Span fontWeight="600">{t("appliedCoupons")}</Span>
123
213
  </FlexBox>
124
- <List>
214
+ <List sx={{ p: 0, display: "flex", flexDirection: "column", gap: 1 }}>
125
215
  {appliedCoupons.map((coupon) => (
126
216
  <CouponEntry.AppliedItem key={coupon.Code} coupon={coupon} />
127
217
  ))}
128
218
  </List>
129
- </>
219
+ </Box>
130
220
  );
131
221
  };
132
222
 
133
- CouponEntry.AppliedItem = ({ coupon }: { coupon: Coupon }) => {
223
+ CouponEntry.AppliedList = CouponEntryAppliedList;
224
+
225
+ const CouponEntryAppliedItem = ({ coupon }: { coupon: Coupon }) => {
134
226
  const ctx = useContext(CouponContext);
135
227
  if (!ctx) return null;
136
228
  const { handleRemoveCoupon } = ctx;
@@ -139,17 +231,32 @@ CouponEntry.AppliedItem = ({ coupon }: { coupon: Coupon }) => {
139
231
  secondaryAction={
140
232
  <IconButton
141
233
  edge="end"
142
- aria-label="delete"
234
+ size="small"
143
235
  onClick={() => handleRemoveCoupon(coupon.Code ?? "")}
144
236
  title="Remove Coupon"
237
+ sx={{ ml: 1 }}
145
238
  >
146
- <Close />
239
+ <Close fontSize="small" />
147
240
  </IconButton>
148
241
  }
242
+ sx={{
243
+ width: "100%",
244
+ px: 1.5,
245
+ py: 1,
246
+ border: "1px solid",
247
+ borderColor: "primary.main",
248
+ borderRadius: 2,
249
+ backgroundColor: "background.paper",
250
+ "&:hover": {
251
+ backgroundColor: "action.hover",
252
+ },
253
+ }}
149
254
  >
150
- <ListItemText primary={coupon.Code} />
255
+ <ListItemText primary={coupon.Code} sx={{ my: 0, fontSize: 14 }} />
151
256
  </ListItem>
152
257
  );
153
258
  };
154
259
 
260
+ CouponEntry.AppliedItem = CouponEntryAppliedItem;
261
+
155
262
  export default CouponEntry;