@evenicanpm/storefront-core 2.2.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.
Files changed (80) hide show
  1. package/package.json +7 -3
  2. package/src/api-manager/README.md +52 -27
  3. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
  4. package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
  6. package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
  7. package/src/api-manager/datasources/d365/d365-user.datasource.ts +125 -36
  8. package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
  9. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +10 -3
  10. package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
  11. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
  12. package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
  13. package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
  14. package/src/api-manager/index.ts +54 -3
  15. package/src/api-manager/lib/get-graphql-client.ts +11 -5
  16. package/src/api-manager/schemas/cart.schema.ts +10 -1
  17. package/src/api-manager/schemas/invoice.schema.ts +55 -0
  18. package/src/api-manager/schemas/order.schema.ts +13 -1
  19. package/src/api-manager/schemas/user.schema.ts +8 -0
  20. package/src/api-manager/services/create-query.ts +4 -10
  21. package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
  22. package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
  23. package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
  24. package/src/api-manager/services/order/queries/get-orders.ts +6 -3
  25. package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
  26. package/src/api-manager/types/Datasource.ts +21 -2
  27. package/src/auth/better-auth.ts +1 -1
  28. package/src/cms/blocks/block-manager.tsx +18 -3
  29. package/src/cms/blocks/components/cta-banner.tsx +65 -0
  30. package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
  31. package/src/cms/blocks/components/footer/index.tsx +9 -71
  32. package/src/cms/blocks/components/footer/interfaces.ts +4 -0
  33. package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
  34. package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
  35. package/src/cms/blocks/components/hero-image.tsx +83 -0
  36. package/src/cms/blocks/index.tsx +2 -0
  37. package/src/cms/blocks/interfaces.ts +20 -0
  38. package/src/cms/draft-mode-badge.tsx +26 -0
  39. package/src/cms/queries.ts +81 -0
  40. package/src/components/_tests_/site-logo.test.tsx +1 -1
  41. package/src/components/flex-box/flex-box.tsx +2 -0
  42. package/src/components/header/components/user.tsx +35 -11
  43. package/src/components/header/sticky-header.tsx +1 -0
  44. package/src/components/mini-cart/mini-cart.tsx +3 -3
  45. package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
  46. package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
  47. package/src/components/site-logo.tsx +1 -1
  48. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +1 -0
  49. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
  50. package/src/pages/account/account-navigation.tsx +88 -60
  51. package/src/pages/account/account-routes.ts +86 -0
  52. package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
  53. package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
  54. package/src/pages/account/orders/order-history-header.tsx +54 -0
  55. package/src/pages/account/orders/order-history-pagination.tsx +55 -0
  56. package/src/pages/account/orders/order-history-root.tsx +125 -0
  57. package/src/pages/account/orders/order-history-row.tsx +110 -0
  58. package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
  59. package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
  60. package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
  61. package/src/pages/account/orders/order-history-sort.tsx +90 -0
  62. package/src/pages/account/orders/order-history-table.tsx +54 -0
  63. package/src/pages/account/orders/order-row.tsx +2 -2
  64. package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
  65. package/src/pages/account/table-header.tsx +20 -0
  66. package/src/pages/account/table-row-skeleton.tsx +31 -0
  67. package/src/pages/account/wishlist/wishlist-item.tsx +4 -1
  68. package/src/pages/blog/blog-card.tsx +9 -3
  69. package/src/pages/blog/blog-detail-view.tsx +152 -0
  70. package/src/pages/blog/blog-list-view.tsx +59 -0
  71. package/src/pages/cart/estimate-shipping.tsx +6 -5
  72. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
  73. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
  74. package/src/pages/cms-page-view.tsx +53 -0
  75. package/src/pages/product-details/product-description.tsx +25 -13
  76. package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
  77. package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
  78. package/src/pages/product-details/product-tabs.tsx +20 -4
  79. package/src/pages/quickorder/order-upload.tsx +12 -6
  80. package/src/pages/quickorder/quick-order.tsx +25 -20
@@ -0,0 +1,20 @@
1
+ import type {
2
+ CustomerBalances,
3
+ GetCustomerBalanceInput,
4
+ } from "@evenicanpm/storefront-core/src/api-manager/schemas/user.schema";
5
+ import {
6
+ createApiPath,
7
+ createQuery,
8
+ } from "@evenicanpm/storefront-core/src/api-manager/services/create-query";
9
+
10
+ const apiPath = createApiPath("user", "getCustomerBalance");
11
+
12
+ const getCustomerBalance = createQuery<
13
+ GetCustomerBalanceInput,
14
+ CustomerBalances
15
+ >(apiPath, {
16
+ queryKey: "customerBalance",
17
+ disableProperties: ["accountNumber"],
18
+ });
19
+
20
+ export default getCustomerBalance;
@@ -7,10 +7,16 @@ import type {
7
7
  GetStatesInput,
8
8
  } from "@evenicanpm/storefront-core/src/api-manager/schemas/address.schema";
9
9
  import type * as cartSchemas from "@evenicanpm/storefront-core/src/api-manager/schemas/cart.schema";
10
+ import type {
11
+ GetInvoicesDetailsInput,
12
+ GetInvoicesInput,
13
+ Invoice,
14
+ InvoiceDetails,
15
+ } from "@evenicanpm/storefront-core/src/api-manager/schemas/invoice.schema";
10
16
  import type {
11
17
  GetSalesOrderDetailsInput,
18
+ GetSalesOrdersInput,
12
19
  SalesOrder,
13
- SalesOrderInput,
14
20
  } from "@evenicanpm/storefront-core/src/api-manager/schemas/order.schema";
15
21
  import type {
16
22
  ChannelConfiguration,
@@ -47,6 +53,7 @@ export interface DatasourceApis {
47
53
  order: OrderApi;
48
54
  address: AddressApi;
49
55
  organization: OrganizationApi;
56
+ invoice: InvoiceApi;
50
57
  }
51
58
 
52
59
  export abstract class Datasource {
@@ -59,6 +66,7 @@ export abstract class Datasource {
59
66
  abstract address: Partial<AddressApi>;
60
67
  abstract organization: Partial<OrganizationApi>;
61
68
  abstract session: Partial<SessionApi>;
69
+ abstract invoice: Partial<InvoiceApi>;
62
70
 
63
71
  constructor(name: string) {
64
72
  this.name = name;
@@ -89,6 +97,9 @@ export interface UserApi {
89
97
  updateUser(user: userSchemas.User): Promise<userSchemas.User>;
90
98
  getUser(): Promise<userSchemas.User | null>;
91
99
  getUserCounts(userId: string): Promise<userSchemas.UserCounts>;
100
+ getCustomerBalance(
101
+ input: userSchemas.GetCustomerBalanceInput,
102
+ ): Promise<userSchemas.CustomerBalances>;
92
103
  // wishlists
93
104
  getWishlists(): Promise<userSchemas.Wishlist[]>;
94
105
  getWishlistDetails(
@@ -199,7 +210,7 @@ export interface CategoryApi {
199
210
  * Order Data Actions
200
211
  */
201
212
  export interface OrderApi {
202
- getSalesOrders(input: SalesOrderInput): Promise<SalesOrder[]>;
213
+ getSalesOrders(input: GetSalesOrdersInput): Promise<SalesOrder[]>;
203
214
  getSalesOrderDetails(input: GetSalesOrderDetailsInput): Promise<SalesOrder>;
204
215
  }
205
216
 
@@ -214,3 +225,11 @@ export interface AddressApi {
214
225
  updateAddress(address: Address): Promise<Address>;
215
226
  deleteAddress(addressId: string): Promise<string>;
216
227
  }
228
+
229
+ /**
230
+ * Invoice Data Actions
231
+ */
232
+ export interface InvoiceApi {
233
+ getInvoices(input: GetInvoicesInput): Promise<Invoice[]>;
234
+ getInvoiceDetails(input: GetInvoicesDetailsInput): Promise<InvoiceDetails>;
235
+ }
@@ -1,6 +1,6 @@
1
1
  import { betterAuth } from "better-auth";
2
- import { genericOAuth, customSession } from "better-auth/plugins";
3
2
  import { nextCookies } from "better-auth/next-js";
3
+ import { customSession, genericOAuth } from "better-auth/plugins";
4
4
 
5
5
  const tenantName = process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_TENANT_NAME || "";
6
6
  const loginAuthority = `https://${tenantName}.ciamlogin.com/${tenantName}.onmicrosoft.com`;
@@ -3,12 +3,14 @@ import type { Block } from "@/cms/blocks/interfaces";
3
3
  import {
4
4
  BrandLogosScroller,
5
5
  CategoryCarousel,
6
+ CtaBanner,
6
7
  FeaturedCategories,
7
8
  FeaturedProducts,
8
9
  FeaturePill,
9
10
  Footer,
10
11
  Hero,
11
12
  HeroCarousel,
13
+ HeroImage,
12
14
  Image,
13
15
  ProductCarousel,
14
16
  ProductSectionFullWidth,
@@ -18,6 +20,8 @@ import {
18
20
 
19
21
  const componentMap = {
20
22
  "blocks.hero-section": Hero,
23
+ "blocks.hero-image": HeroImage,
24
+ "blocks.cta-banner": CtaBanner,
21
25
  "blocks.features-pill": FeaturePill, // Should replace this with bazaar-react-ts/src/pages-sections/market-2/section-2/section-2.tsx
22
26
  "blocks.featured-products": FeaturedProducts,
23
27
  "blocks.featured-categories": FeaturedCategories,
@@ -40,8 +44,16 @@ const getBlockComponent: <B extends Block>(
40
44
  ) => JSX.Element | null = (block: Block) => {
41
45
  const Component = componentMap[
42
46
  block.__component as keyof typeof componentMap
43
- ] as React.ComponentType<Block>;
44
- return <Component key={`block-${block.id}`} {...block} />;
47
+ ] as React.ComponentType<Block> | undefined;
48
+ if (!Component) {
49
+ console.warn(
50
+ `BlockManager: no component mapped for "${block.__component}"`,
51
+ );
52
+ return null;
53
+ }
54
+ return (
55
+ <Component key={`block-${block.__component}-${block.id}`} {...block} />
56
+ );
45
57
  };
46
58
 
47
59
  interface BlockManagerProps {
@@ -55,7 +67,10 @@ const BlockManager: React.FC<BlockManagerProps> = ({ blocks }) => {
55
67
  {blocks.map((block, idx) => {
56
68
  const isLast = idx === blocks.length - 1;
57
69
  return (
58
- <Box key={`block-wrapper-${block.id}`} mb={isLast ? 0 : 6}>
70
+ <Box
71
+ key={`block-wrapper-${block.__component}-${block.id}`}
72
+ mb={isLast ? 0 : 6}
73
+ >
59
74
  {getBlockComponent(block, idx)}
60
75
  </Box>
61
76
  );
@@ -0,0 +1,65 @@
1
+ import { Box, Button, Container, Typography } from "@mui/material";
2
+ import NextLink from "next/link";
3
+ import type { CtaBannerBlock } from "@/cms/blocks/interfaces";
4
+
5
+ const CtaBanner = ({
6
+ heading,
7
+ subtext,
8
+ buttonText,
9
+ buttonLink,
10
+ backgroundColor,
11
+ }: CtaBannerBlock) => {
12
+ const bgColor = backgroundColor || "#1a237e";
13
+
14
+ return (
15
+ <Box
16
+ sx={{
17
+ backgroundColor: bgColor,
18
+ color: "#fff",
19
+ py: { xs: 6, md: 8 },
20
+ px: 2,
21
+ textAlign: "center",
22
+ width: "100%",
23
+ }}
24
+ >
25
+ <Container maxWidth="md">
26
+ <Typography
27
+ variant="h3"
28
+ component="h2"
29
+ sx={{ fontWeight: 700, mb: subtext ? 2 : 3, color: "#fff" }}
30
+ >
31
+ {heading}
32
+ </Typography>
33
+ {subtext && (
34
+ <Typography
35
+ variant="h6"
36
+ sx={{ color: "rgba(255,255,255,0.85)", mb: 3, fontWeight: 400 }}
37
+ >
38
+ {subtext}
39
+ </Typography>
40
+ )}
41
+ {buttonText && buttonLink && (
42
+ <Button
43
+ component={NextLink}
44
+ href={buttonLink}
45
+ variant="contained"
46
+ size="large"
47
+ sx={{
48
+ backgroundColor: "#fff",
49
+ color: bgColor,
50
+ fontWeight: 700,
51
+ px: 4,
52
+ "&:hover": {
53
+ backgroundColor: "rgba(255,255,255,0.9)",
54
+ },
55
+ }}
56
+ >
57
+ {buttonText}
58
+ </Button>
59
+ )}
60
+ </Container>
61
+ </Box>
62
+ );
63
+ };
64
+
65
+ export default CtaBanner;
@@ -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 { Box, Container } from "@mui/material";
3
- import Grid from "@mui/material/Grid2";
4
- import type {
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 getBlockComponent = (block: FooterBlock, index: number) => {
15
- let Block: typeof Links | typeof Logo | typeof Contact;
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 fetch(cms.getCmsApiUrl("footer"), {
37
- cache: "no-store", // Ensure server-side fetching
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
- <Paragraph py={0.6} color="grey.500">
19
- 70 Washington Square South, New York, NY 10012, United States
20
- </Paragraph>
21
+ {props.address && (
22
+ <Paragraph py={0.6} color="grey.500">
23
+ {props.address}
24
+ </Paragraph>
25
+ )}
21
26
 
22
- <Paragraph py={0.6} color="grey.500">
23
- Email: evenica@example.org
24
- </Paragraph>
27
+ {props.email && (
28
+ <Paragraph py={0.6} color="grey.500">
29
+ Email: {props.email}
30
+ </Paragraph>
31
+ )}
25
32
 
26
- <Paragraph py={0.6} mb={2} color="grey.500">
27
- Phone: +1 1123 456 780
28
- </Paragraph>
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)); // same like in header
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
- <SiteLogo {...(downMd ? { height: "2.125rem" } : {})} />
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}
@@ -0,0 +1,83 @@
1
+ import { Box, Button, Container, Typography } from "@mui/material";
2
+ import NextImage from "next/image";
3
+ import NextLink from "next/link";
4
+ import type { HeroImageBlock } from "@/cms/blocks/interfaces";
5
+
6
+ const sxBanner = {
7
+ position: "relative",
8
+ backgroundColor: "grey.900",
9
+ color: "white",
10
+ height: 500,
11
+ width: "100%",
12
+ display: "flex",
13
+ alignItems: "center",
14
+ justifyContent: "center",
15
+ overflow: "hidden",
16
+ marginBottom: "50px",
17
+ };
18
+
19
+ const contentContainer = {
20
+ position: "absolute",
21
+ top: 0,
22
+ left: 0,
23
+ right: 0,
24
+ bottom: 0,
25
+ display: "flex",
26
+ flexDirection: "column",
27
+ justifyContent: "center",
28
+ alignItems: "center",
29
+ zIndex: 2,
30
+ textAlign: "center",
31
+ padding: "0 20px",
32
+ };
33
+
34
+ const overlayStyle = {
35
+ position: "absolute",
36
+ inset: 0,
37
+ backgroundColor: "rgba(10,25,60,0.55)",
38
+ zIndex: 1,
39
+ };
40
+
41
+ const HeroImage = ({
42
+ heading,
43
+ subheading,
44
+ buttonText,
45
+ buttonLink,
46
+ image,
47
+ }: HeroImageBlock) => {
48
+ const imageUrl = image?.url;
49
+
50
+ return (
51
+ <Box sx={sxBanner}>
52
+ {imageUrl && (
53
+ <NextImage
54
+ src={imageUrl}
55
+ alt={heading}
56
+ fill
57
+ style={{ objectFit: "cover", objectPosition: "center" }}
58
+ priority
59
+ />
60
+ )}
61
+ <Box sx={overlayStyle} />
62
+ <Container sx={contentContainer}>
63
+ <Typography variant="h1" gutterBottom>
64
+ {heading}
65
+ </Typography>
66
+ {subheading && (
67
+ <Typography variant="h5" gutterBottom sx={{ marginBottom: "30px" }}>
68
+ {subheading}
69
+ </Typography>
70
+ )}
71
+ {buttonText && buttonLink && (
72
+ <NextLink href={buttonLink} passHref>
73
+ <Button color="primary" size="large" variant="contained">
74
+ {buttonText}
75
+ </Button>
76
+ </NextLink>
77
+ )}
78
+ </Container>
79
+ </Box>
80
+ );
81
+ };
82
+
83
+ export default HeroImage;
@@ -14,3 +14,5 @@ export { default as ProductCarousel } from "@/cms/blocks/components/product-caro
14
14
  export { default as ProductSectionFullWidth } from "@/cms/blocks/components/product-section-fullwidth/index";
15
15
  export { default as RichText } from "@/cms/blocks/components/rich-text";
16
16
  export { default as Services } from "@/cms/blocks/components/services/index";
17
+ export { default as CtaBanner } from "./components/cta-banner";
18
+ export { default as HeroImage } from "./components/hero-image";
@@ -141,6 +141,26 @@ export interface RichTextBlock {
141
141
  content: string;
142
142
  }
143
143
 
144
+ export interface HeroImageBlock {
145
+ id: number;
146
+ __component: "blocks.hero-image";
147
+ heading: string;
148
+ subheading?: string | null;
149
+ buttonText?: string | null;
150
+ buttonLink?: string | null;
151
+ image?: Media | null;
152
+ }
153
+
154
+ export interface CtaBannerBlock {
155
+ id: number;
156
+ __component: "blocks.cta-banner";
157
+ heading: string;
158
+ subtext?: string | null;
159
+ buttonText?: string | null;
160
+ buttonLink?: string | null;
161
+ backgroundColor?: string | null;
162
+ }
163
+
144
164
  export interface ImageFormat {
145
165
  ext: string;
146
166
  url: string;
@@ -0,0 +1,26 @@
1
+ import { Box, Chip } from "@mui/material";
2
+
3
+ export function DraftModeBadge() {
4
+ return (
5
+ <Box
6
+ sx={{
7
+ position: "fixed",
8
+ bottom: 16,
9
+ right: 16,
10
+ zIndex: 9999,
11
+ }}
12
+ >
13
+ <Chip
14
+ label="Draft Mode"
15
+ size="small"
16
+ sx={{
17
+ backgroundColor: "#f59e0b",
18
+ color: "#000",
19
+ fontWeight: 700,
20
+ fontSize: "0.75rem",
21
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
22
+ }}
23
+ />
24
+ </Box>
25
+ );
26
+ }