@evenicanpm/storefront-core 2.4.1 → 2.5.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 (122) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  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/sections/footer-contact.tsx +0 -2
  21. package/src/cms/blocks/components/footer/sections/footer-logo.tsx +2 -1
  22. package/src/components/SearchInput.stories.tsx +12 -10
  23. package/src/components/base-layout.tsx +42 -0
  24. package/src/components/categories/category-list/category-list.tsx +32 -12
  25. package/src/components/header/__tests__/header.test.tsx +0 -24
  26. package/src/components/header/components/mobile-header.tsx +16 -8
  27. package/src/components/header/header.tsx +6 -3
  28. package/src/components/mini-cart/components/bottom-actions.tsx +7 -2
  29. package/src/components/mini-cart/components/cart-item.tsx +16 -13
  30. package/src/components/mini-cart/components/empty-view.tsx +12 -6
  31. package/src/components/mini-cart/components/top-header.tsx +9 -8
  32. package/src/components/mini-cart/mini-cart-trigger.tsx +11 -5
  33. package/src/components/mini-cart/mini-cart.tsx +30 -15
  34. package/src/components/mobile-navigation/mobile-navigation-bar.tsx +6 -2
  35. package/src/components/navbar/categories.tsx +8 -2
  36. package/src/components/navbar/category-based-menu/category-based-menu.tsx +6 -2
  37. package/src/components/navbar/mega-menu/mega-menu.tsx +22 -14
  38. package/src/components/navbar/mobile-menu/mobile-menu.test.tsx +8 -11
  39. package/src/components/navbar/mobile-menu/mobile-menu.tsx +13 -11
  40. package/src/components/navbar/nav-list/nav-list.tsx +3 -2
  41. package/src/components/navbar/navbar.tsx +4 -2
  42. package/src/components/product-cards/product-card/product-card.tsx +50 -26
  43. package/src/components/product-cards/product-card-plp/product-card.tsx +37 -16
  44. package/src/components/product-cards/product-card-plp-list/product-card.tsx +38 -16
  45. package/src/components/product-cards/product-card-search/product-card.tsx +34 -13
  46. package/src/components/product-dialog/ProductDialog.stories.tsx +20 -18
  47. package/src/components/product-dialog/compound/product-dialog.tsx +7 -4
  48. package/src/components/product-dimensions/compound/dimensions-group.tsx +4 -2
  49. package/src/components/product-dimensions/compound/dimensions.tsx +15 -6
  50. package/src/components/product-quantity-buttons/compound/quantity-buttons-root.tsx +21 -13
  51. package/src/components/products-view/compound/products-grid-view.tsx +11 -4
  52. package/src/components/products-view/compound/products-list-view.tsx +11 -4
  53. package/src/components/search-bar/compound/search-bar-root.tsx +6 -4
  54. package/src/components/search-bar/compound/textfield-adornment-category.tsx +25 -8
  55. package/src/components/search-bar/hooks/use-click-outside.ts +7 -1
  56. package/src/components/search-bar/hooks/use-search.ts +3 -3
  57. package/src/components/user/index.ts +3 -0
  58. package/src/components/user/use-user-auth.ts +163 -0
  59. package/src/components/user/user.tsx +215 -0
  60. package/src/components/wishlist-dialogs/add-to-wishlist/AddToWishlist.stories.tsx +20 -15
  61. package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +11 -8
  62. package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-list.tsx +18 -3
  63. package/src/components/wishlist-dialogs/add-to-wishlist/index.tsx +0 -1
  64. package/src/components/wishlist-dialogs/create-wishlist/compound/create-wishlist-dialog.tsx +6 -2
  65. package/src/hooks/use-overflow-detect.ts +4 -4
  66. package/src/lib/page-slugs.ts +16 -0
  67. package/src/pages/account/account-navigation.tsx +7 -14
  68. package/src/pages/account/account-routes.ts +4 -9
  69. package/src/pages/account/addresses/address-form.tsx +38 -16
  70. package/src/pages/account/addresses/address-item.tsx +18 -8
  71. package/src/pages/account/dashboard-header.tsx +20 -13
  72. package/src/pages/account/orders/order-history-filters-panel.tsx +21 -20
  73. package/src/pages/account/orders/order-history-header.tsx +14 -4
  74. package/src/pages/account/orders/order-history-root.tsx +3 -1
  75. package/src/pages/account/orders/order-history-row.tsx +12 -4
  76. package/src/pages/account/orders/order-history-search-and-filter.tsx +88 -59
  77. package/src/pages/account/orders/order-history-search-dropdown.tsx +26 -9
  78. package/src/pages/account/orders/order-history-sort.tsx +3 -1
  79. package/src/pages/account/orders/order-history-table.tsx +27 -5
  80. package/src/pages/account/orders/order-progress.tsx +12 -6
  81. package/src/pages/account/orders/order-row.tsx +18 -6
  82. package/src/pages/account/orders/order-summary.tsx +24 -8
  83. package/src/pages/account/orders/ordered-products.tsx +15 -5
  84. package/src/pages/account/profile/user-info.tsx +11 -3
  85. package/src/pages/account/table-row-skeleton.tsx +1 -1
  86. package/src/pages/account/wishlist/create-new-list.tsx +33 -9
  87. package/src/pages/account/wishlist/wishlist-item.tsx +18 -6
  88. package/src/pages/cart/cart-item.tsx +21 -7
  89. package/src/pages/cart/checkout-form.tsx +55 -22
  90. package/src/pages/cart/coupon-entry.tsx +143 -36
  91. package/src/pages/cart/estimate-shipping.tsx +45 -21
  92. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +22 -14
  93. package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +37 -16
  94. package/src/pages/checkout/checkout-alt-form/steps/address/address-card.tsx +9 -3
  95. package/src/pages/checkout/checkout-alt-form/steps/address/delivery-address.tsx +84 -51
  96. package/src/pages/checkout/checkout-alt-form/steps/address/new-address-form.tsx +41 -19
  97. package/src/pages/checkout/checkout-alt-form/steps/customer-info/customer-information.tsx +17 -10
  98. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +15 -6
  99. package/src/pages/checkout/checkout-alt-form/steps/shipping/delivery-options.tsx +6 -2
  100. package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +12 -4
  101. package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +12 -5
  102. package/src/pages/checkout/checkout-alt-summary/list-item.tsx +17 -10
  103. package/src/pages/confirmation/address.tsx +6 -2
  104. package/src/pages/confirmation/confirmation-summary.tsx +12 -4
  105. package/src/pages/confirmation/ordered-products.tsx +12 -4
  106. package/src/pages/product-details/bopis/store-card/shop-card.tsx +2 -2
  107. package/src/pages/product-details/bopis/store-card/utils/getDaysResources.ts +1 -1
  108. package/src/pages/product-details/product-intro/compound/product-info.tsx +41 -18
  109. package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +18 -12
  110. package/src/pages/product-details/product-intro/compound/product-intro.tsx +20 -10
  111. package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +1 -1
  112. package/src/pages/product-details/product-tabs.tsx +20 -12
  113. package/src/pages/product-list/product-list-view.tsx +4 -2
  114. package/src/pages/product-list/search-bar.tsx +7 -7
  115. package/src/pages/product-list/side-nav.tsx +5 -3
  116. package/src/pages/quickorder/order-upload.tsx +1 -1
  117. package/src/pages/quickorder/provider.tsx +26 -14
  118. package/src/pages/quickorder/quick-order.tsx +49 -22
  119. package/src/providers/nav-provider/index.tsx +16 -11
  120. package/tsconfig.json +0 -5
  121. package/src/components/header/components/user.tsx +0 -322
  122. /package/src/components/{header → user}/__tests__/user.test.tsx +0 -0
@@ -1,322 +0,0 @@
1
- "use client";
2
-
3
- import useSessionInit from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-init";
4
- import useSessionLogout from "@evenicanpm/storefront-core/src/api-manager/services/session/mutations/session-logout";
5
- import useUpdateUser from "@evenicanpm/storefront-core/src/api-manager/services/user/mutations/update-user";
6
- import getUser from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-user";
7
- import signOutCustom from "@evenicanpm/storefront-core/src/auth/signout";
8
- import MobileUserIcon from "@evenicanpm/storefront-core/src/components/icons/user";
9
- import { NavLink } from "@evenicanpm/storefront-core/src/components/nav-link";
10
- import T from "@evenicanpm/storefront-core/src/components/T";
11
- import {
12
- type AccountRoute,
13
- type AccountRouteOverrides,
14
- useCreateAccountRoutes,
15
- } from "@evenicanpm/storefront-core/src/pages/account/account-routes";
16
- // UI Components
17
- import {
18
- Box,
19
- Button,
20
- IconButton,
21
- List,
22
- ListItemButton,
23
- Popover,
24
- } from "@mui/material";
25
- import {
26
- customSessionClient,
27
- genericOAuthClient,
28
- } from "better-auth/client/plugins";
29
- // Auth components
30
- import { createAuthClient } from "better-auth/react";
31
- import { useRouter } from "next/navigation";
32
-
33
- const authClient = createAuthClient({
34
- plugins: [genericOAuthClient(), customSessionClient()],
35
- });
36
- const { useSession: useBetterAuthSession } = authClient;
37
-
38
- import type React from "react";
39
- import {
40
- createContext,
41
- type MouseEvent,
42
- type ReactNode,
43
- useContext,
44
- useEffect,
45
- useRef,
46
- useState,
47
- } from "react";
48
- import { MdPersonOutline as PersonOutline } from "react-icons/md";
49
-
50
- const routeToMenuItem = (route: AccountRoute) => ({
51
- href: route.href,
52
- label: `Account.Menu.${route.titleKey}`,
53
- Icon: route.Icon,
54
- count: route.count,
55
- });
56
-
57
- const buildMenuItems = (overrides?: AccountRouteOverrides) => {
58
- const routes = useCreateAccountRoutes(overrides);
59
- return [
60
- routeToMenuItem(routes.profile),
61
- routeToMenuItem(routes.orders),
62
- routeToMenuItem(routes.wishlists),
63
- routeToMenuItem(routes.invoices),
64
- routeToMenuItem(routes.addresses),
65
- ];
66
- };
67
-
68
- type MenuItem = { href: string; label: string };
69
-
70
- interface UserContextValue {
71
- isAuthenticated: boolean;
72
- status: string;
73
- menuItems: MenuItem[];
74
- handleLogout: () => Promise<void>;
75
- handleSignIn: () => void;
76
- signOut: () => Promise<void>;
77
- anchorEl: HTMLButtonElement | null;
78
- setAnchorEl: (el: HTMLButtonElement | null) => void;
79
- open: boolean;
80
- }
81
-
82
- const UserContext = createContext<UserContextValue | undefined>(undefined);
83
-
84
- interface Props {
85
- children?: ReactNode;
86
- }
87
-
88
- const User = ({ children }: Props) => {
89
- const { data: session, isPending } = useBetterAuthSession();
90
- const status = isPending
91
- ? "loading"
92
- : session
93
- ? "authenticated"
94
- : "unauthenticated";
95
-
96
- if (session === null && !isPending) {
97
- // No error handling needed - session is simply null when not authenticated
98
- }
99
-
100
- const isAuthenticated = !!session;
101
- const isE4Datasource = (process.env.API_MANAGER_PRIMARY ?? "e4") === "e4";
102
- const isD365Datasource = !isE4Datasource;
103
- const { mutateAsync: sessionInit } = useSessionInit();
104
- const { mutateAsync: sessionLogout } = useSessionLogout();
105
- const { mutateAsync: updateUser } = useUpdateUser();
106
- const syncedUserKeyRef = useRef<string>("");
107
- // Register the "me" query on the client so the hydrated cache entry has a queryFn for refetches
108
- const { data: currentUser, isPending: isUserPending } = getUser.useData();
109
-
110
- // session init
111
- useEffect(() => {
112
- const accessToken =
113
- (session?.session as { accessToken?: string } | undefined)?.accessToken ||
114
- "";
115
-
116
- if (
117
- isE4Datasource &&
118
- isAuthenticated &&
119
- session?.user?.email &&
120
- accessToken
121
- ) {
122
- sessionInit({ accessToken, email: session.user.email }).catch(
123
- console.error,
124
- );
125
- }
126
- }, [isE4Datasource, isAuthenticated, session, sessionInit]);
127
-
128
- // For D365, only seed profile details from the auth session when D365 does not
129
- // already have a name. This avoids overwriting explicit profile edits with a
130
- // stale session.user.name snapshot.
131
- useEffect(() => {
132
- if (!isD365Datasource || !isAuthenticated || isUserPending) return;
133
-
134
- const existingFirstName =
135
- typeof currentUser?.FirstName === "string"
136
- ? currentUser.FirstName.trim()
137
- : "";
138
- const existingLastName =
139
- typeof currentUser?.LastName === "string"
140
- ? currentUser.LastName.trim()
141
- : "";
142
- if (existingFirstName || existingLastName) return;
143
-
144
- const fullName =
145
- typeof session?.user?.name === "string" ? session.user.name.trim() : "";
146
- const [firstName = "", ...lastParts] = fullName
147
- .split(/\s+/)
148
- .filter(Boolean);
149
- const lastName = lastParts.join(" ");
150
-
151
- if (!firstName && !lastName) return;
152
-
153
- const syncKey = `${session?.user?.email || ""}:${firstName}:${lastName}`;
154
- if (syncedUserKeyRef.current === syncKey) return;
155
-
156
- syncedUserKeyRef.current = syncKey;
157
- updateUser({
158
- AccountNumber: "",
159
- Email: session?.user?.email || undefined,
160
- FirstName: firstName || undefined,
161
- LastName: lastName || undefined,
162
- }).catch((error) => {
163
- // Allow retry on next render when sync fails.
164
- syncedUserKeyRef.current = "";
165
- console.warn("D365 user sync error:", error);
166
- });
167
- }, [
168
- currentUser,
169
- isD365Datasource,
170
- isAuthenticated,
171
- isUserPending,
172
- session,
173
- updateUser,
174
- ]);
175
-
176
- const handleLogout = async () => {
177
- try {
178
- await sessionLogout({});
179
- signOutCustom();
180
- } catch (err) {
181
- console.error("Logout Error:", err);
182
- }
183
- };
184
-
185
- const handleSignIn = () => {
186
- const callbackUrl = encodeURIComponent(
187
- globalThis.location.pathname + globalThis.location.search,
188
- );
189
- globalThis.location.href = `/login?callbackUrl=${callbackUrl}`;
190
- };
191
-
192
- const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
193
- const open = Boolean(anchorEl);
194
-
195
- const menuItems = buildMenuItems();
196
-
197
- const value: UserContextValue = {
198
- isAuthenticated,
199
- status,
200
- menuItems,
201
- handleLogout,
202
- handleSignIn,
203
- signOut: signOutCustom,
204
- anchorEl,
205
- setAnchorEl,
206
- open,
207
- };
208
-
209
- if (status === "loading") return null;
210
-
211
- return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
212
- };
213
-
214
- // Subcomponents
215
- User.MobileTrigger = function MobileTrigger() {
216
- const ctx = useContext(UserContext);
217
- const router = useRouter();
218
- if (!ctx) return null;
219
- const { isAuthenticated, handleSignIn } = ctx;
220
- return (
221
- <IconButton
222
- aria-label="User Menu"
223
- onClick={() => {
224
- if (isAuthenticated) router.push("/account/profile");
225
- else handleSignIn();
226
- }}
227
- >
228
- <MobileUserIcon style={{ fontSize: "1.25rem" }} />
229
- </IconButton>
230
- );
231
- };
232
-
233
- User.DesktopTrigger = function DesktopTrigger() {
234
- const ctx = useContext(UserContext);
235
- if (!ctx) return null;
236
- const { setAnchorEl } = ctx;
237
- return (
238
- <IconButton
239
- aria-label="User Menu"
240
- onClick={(e: MouseEvent<HTMLButtonElement>) =>
241
- setAnchorEl(e.currentTarget)
242
- }
243
- >
244
- <PersonOutline />
245
- </IconButton>
246
- );
247
- };
248
-
249
- User.Menu = function Menu({
250
- children,
251
- }: {
252
- children?: (items: MenuItem[]) => React.ReactNode;
253
- }) {
254
- const ctx = useContext(UserContext);
255
- if (!ctx) return null;
256
- const { anchorEl, open, setAnchorEl, menuItems, handleLogout } = ctx;
257
- const id = open ? "account-popover" : undefined;
258
-
259
- return (
260
- <Popover
261
- id={id}
262
- open={open}
263
- anchorEl={anchorEl}
264
- onClose={() => setAnchorEl(null)}
265
- anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
266
- transformOrigin={{ vertical: "top", horizontal: "right" }}
267
- >
268
- <List sx={{ p: 1 }} onClick={() => setAnchorEl(null)}>
269
- {children
270
- ? children(menuItems) // Allow caller to control what gets rendered
271
- : menuItems.map(({ href, label }) => (
272
- <User.MenuItem key={href} href={href} label={label} />
273
- ))}
274
- <Box p={1}>
275
- <Button
276
- fullWidth
277
- size="small"
278
- color="primary"
279
- variant="outlined"
280
- onClick={handleLogout}
281
- >
282
- <T path="Account.Menu.btnLogout" />
283
- </Button>
284
- </Box>
285
- </List>
286
- </Popover>
287
- );
288
- };
289
-
290
- User.MenuItem = function MenuItem({
291
- href,
292
- label,
293
- }: {
294
- href: string;
295
- label: string;
296
- }) {
297
- return (
298
- <NavLink href={href} style={{ color: "inherit" }}>
299
- <ListItemButton>
300
- <T path={label} />
301
- </ListItemButton>
302
- </NavLink>
303
- );
304
- };
305
-
306
- User.LoginButton = function LoginButton() {
307
- const ctx = useContext(UserContext);
308
- if (!ctx) return null;
309
- const { handleSignIn } = ctx;
310
- return (
311
- <Button
312
- size="small"
313
- variant="contained"
314
- color="primary"
315
- onClick={() => handleSignIn()}
316
- >
317
- <T path="Account.Menu.btnLogin" />
318
- </Button>
319
- );
320
- };
321
-
322
- export default User;