@evenicanpm/storefront-core 2.3.0 → 2.3.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.
- package/package.json +3 -3
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +29 -14
- package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
- package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
- package/src/cms/blocks/components/footer/index.tsx +9 -71
- package/src/cms/blocks/components/footer/interfaces.ts +4 -0
- package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
- package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
- package/src/components/_tests_/site-logo.test.tsx +1 -1
- package/src/components/header/components/user.tsx +4 -5
- package/src/components/header/sticky-header.tsx +1 -0
- package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
- package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
- package/src/components/site-logo.tsx +1 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
- package/src/pages/account/account-navigation.tsx +2 -2
- package/src/pages/account/account-routes.ts +38 -4
- package/src/pages/account/table-row-skeleton.tsx +31 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +4 -1
- package/src/pages/blog/blog-detail-view.tsx +3 -1
- package/src/pages/cms-page-view.tsx +1 -1
- package/src/pages/product-details/product-description.tsx +25 -13
- package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
- package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
- package/src/pages/product-details/product-tabs.tsx +20 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evenicanpm/storefront-core",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Core module for D365/e4 Headless Storefront",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"author": "Evenica",
|
|
17
17
|
"license": "ISC",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@evenicanpm/cms": "^2.3.
|
|
19
|
+
"@evenicanpm/cms": "^2.3.1",
|
|
20
20
|
"@evenicanpm/ui": "^2.0.0",
|
|
21
21
|
"strapi-sdk-js": "^3.0.0"
|
|
22
22
|
},
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"esbuild": "0.19.11"
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "7a5a6a8c3c3dd9efe96fbbb884b21e7681d32550"
|
|
58
58
|
}
|
|
@@ -320,22 +320,37 @@ class D365UserDatasource extends D365DatasourceBase implements UserApi {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
async addProductsToWishlist(input: WishlistLinesInput): Promise<Wishlist> {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
return tracer.startActiveSpan(
|
|
324
|
+
"d365.addProductsToWishlist",
|
|
325
|
+
async (span) => {
|
|
326
|
+
const context = this.context;
|
|
327
|
+
const id = input.Id;
|
|
328
|
+
const catalogId = this.context.requestContext.apiSettings.catalogId;
|
|
326
329
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
productListLines
|
|
332
|
-
|
|
330
|
+
const productListLines = input.ProductListLines.map((line) => ({
|
|
331
|
+
...line,
|
|
332
|
+
CatalogId: catalogId,
|
|
333
|
+
}));
|
|
334
|
+
console.log(productListLines);
|
|
335
|
+
try {
|
|
336
|
+
const updatedProductListLines = await addProductListLinesAsync(
|
|
337
|
+
this.getRetailContext(context),
|
|
338
|
+
id,
|
|
339
|
+
productListLines,
|
|
340
|
+
);
|
|
333
341
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
342
|
+
const wishlist = {
|
|
343
|
+
Id: id,
|
|
344
|
+
productListLines: updatedProductListLines,
|
|
345
|
+
};
|
|
346
|
+
return wishlist;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
throw new Error(`Error adding to wishlist: ${error}`);
|
|
349
|
+
} finally {
|
|
350
|
+
span.end();
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
);
|
|
339
354
|
}
|
|
340
355
|
|
|
341
356
|
async removeProductsFromWishlist(input: WishlistLinesInput): Promise<void> {
|
|
@@ -3428,6 +3428,7 @@ export type Mutation = {
|
|
|
3428
3428
|
reindex: IndexAttemptResponse;
|
|
3429
3429
|
relatedAccountCreate?: Maybe<RelatedAccountCreateMutationResponse>;
|
|
3430
3430
|
relatedAccountDelete?: Maybe<RelatedAccountDeleteMutationResponse>;
|
|
3431
|
+
removeIndexHistory?: Maybe<Scalars['ID']['output']>;
|
|
3431
3432
|
/** Removes role and returns removed role */
|
|
3432
3433
|
removeRole?: Maybe<Role>;
|
|
3433
3434
|
/**
|
|
@@ -4290,6 +4291,12 @@ export type MutationRelatedAccountDeleteArgs = {
|
|
|
4290
4291
|
};
|
|
4291
4292
|
|
|
4292
4293
|
|
|
4294
|
+
export type MutationRemoveIndexHistoryArgs = {
|
|
4295
|
+
id: Scalars['ID']['input'];
|
|
4296
|
+
index?: InputMaybe<Scalars['String']['input']>;
|
|
4297
|
+
};
|
|
4298
|
+
|
|
4299
|
+
|
|
4293
4300
|
export type MutationRemoveRoleArgs = {
|
|
4294
4301
|
input: RemoveRoleInput;
|
|
4295
4302
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { FooterBlock } from "@evenicanpm/cms/src/content/footer/types";
|
|
2
|
+
import { Box, Container } from "@mui/material";
|
|
3
|
+
import Grid from "@mui/material/Grid2";
|
|
4
|
+
import Contact from "@/cms/blocks/components/footer/sections/footer-contact";
|
|
5
|
+
import Links from "@/cms/blocks/components/footer/sections/footer-links";
|
|
6
|
+
import Logo from "@/cms/blocks/components/footer/sections/footer-logo";
|
|
7
|
+
|
|
8
|
+
const getBlockComponent = (block: FooterBlock, index: number) => {
|
|
9
|
+
switch (block.__component) {
|
|
10
|
+
case "footer.links":
|
|
11
|
+
return (
|
|
12
|
+
<Links key={`index-${index}`} title={block.title} links={block.links} />
|
|
13
|
+
);
|
|
14
|
+
case "footer.logo":
|
|
15
|
+
return (
|
|
16
|
+
<Logo
|
|
17
|
+
key={`index-${index}`}
|
|
18
|
+
logoImage={block.logoImage}
|
|
19
|
+
description={block.description}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
case "footer.contact":
|
|
23
|
+
return (
|
|
24
|
+
<Contact
|
|
25
|
+
key={`index-${index}`}
|
|
26
|
+
address={block.address}
|
|
27
|
+
email={block.email}
|
|
28
|
+
phoneNumber={block.phoneNumber}
|
|
29
|
+
social={block.social}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const FooterContent = ({ sections }: { sections: FooterBlock[] }) => {
|
|
38
|
+
if (!sections?.length) return null;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Box
|
|
42
|
+
component="footer"
|
|
43
|
+
bgcolor="#222935"
|
|
44
|
+
mt={{ xs: 7 }}
|
|
45
|
+
mb={{ sm: 0, xs: 7 }}
|
|
46
|
+
>
|
|
47
|
+
<Box
|
|
48
|
+
component={Container}
|
|
49
|
+
color="white"
|
|
50
|
+
overflow="hidden"
|
|
51
|
+
py={{ sm: 10, xs: 4 }}
|
|
52
|
+
>
|
|
53
|
+
<Grid container spacing={6}>
|
|
54
|
+
{sections.map((block, index) => getBlockComponent(block, index))}
|
|
55
|
+
</Grid>
|
|
56
|
+
</Box>
|
|
57
|
+
</Box>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default FooterContent;
|
|
@@ -1,86 +1,24 @@
|
|
|
1
1
|
// MUI
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
FooterBlock,
|
|
6
|
-
FooterResponse,
|
|
7
|
-
} from "@/cms/blocks/components/footer/interfaces";
|
|
8
|
-
import Contact from "@/cms/blocks/components/footer/sections/footer-contact";
|
|
9
|
-
import Links from "@/cms/blocks/components/footer/sections/footer-links";
|
|
10
|
-
// Local Custom Components
|
|
11
|
-
import Logo from "@/cms/blocks/components/footer/sections/footer-logo";
|
|
12
|
-
import cms from "@/cms/endpoints";
|
|
2
|
+
import { cms } from "@evenicanpm/cms";
|
|
3
|
+
import type { FooterBlock } from "@evenicanpm/cms/src/content/footer/types";
|
|
4
|
+
import FooterContent from "@/cms/blocks/components/footer/footer-content";
|
|
13
5
|
|
|
14
|
-
const
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
switch (block.__component) {
|
|
18
|
-
case "footer.links":
|
|
19
|
-
Block = Links;
|
|
20
|
-
return (
|
|
21
|
-
<Block key={`index-${index}`} title={block.title} links={block.links} />
|
|
22
|
-
);
|
|
23
|
-
case "footer.logo":
|
|
24
|
-
Block = Logo;
|
|
25
|
-
return <Block key={`index-${index}`} description={block.description} />;
|
|
26
|
-
case "footer.contact":
|
|
27
|
-
Block = Contact;
|
|
28
|
-
return <Block key={`index-${index}`} social={block.social} />;
|
|
29
|
-
default:
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const fetchFooterData = async (): Promise<FooterResponse | null> => {
|
|
6
|
+
const Footer = async () => {
|
|
7
|
+
let sections: FooterBlock[] | undefined;
|
|
35
8
|
try {
|
|
36
|
-
const response = await
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const text = await response.text();
|
|
45
|
-
|
|
46
|
-
if (!text) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return JSON.parse(text) as FooterResponse;
|
|
9
|
+
const response = await cms.footer.getFooter();
|
|
10
|
+
sections = (response as { data?: { sections?: FooterBlock[] } })?.data
|
|
11
|
+
?.sections;
|
|
51
12
|
} catch (error) {
|
|
52
13
|
console.error("Error fetching footer data:", error);
|
|
53
14
|
return null;
|
|
54
15
|
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const Footer = async () => {
|
|
58
|
-
const data = await fetchFooterData();
|
|
59
|
-
const sections = data?.data?.sections;
|
|
60
16
|
|
|
61
17
|
if (!sections?.length) {
|
|
62
18
|
return null;
|
|
63
19
|
}
|
|
64
20
|
|
|
65
|
-
return
|
|
66
|
-
<Box
|
|
67
|
-
component="footer"
|
|
68
|
-
bgcolor="#222935"
|
|
69
|
-
mt={{ xs: 7 }}
|
|
70
|
-
mb={{ sm: 0, xs: 7 }}
|
|
71
|
-
>
|
|
72
|
-
<Box
|
|
73
|
-
component={Container}
|
|
74
|
-
color="white"
|
|
75
|
-
overflow="hidden"
|
|
76
|
-
py={{ sm: 10, xs: 4 }}
|
|
77
|
-
>
|
|
78
|
-
<Grid container spacing={6}>
|
|
79
|
-
{sections.map((block, index) => getBlockComponent(block, index))}
|
|
80
|
-
</Grid>
|
|
81
|
-
</Box>
|
|
82
|
-
</Box>
|
|
83
|
-
);
|
|
21
|
+
return <FooterContent sections={sections} />;
|
|
84
22
|
};
|
|
85
23
|
|
|
86
24
|
export default Footer;
|
|
@@ -9,6 +9,7 @@ export interface SocialLink {
|
|
|
9
9
|
interface FooterLogoBlock {
|
|
10
10
|
id: number;
|
|
11
11
|
__component: "footer.logo";
|
|
12
|
+
logoImage?: { url: string; alternativeText?: string };
|
|
12
13
|
description: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -22,6 +23,9 @@ interface FooterLinksBlock {
|
|
|
22
23
|
interface FooterContactBlock {
|
|
23
24
|
id: number;
|
|
24
25
|
__component: "footer.contact";
|
|
26
|
+
address?: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
phoneNumber?: string;
|
|
25
29
|
social: SocialLink[];
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -7,6 +7,9 @@ import { Heading } from "@/cms/blocks/components/footer/styles/index";
|
|
|
7
7
|
interface Props {
|
|
8
8
|
isDark?: boolean;
|
|
9
9
|
title?: string;
|
|
10
|
+
address?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
phoneNumber?: string;
|
|
10
13
|
social?: SocialLink[];
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -15,17 +18,23 @@ const AboutLinks = (props: Props) => {
|
|
|
15
18
|
<Grid size={{ lg: 3, md: 6, sm: 6, xs: 12 }}>
|
|
16
19
|
<Heading>Contact Us</Heading>
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
{props.address && (
|
|
22
|
+
<Paragraph py={0.6} color="grey.500">
|
|
23
|
+
{props.address}
|
|
24
|
+
</Paragraph>
|
|
25
|
+
)}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
{props.email && (
|
|
28
|
+
<Paragraph py={0.6} color="grey.500">
|
|
29
|
+
Email: {props.email}
|
|
30
|
+
</Paragraph>
|
|
31
|
+
)}
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
{props.phoneNumber && (
|
|
34
|
+
<Paragraph py={0.6} mb={2} color="grey.500">
|
|
35
|
+
Phone: {props.phoneNumber}
|
|
36
|
+
</Paragraph>
|
|
37
|
+
)}
|
|
29
38
|
|
|
30
39
|
<SocialLinks socials={props.social} />
|
|
31
40
|
</Grid>
|
|
@@ -2,20 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
import SiteLogo from "@evenicanpm/storefront-core/src/components/site-logo";
|
|
4
4
|
import { Paragraph } from "@evenicanpm/storefront-core/src/components/Typography";
|
|
5
|
+
import { Box } from "@mui/material";
|
|
5
6
|
import Grid from "@mui/material/Grid2";
|
|
6
7
|
import useTheme from "@mui/material/styles/useTheme";
|
|
7
8
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
9
|
+
import cmsEndPoints from "@/cms/endpoints";
|
|
8
10
|
|
|
9
11
|
interface Props {
|
|
12
|
+
logoImage?: { url: string; alternativeText?: string };
|
|
10
13
|
description: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export default function LogoSection(props: Readonly<Props>) {
|
|
14
17
|
const theme = useTheme();
|
|
15
|
-
const downMd = useMediaQuery(theme.breakpoints.down(1150));
|
|
18
|
+
const downMd = useMediaQuery(theme.breakpoints.down(1150));
|
|
19
|
+
const logoHeight = downMd ? "2.125rem" : "3rem";
|
|
20
|
+
|
|
16
21
|
return (
|
|
17
22
|
<Grid size={{ lg: 3, md: 6, sm: 6, xs: 12 }}>
|
|
18
|
-
|
|
23
|
+
{props.logoImage?.url ? (
|
|
24
|
+
<Box mb={1}>
|
|
25
|
+
<img
|
|
26
|
+
src={cmsEndPoints.buildCmsMediaUrl(props.logoImage.url)}
|
|
27
|
+
alt={props.logoImage.alternativeText || "Logo"}
|
|
28
|
+
style={{ height: logoHeight, width: "auto" }}
|
|
29
|
+
/>
|
|
30
|
+
</Box>
|
|
31
|
+
) : (
|
|
32
|
+
<SiteLogo {...(downMd ? { height: "2.125rem" } : {})} />
|
|
33
|
+
)}
|
|
19
34
|
|
|
20
35
|
<Paragraph mb={2.5} color="grey.500">
|
|
21
36
|
{props.description}
|
|
@@ -15,7 +15,7 @@ vi.mock("next/link", () => ({
|
|
|
15
15
|
describe("SiteLogo", () => {
|
|
16
16
|
it("renders logo image", () => {
|
|
17
17
|
render(<SiteLogo />);
|
|
18
|
-
const logo = screen.getByAltText("
|
|
18
|
+
const logo = screen.getByAltText("e4 Logo");
|
|
19
19
|
expect(logo).toBeInTheDocument();
|
|
20
20
|
// next/image rewrites src to an optimized URL (e.g. /_next/image?url=...)
|
|
21
21
|
expect(logo.getAttribute("src")).toMatch(/logo-blue\.png/);
|
|
@@ -16,7 +16,7 @@ import T from "@evenicanpm/storefront-core/src/components/T";
|
|
|
16
16
|
import {
|
|
17
17
|
type AccountRoute,
|
|
18
18
|
type AccountRouteOverrides,
|
|
19
|
-
|
|
19
|
+
useCreateAccountRoutes,
|
|
20
20
|
} from "@evenicanpm/storefront-core/src/pages/account/account-routes";
|
|
21
21
|
// UI Components
|
|
22
22
|
import {
|
|
@@ -61,7 +61,7 @@ const routeToMenuItem = (route: AccountRoute) => ({
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
const buildMenuItems = (overrides?: AccountRouteOverrides) => {
|
|
64
|
-
const routes =
|
|
64
|
+
const routes = useCreateAccountRoutes(overrides);
|
|
65
65
|
return [
|
|
66
66
|
routeToMenuItem(routes.profile),
|
|
67
67
|
routeToMenuItem(routes.orders),
|
|
@@ -89,10 +89,9 @@ const UserContext = createContext<UserContextValue | undefined>(undefined);
|
|
|
89
89
|
|
|
90
90
|
interface Props {
|
|
91
91
|
children?: ReactNode;
|
|
92
|
-
routeOverrides?: AccountRouteOverrides;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
const User = ({ children
|
|
94
|
+
const User = ({ children }: Props) => {
|
|
96
95
|
const { data: session, isPending } = useBetterAuthSession();
|
|
97
96
|
const status = isPending
|
|
98
97
|
? "loading"
|
|
@@ -166,7 +165,7 @@ const User = ({ children, routeOverrides }: Props) => {
|
|
|
166
165
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
|
167
166
|
const open = Boolean(anchorEl);
|
|
168
167
|
|
|
169
|
-
const menuItems = buildMenuItems(
|
|
168
|
+
const menuItems = buildMenuItems();
|
|
170
169
|
|
|
171
170
|
const value: UserContextValue = {
|
|
172
171
|
isAuthenticated,
|
|
@@ -8,6 +8,7 @@ import { Sticky } from "@evenicanpm/ui";
|
|
|
8
8
|
import useTheme from "@mui/material/styles/useTheme";
|
|
9
9
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
10
10
|
import { useCallback, useState } from "react";
|
|
11
|
+
import { AccountRouteOverrides } from "../../pages/account/account-routes";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @category Implementation
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FlexBetween,
|
|
3
|
-
FlexBox,
|
|
4
|
-
} from "@evenicanpm/storefront-core/src/components/flex-box/index";
|
|
1
|
+
import { FlexBetween } from "@evenicanpm/storefront-core/src/components/flex-box/index";
|
|
5
2
|
import ImageWithFallback from "@evenicanpm/storefront-core/src/components/ImageWithFallback";
|
|
6
3
|
// LOCAL CUSTOM COMPONENTS
|
|
7
4
|
import DiscountChip from "@evenicanpm/storefront-core/src/components/product-cards/discount-chip";
|
|
@@ -9,6 +6,7 @@ import FavoriteButton from "@evenicanpm/storefront-core/src/components/product-c
|
|
|
9
6
|
// STYLED COMPONENTS
|
|
10
7
|
import {
|
|
11
8
|
HoverIconWrapper,
|
|
9
|
+
PlpImageWrapper,
|
|
12
10
|
PriceText,
|
|
13
11
|
} from "@evenicanpm/storefront-core/src/components/product-cards/product-card-plp/styles/index";
|
|
14
12
|
import QuantityButtons from "@evenicanpm/storefront-core/src/components/product-cards/quantity-buttons";
|
|
@@ -86,15 +84,9 @@ ProductCardPlp.ImageWrapper = ({ children }: { children: React.ReactNode }) => {
|
|
|
86
84
|
|
|
87
85
|
return (
|
|
88
86
|
<Link href={buildDetailUrl(title, id)}>
|
|
89
|
-
<
|
|
90
|
-
className="hover-box-container"
|
|
91
|
-
position="relative"
|
|
92
|
-
bgcolor="grey.50"
|
|
93
|
-
borderRadius={3}
|
|
94
|
-
mb={2}
|
|
95
|
-
>
|
|
87
|
+
<PlpImageWrapper className="hover-box-container">
|
|
96
88
|
{children}
|
|
97
|
-
</
|
|
89
|
+
</PlpImageWrapper>
|
|
98
90
|
</Link>
|
|
99
91
|
);
|
|
100
92
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { FlexBox } from "@evenicanpm/storefront-core/src/components/flex-box/index";
|
|
3
4
|
import styled from "@mui/material/styles/styled";
|
|
4
5
|
|
|
5
6
|
export const PriceText = styled("p")(({ theme }) => ({
|
|
@@ -17,6 +18,20 @@ export const PriceText = styled("p")(({ theme }) => ({
|
|
|
17
18
|
},
|
|
18
19
|
}));
|
|
19
20
|
|
|
21
|
+
export const PlpImageWrapper = styled(FlexBox)(({ theme }) => ({
|
|
22
|
+
position: "relative",
|
|
23
|
+
backgroundColor: theme.palette.grey[50],
|
|
24
|
+
borderRadius: theme.shape.borderRadius * 3,
|
|
25
|
+
marginBottom: theme.spacing(2),
|
|
26
|
+
|
|
27
|
+
"& img": {
|
|
28
|
+
maxHeight: 300,
|
|
29
|
+
objectFit: "contain",
|
|
30
|
+
width: "100%",
|
|
31
|
+
borderRadius: 12,
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
20
35
|
export const HoverIconWrapper = styled("div")({
|
|
21
36
|
zIndex: 2,
|
|
22
37
|
top: "7px",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/add-to-wishlist/compound/context";
|
|
10
10
|
import UseWishlist from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/add-to-wishlist/hooks/use-add-to-wishlist";
|
|
11
11
|
import { StyledTableRow } from "@evenicanpm/storefront-core/src/components/wishlist-dialogs/styles";
|
|
12
|
+
import { useCreateAccountRoutes } from "@evenicanpm/storefront-core/src/pages/account/account-routes";
|
|
12
13
|
import { Add, Delete } from "@mui/icons-material";
|
|
13
14
|
import { IconButton } from "@mui/material";
|
|
14
15
|
import { format } from "date-fns";
|
|
@@ -25,6 +26,7 @@ export default function WishlistDialogItem() {
|
|
|
25
26
|
|
|
26
27
|
const itemCtx = useContext(WishListItemContext);
|
|
27
28
|
const dialogCtx = useContext(AddToWishListDialogContext);
|
|
29
|
+
const { wishlists: wishlistsRoute } = useCreateAccountRoutes();
|
|
28
30
|
|
|
29
31
|
if (!itemCtx || !dialogCtx)
|
|
30
32
|
throw new Error("WishlistModalItem must be used within a wishlist context");
|
|
@@ -59,7 +61,7 @@ export default function WishlistDialogItem() {
|
|
|
59
61
|
}}
|
|
60
62
|
>
|
|
61
63
|
<Paragraph ellipsis sx={{ fontWeight: "bold" }}>
|
|
62
|
-
<Link href={
|
|
64
|
+
<Link href={`${wishlistsRoute.href}/${wishlist?.Id}`}>
|
|
63
65
|
{wishlist?.Name ?? ""}
|
|
64
66
|
</Link>
|
|
65
67
|
</Paragraph>
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
type AccountRoute,
|
|
14
14
|
type AccountRouteOverrides,
|
|
15
|
-
|
|
15
|
+
useCreateAccountRoutes,
|
|
16
16
|
} from "@evenicanpm/storefront-core/src/pages/account/account-routes";
|
|
17
17
|
// STYLED COMPONENTS
|
|
18
18
|
import {
|
|
@@ -85,7 +85,7 @@ const routeToNavItem = (route: AccountRoute): NavItem => {
|
|
|
85
85
|
export const DEFAULT_MENU_ITEMS = (
|
|
86
86
|
overrides?: AccountRouteOverrides,
|
|
87
87
|
): NavSection[] => {
|
|
88
|
-
const routes =
|
|
88
|
+
const routes = useCreateAccountRoutes(overrides);
|
|
89
89
|
return [
|
|
90
90
|
{
|
|
91
91
|
title: "DASHBOARD",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
1
2
|
import type { IconType } from "react-icons";
|
|
2
3
|
import {
|
|
3
4
|
MdFavoriteBorder as FavoriteBorder,
|
|
@@ -44,9 +45,42 @@ const defaults = {
|
|
|
44
45
|
},
|
|
45
46
|
} as const satisfies Record<string, AccountRoute>;
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Context for overriding account route configuration.
|
|
50
|
+
* Allows parent providers to customize route paths and title keys
|
|
51
|
+
* without modifying the default route definitions.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <RoutesOverrideContext.Provider value={{ wishlists: { href: "/account/order-templates", titleKey: "orderTemplates" } }}>
|
|
56
|
+
* {children}
|
|
57
|
+
* </RoutesOverrideContext.Provider>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const RoutesOverrideContext = createContext<
|
|
61
|
+
AccountRouteOverrides | undefined
|
|
62
|
+
>(undefined);
|
|
63
|
+
|
|
47
64
|
export type AccountRouteKey = keyof typeof defaults;
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Hook that builds the account routes map, merging defaults with context-level
|
|
68
|
+
* and call-site overrides. Override priority (highest wins):
|
|
69
|
+
* 1. `overrides` parameter passed directly to the hook
|
|
70
|
+
* 2. Values from {@link RoutesOverrideContext}
|
|
71
|
+
* 3. Built-in defaults
|
|
72
|
+
*
|
|
73
|
+
* @param overrides - Optional route overrides applied at the call site
|
|
74
|
+
* @returns The full set of account routes with any overrides merged in
|
|
75
|
+
*/
|
|
76
|
+
export const useCreateAccountRoutes = (overrides?: AccountRouteOverrides) => {
|
|
77
|
+
const ctxOverrides = useContext(RoutesOverrideContext);
|
|
78
|
+
return {
|
|
79
|
+
...defaults,
|
|
80
|
+
wishlists: {
|
|
81
|
+
...defaults.wishlists,
|
|
82
|
+
...ctxOverrides?.wishlists,
|
|
83
|
+
...overrides?.wishlists,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import TableRow from "@evenicanpm/storefront-core/src/pages/account/table-row";
|
|
2
|
+
import Box from "@mui/material/Box";
|
|
3
|
+
import Skeleton from "@mui/material/Skeleton";
|
|
4
|
+
import type { SxProps, Theme } from "@mui/material/styles";
|
|
5
|
+
|
|
6
|
+
interface TableRowSkeletonProps {
|
|
7
|
+
sx?: SxProps<Theme>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function TableRowSkeleton({
|
|
11
|
+
sx = { gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr" },
|
|
12
|
+
}: TableRowSkeletonProps) {
|
|
13
|
+
return (
|
|
14
|
+
<TableRow sx={sx}>
|
|
15
|
+
<Skeleton variant="text" width="60%" />
|
|
16
|
+
<Box textAlign="center">
|
|
17
|
+
<Skeleton
|
|
18
|
+
variant="rounded"
|
|
19
|
+
width={70}
|
|
20
|
+
height={24}
|
|
21
|
+
sx={{ mx: "auto" }}
|
|
22
|
+
/>
|
|
23
|
+
</Box>
|
|
24
|
+
<Skeleton variant="text" width="70%" sx={{ mx: { sm: "auto" } }} />
|
|
25
|
+
<Skeleton variant="text" width="50%" sx={{ mx: { sm: "auto" } }} />
|
|
26
|
+
<Box display={{ sm: "inline-flex", xs: "none" }} justifyContent="end">
|
|
27
|
+
<Skeleton variant="circular" width={32} height={32} />
|
|
28
|
+
</Box>
|
|
29
|
+
</TableRow>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -16,6 +16,7 @@ import { format } from "date-fns";
|
|
|
16
16
|
import { useTranslations } from "next-intl";
|
|
17
17
|
import { createContext, type ReactNode, useContext } from "react";
|
|
18
18
|
import { MdDelete as Delete } from "react-icons/md";
|
|
19
|
+
import { useCreateAccountRoutes } from "../account-routes";
|
|
19
20
|
|
|
20
21
|
interface WishlistItemContextValue {
|
|
21
22
|
wishlist: Wishlist;
|
|
@@ -76,10 +77,12 @@ WishlistItem.Name = () => {
|
|
|
76
77
|
const ctx = useContext(WishlistItemContext);
|
|
77
78
|
if (!ctx) return null;
|
|
78
79
|
const { wishlist } = ctx;
|
|
80
|
+
const { wishlists: wishlistsRoute } = useCreateAccountRoutes();
|
|
81
|
+
|
|
79
82
|
return (
|
|
80
83
|
<Paragraph ellipsis>
|
|
81
84
|
<NavLink3
|
|
82
|
-
href={
|
|
85
|
+
href={`${wishlistsRoute.href}/${wishlist?.Id}`}
|
|
83
86
|
text={wishlist?.Name ?? ""}
|
|
84
87
|
/>
|
|
85
88
|
</Paragraph>
|
|
@@ -7,7 +7,7 @@ import { useSuspenseQuery } from "@tanstack/react-query";
|
|
|
7
7
|
import Link from "next/link";
|
|
8
8
|
import { cmsQueryOptions } from "../../cms/queries";
|
|
9
9
|
import { BorderShadowCard } from "../../components/BoxShadowCard";
|
|
10
|
-
import { Display, Subtitle } from "../../components/Typography";
|
|
10
|
+
import { Display, Paragraph, Subtitle } from "../../components/Typography";
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
id: string;
|
|
@@ -29,6 +29,7 @@ export const BlogDetailView = ({ id, locale, cmsLocale }: Props) => {
|
|
|
29
29
|
const title = blogData?.title;
|
|
30
30
|
const shortDescription = blogData?.shortDescription;
|
|
31
31
|
const content = blogData?.content;
|
|
32
|
+
const author = blogData?.author;
|
|
32
33
|
|
|
33
34
|
return (
|
|
34
35
|
<Container className="mt-2 mb-2" maxWidth="xl">
|
|
@@ -128,6 +129,7 @@ export const BlogDetailView = ({ id, locale, cmsLocale }: Props) => {
|
|
|
128
129
|
</Grid>
|
|
129
130
|
<Grid size={{ xs: 12, md: 3 }} order={{ xs: 1, md: 2 }}>
|
|
130
131
|
<Box sx={{ paddingBottom: "15px" }}>
|
|
132
|
+
{author && <Paragraph sx={noSpaceSx}>{author}</Paragraph>}
|
|
131
133
|
<Subtitle sx={noSpaceSx}>Published on: {publishDate}</Subtitle>
|
|
132
134
|
</Box>
|
|
133
135
|
</Grid>
|
|
@@ -40,7 +40,7 @@ export const CmsPageView = ({ slug, cmsLocale, isDraftMode }: Props) => {
|
|
|
40
40
|
) : (
|
|
41
41
|
<Container>
|
|
42
42
|
<Paper sx={{ marginTop: "25px", padding: "50px" }} elevation={1}>
|
|
43
|
-
<H1>
|
|
43
|
+
<H1>e4 Storefront</H1>
|
|
44
44
|
<H4>
|
|
45
45
|
Add a page to the "Page" content-type in CMS to add
|
|
46
46
|
content to this page.
|
|
@@ -3,22 +3,34 @@
|
|
|
3
3
|
import { H3 } from "@evenicanpm/storefront-core/src/components/Typography";
|
|
4
4
|
import type { AttributeValue } from "@msdyn365-commerce/retail-proxy";
|
|
5
5
|
|
|
6
|
-
type Props = { attributes: AttributeValue[] };
|
|
6
|
+
type Props = { attributes: AttributeValue[]; description?: string };
|
|
7
7
|
|
|
8
|
-
export default function ProductDescription({
|
|
8
|
+
export default function ProductDescription({
|
|
9
|
+
attributes,
|
|
10
|
+
description,
|
|
11
|
+
}: Readonly<Props>) {
|
|
9
12
|
return (
|
|
10
13
|
<div>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
{description && (
|
|
15
|
+
<div style={{ marginBottom: 16 }}>
|
|
16
|
+
<p>{description}</p>
|
|
17
|
+
</div>
|
|
18
|
+
)}
|
|
19
|
+
{attributes?.length > 0 && (
|
|
20
|
+
<>
|
|
21
|
+
<H3 mb={2}>Specifications:</H3>
|
|
22
|
+
{attributes.map((attr, index) => {
|
|
23
|
+
if (attr?.TextValue && attr?.Name) {
|
|
24
|
+
return (
|
|
25
|
+
<div key={`attr-${index + 1}`}>
|
|
26
|
+
{attr.Name}: {attr.TextValue}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
})}
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
22
34
|
</div>
|
|
23
35
|
);
|
|
24
36
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@evenicanpm/storefront-core/src/pages/product-details/product-intro/compound/context";
|
|
12
12
|
import ThumbnailWithSkeleton from "@evenicanpm/storefront-core/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton";
|
|
13
13
|
import Grid from "@mui/material/Grid2";
|
|
14
|
+
import Skeleton from "@mui/material/Skeleton";
|
|
14
15
|
import type React from "react";
|
|
15
16
|
import { useContext, useState } from "react";
|
|
16
17
|
|
|
@@ -124,21 +125,38 @@ ProductImages.Main = () => {
|
|
|
124
125
|
"ProductImages.Main must be used inside a product images context",
|
|
125
126
|
);
|
|
126
127
|
const { name, images, primaryImageUrl, selectedImageIndex } = ctx;
|
|
128
|
+
const [loaded, setLoaded] = useState(false);
|
|
129
|
+
|
|
130
|
+
const src =
|
|
131
|
+
Array.isArray(images) &&
|
|
132
|
+
selectedImageIndex !== undefined &&
|
|
133
|
+
images[selectedImageIndex]
|
|
134
|
+
? images[selectedImageIndex]
|
|
135
|
+
: primaryImageUrl;
|
|
127
136
|
|
|
128
137
|
return (
|
|
129
|
-
<FlexBox
|
|
138
|
+
<FlexBox
|
|
139
|
+
borderRadius={3}
|
|
140
|
+
overflow="hidden"
|
|
141
|
+
justifyContent="center"
|
|
142
|
+
mb={6}
|
|
143
|
+
position="relative"
|
|
144
|
+
>
|
|
145
|
+
{!loaded && (
|
|
146
|
+
<Skeleton
|
|
147
|
+
variant="rounded"
|
|
148
|
+
width={300}
|
|
149
|
+
height={300}
|
|
150
|
+
sx={{ position: "absolute", inset: 0, m: "auto" }}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
130
153
|
<ImageWithFallback
|
|
131
154
|
alt={name ?? "product"}
|
|
132
155
|
width={300}
|
|
133
156
|
height={300}
|
|
134
157
|
loading="eager"
|
|
135
|
-
src={
|
|
136
|
-
|
|
137
|
-
selectedImageIndex !== undefined &&
|
|
138
|
-
images[selectedImageIndex]
|
|
139
|
-
? images[selectedImageIndex]
|
|
140
|
-
: primaryImageUrl
|
|
141
|
-
}
|
|
158
|
+
src={src}
|
|
159
|
+
onLoad={() => setLoaded(true)}
|
|
142
160
|
style={{ objectFit: "contain" }}
|
|
143
161
|
/>
|
|
144
162
|
</FlexBox>
|
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
import { Skeleton } from "@mui/material";
|
|
2
1
|
import Image from "next/image";
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
const shimmer = `<svg width="50" height="50" xmlns="http://www.w3.org/2000/svg"><rect width="50" height="50" fill="#f0f0f0"/></svg>`;
|
|
4
|
+
const blurDataURL = `data:image/svg+xml;base64,${typeof window === "undefined" ? Buffer.from(shimmer).toString("base64") : btoa(shimmer)}`;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Product thumbnail image
|
|
7
8
|
* @category Sub-Component
|
|
8
9
|
*/
|
|
9
10
|
function ThumbnailWithSkeleton({ src }: Readonly<{ src: string }>) {
|
|
10
|
-
const [loaded, setLoaded] = useState(false);
|
|
11
|
-
|
|
12
11
|
return (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}}
|
|
27
|
-
/>
|
|
28
|
-
</>
|
|
12
|
+
<Image
|
|
13
|
+
src={src}
|
|
14
|
+
alt="Product thumbnail"
|
|
15
|
+
width={50}
|
|
16
|
+
height={50}
|
|
17
|
+
placeholder="blur"
|
|
18
|
+
blurDataURL={blurDataURL}
|
|
19
|
+
style={{
|
|
20
|
+
width: "80%",
|
|
21
|
+
height: "80%",
|
|
22
|
+
objectFit: "cover",
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
29
25
|
);
|
|
30
26
|
}
|
|
31
27
|
|
|
@@ -23,11 +23,16 @@ const StyledTabs = styled(Tabs)(({ theme }) => ({
|
|
|
23
23
|
},
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
type Props = {
|
|
26
|
+
type Props = {
|
|
27
|
+
productAttributes: AttributeValue[];
|
|
28
|
+
productDescription?: string;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
};
|
|
27
31
|
|
|
28
32
|
export interface TabsContext {
|
|
29
33
|
selectedOption: number;
|
|
30
34
|
productAttributes: AttributeValue[];
|
|
35
|
+
productDescription?: string;
|
|
31
36
|
handleOptionClick: (_: React.SyntheticEvent, value: number) => void;
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -54,6 +59,7 @@ const TabsContext = createContext<TabsContext | null>(null);
|
|
|
54
59
|
*/
|
|
55
60
|
export default function ProductTabs({
|
|
56
61
|
productAttributes,
|
|
62
|
+
productDescription,
|
|
57
63
|
children,
|
|
58
64
|
}: Readonly<Props>) {
|
|
59
65
|
const [selectedOption, setSelectedOption] = useState(0);
|
|
@@ -62,7 +68,12 @@ export default function ProductTabs({
|
|
|
62
68
|
|
|
63
69
|
return (
|
|
64
70
|
<TabsContext.Provider
|
|
65
|
-
value={{
|
|
71
|
+
value={{
|
|
72
|
+
selectedOption,
|
|
73
|
+
handleOptionClick,
|
|
74
|
+
productAttributes,
|
|
75
|
+
productDescription,
|
|
76
|
+
}}
|
|
66
77
|
>
|
|
67
78
|
<Box mb={6}>{children}</Box>
|
|
68
79
|
</TabsContext.Provider>
|
|
@@ -130,6 +141,11 @@ ProductTabs.Description = () => {
|
|
|
130
141
|
throw new Error(
|
|
131
142
|
"ProductTabs.Tabs must be used inside a ProductTabsContext",
|
|
132
143
|
);
|
|
133
|
-
const { productAttributes } = ctx;
|
|
134
|
-
return
|
|
144
|
+
const { productAttributes, productDescription } = ctx;
|
|
145
|
+
return (
|
|
146
|
+
<ProductDescription
|
|
147
|
+
attributes={productAttributes}
|
|
148
|
+
description={productDescription}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
135
151
|
};
|