Repository: CodeForAfrica/ui
Pull request #1170: Feature/hero block UI
Author: login: koechkevin
Created: 2025-05-27T11:31:53Z | Merged: 2025-05-30T08:36:41Z

## Description

This PR Adds Hero Block cms and component for Trustlab

Fixes

## Type of change

Please delete options that are not relevant.


- [x] New feature (non-breaking change which adds functionality)


## Screenshots
![image](https://github.com/user-attachments/assets/fc530352-6b8e-439c-9ecc-11f3042c757e)
![image](https://github.com/user-attachments/assets/2d9bc555-e392-44dd-94d2-37b92bb8e0e8)
![image](https://github.com/user-attachments/assets/48a97503-a272-4dbc-8bce-d57dbbab7fd5)

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation



Diff:
diff --git a/apps/promisetracker/src/utils/auth/error-list.js b/apps/promisetracker/src/utils/auth/error-list.js
index 38d252c7c2..76715aab32 100644
--- a/apps/promisetracker/src/utils/auth/error-list.js
+++ b/apps/promisetracker/src/utils/auth/error-list.js
@@ -4,7 +4,6 @@ export default function getErrorDescription(code) {
   if (!code) {
     return null;
   }
-
   switch (code) {
     case "OAUTH_GET_ACCESS_TOKEN_ERROR":
     case "OAUTH_V1_GET_ACCESS_TOKEN_ERROR":
diff --git a/apps/trustlab/public/images/cms/blocks/hero.png b/apps/trustlab/public/images/cms/blocks/hero.png
new file mode 100644
index 0000000000..bc9c3d1fd5
Binary files /dev/null and b/apps/trustlab/public/images/cms/blocks/hero.png differ
diff --git a/apps/trustlab/src/components/Hero/Hero.js b/apps/trustlab/src/components/Hero/Hero.js
new file mode 100644
index 0000000000..3ba51b92e0
--- /dev/null
+++ b/apps/trustlab/src/components/Hero/Hero.js
@@ -0,0 +1,152 @@
+import { Section } from "@commons-ui/core";
+import { Link } from "@commons-ui/next";
+import { LexicalRichText } from "@commons-ui/payload";
+import { Slide, Box, Button } from "@mui/material";
+import React, { forwardRef, useState } from "react";
+
+const Hero = forwardRef(function Hero({ slides }, ref) {
+  const [activeStep, setActiveStep] = useState(0);
+  if (!slides || !slides.length) {
+    return null;
+  }
+  return (
+    <Section
+      sx={{
+        maxWidth: { md: "100%", xs: "100%" },
+      }}
+      bgcolor="common.black"
+      color="common.white"
+      fixed={false}
+      ref={ref}
+    >
+      <Box
+        sx={{
+          position: "relative",
+          width: "100%",
+          height: "500px",
+          overflow: "hidden",
+        }}
+      >
+        {slides.map((slide, index) => (
+          <Slide
+            key={slide.id}
+            direction="left"
+            in={activeStep === index}
+            mountOnEnter
+            unmountOnExit
+            timeout={{ enter: 800, exit: 800 }}
+            appear={false}
+          >
+            <Box
+              sx={{
+                position: "absolute",
+                top: 0,
+                left: 0,
+                width: "100%",
+                height: "100%",
+                backgroundImage: slide.backgroundImage?.url
+                  ? `url(${slide.backgroundImage?.url})`
+                  : "none",
+                backgroundSize: "cover",
+                backgroundPosition: "center",
+                color: "#fff",
+              }}
+            >
+              <Box
+                sx={{
+                  width: "100%",
+                  height: "100%",
+                  backgroundColor: "rgba(0, 0, 0, 0.8)",
+                  pointerEvents: "none",
+                  zIndex: 1,
+                }}
+              >
+                <Box
+                  sx={(theme) => ({
+                    pt: 10,
+                    px: { xs: 2.5, sm: 0 },
+                    margin: "0 auto",
+                    maxWidth: theme.contentWidths.values,
+                  })}
+                >
+                  <LexicalRichText
+                    elements={slide.title}
+                    TypographyProps={{
+                      variant: "display1",
+                      gutterBottom: true,
+                    }}
+                  />
+                  <LexicalRichText
+                    elements={slide.subtitle}
+                    TypographyProps={{
+                      variant: "h3",
+                      gutterBottom: true,
+                    }}
+                  />
+                  <LexicalRichText
+                    elements={slide.description}
+                    TypographyProps={{
+                      variant: "subtitle1",
+                      sx: { mt: 2 },
+                    }}
+                  />
+
+                  {slide.href && (
+                    <Button
+                      component={slide.href ? Link : undefined}
+                      href={slide.href}
+                      variant="outlined"
+                      sx={{
+                        backgroundColor: "common.white",
+                        color: "#463E3E",
+                        border: "none",
+                        mt: 2,
+                        "&:hover": {
+                          transform: "scale(0.95)",
+                        },
+                      }}
+                    >
+                      {slide.label}
+                    </Button>
+                  )}
+                </Box>
+              </Box>
+            </Box>
+          </Slide>
+        ))}
+        <Box
+          sx={(theme) => ({
+            position: "relative",
+            px: { xs: 2.5, sm: 6, md: 0 },
+            maxWidth: theme.contentWidths.values,
+            m: "0 auto",
+          })}
+        >
+          <Box
+            sx={{ position: "absolute", left: 0, top: { sm: 336, xs: 400 } }}
+          >
+            {slides.map((_, index) => (
+              <Button
+                key={slides[index].title}
+                onClick={() => setActiveStep(index)}
+                sx={{
+                  width: 12,
+                  height: 12,
+                  minWidth: 0,
+                  borderRadius: "50%",
+                  border: "1px solid white",
+                  backgroundColor:
+                    activeStep === index ? "white" : "transparent",
+                  mx: 0.5,
+                  p: 0,
+                }}
+              />
+            ))}
+          </Box>
+        </Box>
+      </Box>
+    </Section>
+  );
+});
+
+export default Hero;
diff --git a/apps/trustlab/src/components/Hero/Hero.snap.js b/apps/trustlab/src/components/Hero/Hero.snap.js
new file mode 100644
index 0000000000..775eee18ca
--- /dev/null
+++ b/apps/trustlab/src/components/Hero/Hero.snap.js
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Hero /> renders unchanged 1`] = `<div />`;
diff --git a/apps/trustlab/src/components/Hero/Hero.test.js b/apps/trustlab/src/components/Hero/Hero.test.js
new file mode 100644
index 0000000000..690853ac70
--- /dev/null
+++ b/apps/trustlab/src/components/Hero/Hero.test.js
@@ -0,0 +1,19 @@
+import { createRender } from "@commons-ui/testing-library";
+import React from "react";
+
+import Hero from "./Hero";
+
+import theme from "@/trustlab/theme";
+
+const render = createRender({ theme });
+
+const defaultProps = {
+  slides: [],
+};
+
+describe("<Hero />", () => {
+  it("renders unchanged", () => {
+    const { container } = render(<Hero {...defaultProps} />);
+    expect(container).toMatchSnapshot();
+  });
+});
diff --git a/apps/trustlab/src/components/Hero/index.js b/apps/trustlab/src/components/Hero/index.js
new file mode 100644
index 0000000000..4ca8fd8ad7
--- /dev/null
+++ b/apps/trustlab/src/components/Hero/index.js
@@ -0,0 +1,3 @@
+import Hero from "./Hero";
+
+export default Hero;
diff --git a/apps/trustlab/src/lib/data/blockify/index.js b/apps/trustlab/src/lib/data/blockify/index.js
new file mode 100644
index 0000000000..23c697867a
--- /dev/null
+++ b/apps/trustlab/src/lib/data/blockify/index.js
@@ -0,0 +1,23 @@
+const propsifyBlockBySlug = {};
+
+async function blockify(blocks, api, context) {
+  const promises = blocks?.map(async (block) => {
+    const slug = block.blockType;
+    const propsifyBlock = propsifyBlockBySlug[slug];
+
+    if (propsifyBlock) {
+      return propsifyBlock(block, api, context);
+    }
+    return {
+      ...block,
+      slug,
+    };
+  });
+
+  if (promises) {
+    return Promise.all(promises);
+  }
+  return blocks ?? null;
+}
+
+export default blockify;
diff --git a/apps/trustlab/src/lib/data/common/index.js b/apps/trustlab/src/lib/data/common/index.js
index dc0880b43c..8a25a92e7f 100644
--- a/apps/trustlab/src/lib/data/common/index.js
+++ b/apps/trustlab/src/lib/data/common/index.js
@@ -1,3 +1,5 @@
+import blockify from "@/trustlab/lib/data/blockify";
+
 function imageFromMedia({ alt, url }) {
   return { alt, src: url ?? null };
 }
@@ -178,10 +180,12 @@ export async function getPageProps(api, context) {
     return null;
   }
   const siteSettings = await api.findGlobal("site-settings");
+  const blocks = await blockify(page?.blocks, api, context);
   const navbar = getNavBar(siteSettings);
   const footer = getFooter(siteSettings);
   return {
     navbar,
+    blocks,
     footer,
   };
 }
diff --git a/apps/trustlab/src/pages/[[...slugs]].page.js b/apps/trustlab/src/pages/[[...slugs]].page.js
index 99a44d478a..51916246fb 100644
--- a/apps/trustlab/src/pages/[[...slugs]].page.js
+++ b/apps/trustlab/src/pages/[[...slugs]].page.js
@@ -1,10 +1,12 @@
 import React from "react";
 import { SWRConfig } from "swr";
 
-import RefreshRouteOnSave from "@/trustlab/components/PayloadLivePreview";
+import Hero from "@/trustlab/components/Hero";
 import { getPageStaticPaths, getPageStaticProps } from "@/trustlab/lib/data";
 
-const componentsBySlugs = {};
+const componentsBySlugs = {
+  hero: Hero,
+};
 
 function Page({ blocks, fallback }) {
   if (!blocks?.length) {
@@ -19,7 +21,6 @@ function Page({ blocks, fallback }) {
   }
   return (
     <PageComponent {...pageComponentProps}>
-      <RefreshRouteOnSave />
       {blocks.map((block) => {
         const Component = componentsBySlugs[block.slug];
         if (!Component) {
diff --git a/apps/trustlab/src/payload/blocks/Hero.js b/apps/trustlab/src/payload/blocks/Hero.js
new file mode 100644
index 0000000000..b467e794c0
--- /dev/null
+++ b/apps/trustlab/src/payload/blocks/Hero.js
@@ -0,0 +1,48 @@
+import { richText, image, link } from "@commons-ui/payload";
+
+const Hero = {
+  slug: "hero",
+  imageURL: "/images/cms/blocks/hero.png",
+  imageAltText: "Used in homepage.",
+  fields: [
+    {
+      name: "slides",
+      type: "array",
+      label: { en: "Slides" },
+
+      fields: [
+        richText({
+          name: "title",
+          required: true,
+        }),
+        richText({
+          name: "subtitle",
+          required: true,
+        }),
+        richText({
+          name: "description",
+          required: true,
+          admin: {
+            description: "A brief description of the slide content.",
+          },
+        }),
+        image({
+          overrides: {
+            name: "backgroundImage",
+            required: true,
+          },
+        }),
+        link({
+          name: "buttonLink",
+          label: "Button Link",
+          required: true,
+          admin: {
+            description: "The link to navigate to when the button is clicked.",
+          },
+        }),
+      ],
+    },
+  ],
+};
+
+export default Hero;
diff --git a/apps/trustlab/src/payload/blocks/TestBlock.js b/apps/trustlab/src/payload/blocks/TestBlock.js
deleted file mode 100644
index 2312eee8d7..0000000000
--- a/apps/trustlab/src/payload/blocks/TestBlock.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// TODO: Remove this block. Just for testing.
-const TestBlock = {
-  slug: "test",
-  fields: [
-    {
-      name: "title",
-      type: "text",
-      required: true,
-    },
-    {
-      name: "content",
-      type: "richText",
-      required: true,
-    },
-  ],
-};
-export default TestBlock;
diff --git a/apps/trustlab/src/payload/collections/Pages.js b/apps/trustlab/src/payload/collections/Pages.js
index 92a76dda52..70772683b3 100644
--- a/apps/trustlab/src/payload/collections/Pages.js
+++ b/apps/trustlab/src/payload/collections/Pages.js
@@ -2,7 +2,7 @@ import { slug, fullTitle } from "@commons-ui/payload";
 
 import { canManagePages } from "@/trustlab/payload/access/abilities";
 import { anyone } from "@/trustlab/payload/access/anyone";
-import TestBlock from "@/trustlab/payload/blocks/TestBlock";
+import Hero from "@/trustlab/payload/blocks/Hero";
 import { hideAPIURL } from "@/trustlab/payload/utils";
 
 const Pages = {
@@ -39,8 +39,7 @@ const Pages = {
     {
       name: "blocks",
       type: "blocks",
-      // TODO: Remove TestBlock. Payload admin breaks if blocks is empty.
-      blocks: [TestBlock],
+      blocks: [Hero],
       localized: true,
       admin: {
         initCollapsed: true,
diff --git a/apps/trustlab/src/theme/index.js b/apps/trustlab/src/theme/index.js
index 87f95a0979..6b8ffcb762 100644
--- a/apps/trustlab/src/theme/index.js
+++ b/apps/trustlab/src/theme/index.js
@@ -55,7 +55,7 @@ const theme = createTheme({
     display1: {
       fontFamily: FONT_FAMILY_PRIMARY,
       fontStyle: "normal",
-      fontWeight: 300,
+      fontWeight: 700,
       lineHeight: 72 / 72,
     },
     display2: {
diff --git a/packages/commons-ui-payload/src/LexicalRichText/LexicalRichText.js b/packages/commons-ui-payload/src/LexicalRichText/LexicalRichText.js
index e3d4e5cc4c..604ba0fa83 100644
--- a/packages/commons-ui-payload/src/LexicalRichText/LexicalRichText.js
+++ b/packages/commons-ui-payload/src/LexicalRichText/LexicalRichText.js
@@ -12,8 +12,9 @@ export const jsxConverters =
   });
 
 const LexicalRichText = forwardRef(function LexicalRichText(props, ref) {
-  const { elements, ...others } = props;
-  const converters = jsxConverters(others);
+  const { elements, ...converterProps } = props;
+  const converters = jsxConverters(converterProps);
+  const { TypographyProps, ...others } = converterProps;
 
   return (
     <Box {...others} ref={ref}>


Reviews:
user: login: kilemensi
body: 👍🏽 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T08:31:29Z
user: login: kelvinkipruto
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T11:34:27Z
user: login: koechkevin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T11:55:19Z
user: login: kelvinkipruto
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T12:57:08Z
user: login: kelvinkipruto
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T13:18:26Z
user: login: kelvinkipruto
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T13:50:09Z
user: login: kelvinkipruto
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T07:21:52Z
user: login: kilemensi
body: Code LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-30T07:57:00Z

Review comments:
diff_hunk: @@ -12,6 +12,9 @@ const outputFileTracingRoot = PROJECT_ROOT
 
 /** @type {import('next').NextConfig} */
 const nextConfig = {
+  experimental: {
+    esmExternals: "loose",
+  },
path: apps/trustlab/next.config.mjs
user: login: kilemensi
body: We should be avoiding these... if we can't lets include a comment on which packages are forcing us to use it.
created_at: 2025-05-28T07:49:55Z
author_association: MEMBER
diff_hunk: @@ -38,6 +38,7 @@
     "@payloadcms/ui": "catalog:payload-v3",
     "@sentry/nextjs": "catalog:",
     "cross-env": "catalog:",
+    "date-fns": "catalog:",
path: apps/trustlab/package.json
user: login: kilemensi
body: Just did a quick search & I don't see `date-fns` being used anywhere.
created_at: 2025-05-28T07:50:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+import { Section } from "@commons-ui/core";
+import { Link } from "@commons-ui/next";
+import { LexicalRichText } from "@commons-ui/payload";
+import { Slide, Box, Button } from "@mui/material";
+import React, { useState } from "react";
+
+function Hero({ slides }) {
+  const [activeStep, setActiveStep] = useState(0);
+  if (!slides || !slides.length) {
+    return null;
+  }
+  return (
+    <Box bgcolor="common.black" color="common.white">
path: apps/trustlab/src/components/Hero/Hero.js
user: login: kilemensi
body: 1. If we're on React [`v19`](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop), we need to pass `ref` (and `sx`) to top level components. If we're using `v18` or older, we need to use `forwardRef`.
2. `Section` also needs to be close to the very top (if not the top); they help break down a page into separate "sections". A slide doesn't need to have it's own section, a slide can just change contents of a section.
created_at: 2025-05-28T07:58:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+const propsifyBlockBySlug = {};
+
+async function blockify(blocks, api, context) {
+  const promises = blocks?.map(async (block) => {
+    const slug = block.blockType;
+    const propsifyBlock = propsifyBlockBySlug[slug];
+
+    if (propsifyBlock) {
+      return propsifyBlock(block, api, context);
+    }
+    return {
+      ...block,
+      slug,
+    };
path: apps/trustlab/src/lib/data/blockify/index.js
user: login: kilemensi
body: With this here, I don't think we need this [`hero`](https://github.com/CodeForAfrica/ui/blob/7d520eaee7479537dee8290fa74f32295379dd79/apps/trustlab/src/lib/data/blockify/hero.js#L1) function as it does the same thing. 
created_at: 2025-05-28T08:01:27Z
author_association: MEMBER
diff_hunk: @@ -14,9 +14,10 @@ export const jsxConverters =
 const LexicalRichText = forwardRef(function LexicalRichText(props, ref) {
   const { elements, ...others } = props;
   const converters = jsxConverters(others);
+  const { TypographyProps, ...boxProps } = others;
path: packages/commons-ui-payload/src/LexicalRichText/LexicalRichText.js
user: login: kilemensi
body: Why not

```suggestion
  const { elements, ...converterProps } = props;
  const converters = jsxConverters(converterProps);
  const { TypographyProps, ...others } = converterProps;
```
created_at: 2025-05-28T08:31:18Z
author_association: MEMBER
diff_hunk: @@ -38,8 +38,7 @@ const Pages = {
     {
       name: "blocks",
       type: "blocks",
-      // TODO: Remove TestBlock. Payload admin breaks if blocks is empty.
-      blocks: [TestBlock],
+      blocks: [Hero],
path: apps/trustlab/src/payload/collections/Pages.js
user: login: kelvinkipruto
body: With `TestBlock` removed, we should delete the unused [block](https://github.com/CodeForAfrica/ui/blob/87f10db577f42513924136ecef09a223650b63fb/apps/trustlab/src/payload/blocks/TestBlock.js)
created_at: 2025-05-28T11:29:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,7 +21,6 @@ function Page({ blocks, fallback }) {
   }
   return (
     <PageComponent {...pageComponentProps}>
-      <RefreshRouteOnSave />
path: apps/trustlab/src/pages/[[...slugs]].page.js
user: login: kelvinkipruto
body: @koechkevin Is this not required for Payload Live Preview to work as expected? 
We can maybe add it it the [Page Component](https://github.com/CodeForAfrica/ui/blob/a87b7917f874258a8696fbce37d08509f07198b8/apps/trustlab/src/components/Page/Page.js) 
created_at: 2025-05-28T11:31:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,7 +21,6 @@ function Page({ blocks, fallback }) {
   }
   return (
     <PageComponent {...pageComponentProps}>
-      <RefreshRouteOnSave />
path: apps/trustlab/src/pages/[[...slugs]].page.js
user: login: koechkevin
body: There's an issue here @kelvinkipruto, there's a recursive refresh caused by this component.
created_at: 2025-05-28T11:55:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,7 +21,6 @@ function Page({ blocks, fallback }) {
   }
   return (
     <PageComponent {...pageComponentProps}>
-      <RefreshRouteOnSave />
path: apps/trustlab/src/pages/[[...slugs]].page.js
user: login: kelvinkipruto
body: @koechkevin I see. The current live preview implementation depends on [RSC](https://payloadcms.com/docs/live-preview/server), and we reverted back to React 18. We can create an issue to track this.
created_at: 2025-05-28T12:57:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+import { richText, image, link } from "@commons-ui/payload";
+
+const Hero = {
+  slug: "hero",
+  imageURL: "/images/cms/blocks/hero.png",
path: apps/trustlab/src/payload/blocks/Hero.js
user: login: kelvinkipruto
body: Looks like this is not the correct path of the [image](https://github.com/CodeForAfrica/ui/blob/40a14f5d7d11eb7973ba0e21890fff8e393e24f7/apps/trustlab/public/images/cms/hero.png)
created_at: 2025-05-28T13:00:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+import { Section } from "@commons-ui/core";
+import { Link } from "@commons-ui/next";
+import { LexicalRichText } from "@commons-ui/payload";
+import { Slide, Box, Button } from "@mui/material";
+import React, { forwardRef, useState } from "react";
+
+const Hero = forwardRef(function Hero({ slides }, ref) {
+  const [activeStep, setActiveStep] = useState(0);
+  if (!slides || !slides.length) {
+    return null;
+  }
+  return (
+    <Section
+      sx={{
+        maxWidth: "unset",
+      }}
+      bgcolor="common.black"
+      color="common.white"
+      fixed={false}
+      ref={ref}
+    >
+      <Box
+        sx={{
+          position: "relative",
+          width: "100%",
+          height: "500px",
+          overflow: "hidden",
+        }}
+      >
+        {slides.map((slide, index) => (
+          <Slide
+            key={slide.id}
+            direction="left"
+            in={activeStep === index}
+            mountOnEnter
+            unmountOnExit
+            timeout={{ enter: 800, exit: 800 }}
+            appear={false}
+          >
+            <Box
+              sx={{
+                position: "absolute",
+                top: 0,
+                left: 0,
+                width: "100%",
+                height: "100%",
+                backgroundImage: slide.backgroundImage?.url
+                  ? `url(${slide.backgroundImage?.url})`
+                  : "none",
+                backgroundSize: "cover",
+                backgroundPosition: "center",
+                color: "#fff",
+              }}
+            >
+              <Box
+                sx={{
+                  width: "100%",
+                  height: "100%",
+                  backgroundColor: "rgba(0, 0, 0, 0.8)",
path: apps/trustlab/src/components/Hero/Hero.js
user: login: kelvinkipruto
body: @koechkevin Not sure about the best approach here as I can [see](https://www.figma.com/design/b7Qmr2zxC3tQ0rzlOTbbOH/Trustlab-UI---Nov-2024-to-Present?node-id=702-12556&t=naFIITGS0JjenPuN-4) that some of the carousels have an opacity whereas other do not.
created_at: 2025-05-28T13:08:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,7 +21,6 @@ function Page({ blocks, fallback }) {
   }
   return (
     <PageComponent {...pageComponentProps}>
-      <RefreshRouteOnSave />
path: apps/trustlab/src/pages/[[...slugs]].page.js
user: login: kelvinkipruto
body: #1174 
created_at: 2025-05-28T13:50:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,150 @@
+import { Section } from "@commons-ui/core";
+import { Link } from "@commons-ui/next";
+import { LexicalRichText } from "@commons-ui/payload";
+import { Slide, Box, Button } from "@mui/material";
+import React, { forwardRef, useState } from "react";
+
+const Hero = forwardRef(function Hero({ slides }, ref) {
+  const [activeStep, setActiveStep] = useState(0);
+  if (!slides || !slides.length) {
+    return null;
+  }
+  return (
+    <Section
+      sx={{
+        maxWidth: { md: "100%", xs: "100%" },
+      }}
+      bgcolor="common.black"
+      color="common.white"
+      fixed={false}
+      ref={ref}
+    >
+      <Box
+        sx={{
+          position: "relative",
+          width: "100%",
+          height: "500px",
+          overflow: "hidden",
+        }}
+      >
+        {slides.map((slide, index) => (
+          <Slide
+            key={slide.id}
+            direction="left"
+            in={activeStep === index}
+            mountOnEnter
+            unmountOnExit
+            timeout={{ enter: 800, exit: 800 }}
+            appear={false}
+          >
+            <Box
+              sx={{
+                position: "absolute",
+                top: 0,
+                left: 0,
+                width: "100%",
+                height: "100%",
+                backgroundImage: slide.backgroundImage?.url
+                  ? `url(${slide.backgroundImage?.url})`
+                  : "none",
+                backgroundSize: "cover",
+                backgroundPosition: "center",
+                color: "#fff",
+              }}
+            >
+              <Box
+                sx={{
+                  width: "100%",
+                  height: "100%",
+                  backgroundColor: "rgba(0, 0, 0, 0.8)",
+                  pointerEvents: "none",
+                  zIndex: 1,
+                }}
+              >
+                <Box
+                  sx={(theme) => ({
+                    pt: 10,
+                    px: { xs: 2.5, sm: 0 },
+                    margin: "0 auto",
+                    maxWidth: theme.contentWidths.values,
+                  })}
+                >
+                  <LexicalRichText
+                    elements={slide.title}
+                    TypographyProps={{
+                      variant: "display1",
+                      gutterBottom: true,
+                    }}
+                  />
+                  <LexicalRichText
+                    elements={slide.subtitle}
+                    TypographyProps={{
+                      variant: "h3",
+                      gutterBottom: true,
+                    }}
+                  />
+                  <LexicalRichText
+                    elements={slide.description}
+                    TypographyProps={{
+                      variant: "subtitle1",
+                      sx: { mt: 2 },
+                    }}
+                  />
+
+                  {slide.href && (
+                    <Button
+                      component={slide.href ? Link : undefined}
+                      href={slide.href}
+                      variant="outlined"
+                      sx={{
+                        backgroundColor: "common.white",
+                        color: "#463E3E",
+                        border: "none",
+                        mt: 2,
+                        "&:hover": {
+                          transform: "scale(0.95)",
+                        },
+                      }}
+                    >
+                      {slide.label}
+                    </Button>
+                  )}
+                </Box>
+              </Box>
+            </Box>
+          </Slide>
+        ))}
+        <Section
path: apps/trustlab/src/components/Hero/Hero.js
user: login: kilemensi
body: A single Section should be enough... but it shouldn't hold this PR back any further.
created_at: 2025-05-30T07:56:36Z
author_association: MEMBER

PR comments:
user: login: kelvinkipruto
created_at: 2025-05-28T13:47:27Z
body: @koechkevin Looks good. However, I have noticed some discrepancies between the current theme setup and the design. For example:
1. The Hero [title](https://www.figma.com/design/b7Qmr2zxC3tQ0rzlOTbbOH/Trustlab-UI---Nov-2024-to-Present?node-id=702-13660&t=naFIITGS0JjenPuN-0) should have a font-weight of 800. In our current implementation, it has [700](https://github.com/CodeForAfrica/ui/blob/2595b5bbfedfc58cf4fb8fcca6d049183b96e96f/apps/trustlab/src/theme/index.js#L58).
2. There is also a discrepancy in the naming of the typography variables. For example, the Hero [subtitle](https://www.figma.com/design/b7Qmr2zxC3tQ0rzlOTbbOH/Trustlab-UI---Nov-2024-to-Present?node-id=702-12563&t=naFIITGS0JjenPuN-4) should have a typography `h1`, and in our case, we're using `h3`.
3. The Hero Link Button text style does not match the actual design.

My suggestion would be to first fix our theme to match the actual design to ensure consistency. We can adapt [CharterAfrica's](https://github.com/CodeForAfrica/ui/blob/00d90d46fd01941748f8d35766e81af3cea8833e/apps/charterafrica/src/theme/index.js) implementation as the sizes are similar to the design. 

**PS: I can get a PR early tomorrow on this.**
author_association: CONTRIBUTOR
user: login: kilemensi
created_at: 2025-05-29T11:49:03Z
body: I remember there was a talk of a PR to fix the theme before we can proceed with this one @kelvinkipruto / @koechkevin. Are we winning?
author_association: MEMBER
user: login: kelvinkipruto
created_at: 2025-05-30T07:22:18Z
body: @koechkevin We can merge this then use it for testing alongside the header and footer
author_association: CONTRIBUTOR
user: login: kilemensi
created_at: 2025-05-30T08:10:24Z
body: Just tried running this locally @koechkevin and the button link on hero is not clickable.
author_association: MEMBER
user: login: kilemensi
created_at: 2025-05-30T10:54:43Z
body: > Just tried running this locally @koechkevin and the button link on hero is not clickable.

Did we fix this issue @koechkevin ? If not lets add a story to track it.
author_association: MEMBER
user: login: koechkevin
created_at: 2025-05-30T10:57:55Z
body: > > Just tried running this locally @koechkevin and the button link on hero is not clickable.
> 
> Did we fix this issue @koechkevin ? If not lets add a story to track it.

@kilemensi I noticed this immediately after merging but I added a fix [in this PR](https://github.com/CodeForAfrica/ui/pull/1171/files#diff-eb33df34da6055ed98e59041ca0f319797c1cb97a6f6748cdda108fe9d2d8454)
author_association: CONTRIBUTOR