diff --git a/apps/climatemappedafrica/package.json b/apps/climatemappedafrica/package.json
index 308612472..3bdfe54a1 100644
--- a/apps/climatemappedafrica/package.json
+++ b/apps/climatemappedafrica/package.json
@@ -78,6 +78,7 @@
"react-vega": "catalog:",
"sharp": "catalog:",
"simplebar-react": "catalog:",
+ "slate": "catalog:",
"swr": "catalog:",
"vega": "catalog:",
"vega-embed": "catalog:",
diff --git a/apps/climatemappedafrica/public/images/cms/blocks/hero.png b/apps/climatemappedafrica/public/images/cms/blocks/hero.png
new file mode 100644
index 000000000..8cc7eccb5
Binary files /dev/null and b/apps/climatemappedafrica/public/images/cms/blocks/hero.png differ
diff --git a/apps/climatemappedafrica/src/assets/images/bg-map-white.jpg b/apps/climatemappedafrica/src/assets/images/bg-map-white.jpg
new file mode 100644
index 000000000..5873d7630
Binary files /dev/null and b/apps/climatemappedafrica/src/assets/images/bg-map-white.jpg differ
diff --git a/apps/climatemappedafrica/src/components/Hero/Hero.js b/apps/climatemappedafrica/src/components/Hero/Hero.js
new file mode 100644
index 000000000..7f375673c
--- /dev/null
+++ b/apps/climatemappedafrica/src/components/Hero/Hero.js
@@ -0,0 +1,163 @@
+import { RichTypography } from "@commons-ui/core";
+import { Box, Grid, useMediaQuery } from "@mui/material";
+import dynamic from "next/dynamic";
+import PropTypes from "prop-types";
+import React, { useState } from "react";
+
+import heroBg from "@/climatemappedafrica/assets/images/bg-map-white.jpg";
+import DropdownSearch from "@/climatemappedafrica/components/DropdownSearch";
+import Image from "@/climatemappedafrica/components/Image";
+import RichHeader from "@/climatemappedafrica/components/RichHeader";
+import Section from "@/climatemappedafrica/components/Section";
+
+const Map = dynamic(() => import("./Map"), { ssr: false });
+
+function Hero({
+ comment,
+ title,
+ subtitle,
+ searchLabel,
+ featuredLocations,
+ searchPlaceholder,
+ properties,
+ location: { center },
+ level,
+ ...props
+}) {
+ const isUpLg = useMediaQuery((theme) => theme.breakpoints.up("lg"));
+ const [hoverGeo, setHoverGeo] = useState(null);
+ const continentLevelZoom = isUpLg ? 2.4 : 2.1;
+ const countryLevelZoom = isUpLg ? 6 : 5.25;
+ const zoom = level === "continent" ? continentLevelZoom : countryLevelZoom;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+ {comment}
+
+
+ {/* Since map is dynamic-ally loaded, no need for implementation="css" */}
+
+
+ {center ? (
+
+ ) : null}
+
+
+ {hoverGeo}
+
+
+
+
+
+
+
+ );
+}
+
+Hero.propTypes = {
+ comment: PropTypes.string,
+ subtitle: PropTypes.arrayOf(PropTypes.shape({})),
+ searchLabel: PropTypes.string,
+ title: PropTypes.string,
+ featuredLocations: PropTypes.arrayOf(PropTypes.shape({})),
+ properties: PropTypes.shape({}),
+ level: PropTypes.string,
+};
+
+export default Hero;
diff --git a/apps/climatemappedafrica/src/components/Hero/Hero.snap.js b/apps/climatemappedafrica/src/components/Hero/Hero.snap.js
new file mode 100644
index 000000000..4fc9fb1e1
--- /dev/null
+++ b/apps/climatemappedafrica/src/components/Hero/Hero.snap.js
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ Data to hold your government accountable
+
+
+
+
+ PesaYetu helps journalists, researchers and activists transform their work with in-depth county-specific information. Get started now with datasets from Kenya.
+
+
+
+
+
+
+ Search for a location
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/apps/climatemappedafrica/src/components/Hero/Hero.test.js b/apps/climatemappedafrica/src/components/Hero/Hero.test.js
new file mode 100644
index 000000000..566723acb
--- /dev/null
+++ b/apps/climatemappedafrica/src/components/Hero/Hero.test.js
@@ -0,0 +1,58 @@
+import { createRender } from "@commons-ui/testing-library";
+import React from "react";
+
+import Hero from "./Hero";
+
+import theme from "@/climatemappedafrica/theme";
+
+// eslint-disable-next-line testing-library/render-result-naming-convention
+const render = createRender({ theme });
+
+const defaultProps = {
+ title: [
+ {
+ children: [
+ { text: "Data to hold your government accountable", children: null },
+ ],
+ },
+ ],
+ subtitle: [
+ {
+ children: [
+ {
+ text: "PesaYetu helps journalists, researchers and activists transform their work with in-depth county-specific information. Get started now with datasets from Kenya.\n",
+ children: null,
+ },
+ ],
+ },
+ ],
+ searchLabel: "Search for a location",
+ featuredLocations: [],
+ searchPlaceholder: "Search for a location",
+ properties: {
+ code: "KE",
+ name: "Kenya",
+ area: 586002.515,
+ parent: "AF",
+ level: "country",
+ version: "Climate",
+ },
+ location: {},
+ level: "country",
+ id: "670e3996766697e7feb349d5",
+ blockType: "hero",
+ slug: "hero",
+ boundary: {
+ type: "FeatureCollection",
+ features: [],
+ },
+ variant: "explore",
+ icon: null,
+};
+
+describe(" ", () => {
+ it("renders unchanged", () => {
+ const { container } = render( );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/apps/climatemappedafrica/src/components/Hero/Map.js b/apps/climatemappedafrica/src/components/Hero/Map.js
new file mode 100644
index 000000000..8b34ab9f2
--- /dev/null
+++ b/apps/climatemappedafrica/src/components/Hero/Map.js
@@ -0,0 +1,119 @@
+import { Box } from "@mui/material";
+import { useRouter } from "next/router";
+import PropTypes from "prop-types";
+import React from "react";
+import { MapContainer, GeoJSON } from "react-leaflet";
+
+import "leaflet/dist/leaflet.css";
+import theme from "@/climatemappedafrica/theme";
+
+function Map({
+ center,
+ zoom,
+ boundary,
+ styles = {
+ height: "100%",
+ width: "100%",
+ },
+ geoJSONStyles = {
+ color: "#2A2A2C",
+ weight: 1,
+ opacity: 1,
+ fillColor: "#fff",
+ dashArray: "2",
+ },
+ onLayerMouseOver,
+ featuredLocations,
+}) {
+ const router = useRouter();
+
+ const countyCodes = featuredLocations?.map(({ code }) => code);
+
+ const onEachFeature = (feature, layer) => {
+ layer.setStyle({
+ fillColor: theme.palette.background.default,
+ fillOpacity: 1,
+ });
+
+ if (countyCodes.includes(feature.properties.code?.toLowerCase())) {
+ layer.setStyle({
+ weight: 1.5,
+ dashArray: 0,
+ });
+ layer.on("mouseover", () => {
+ onLayerMouseOver(feature.properties.name.toLowerCase());
+ layer.setStyle({
+ fillColor: theme.palette.primary.main,
+ fillOpacity: 0.5,
+ });
+ });
+ layer.on("mouseout", () => {
+ onLayerMouseOver(null);
+ layer.setStyle({
+ fillOpacity: 1,
+ fillColor: theme.palette.background.default,
+ });
+ });
+ layer.on("click", () => {
+ router.push(`/explore/${feature.properties.code.toLowerCase()}`);
+ });
+ }
+ };
+
+ return (
+ ({
+ position: "relative",
+ height: { sm: "299px", lg: "471px" },
+ width: { sm: "236px", lg: "371px" },
+ marginTop: { sm: "55px", lg: "42px" },
+ "& .leaflet-container": {
+ background: "transparent",
+ },
+ })}
+ >
+
+
+
+
+ );
+}
+
+Map.propTypes = {
+ center: (props, propName, componentName) => {
+ const { [propName]: prop } = props;
+ if (!Array.isArray(prop) || prop.length !== 2 || prop.some(Number.isNaN)) {
+ return new Error(
+ `Invalid prop \`${propName}\` supplied to` +
+ ` \`${componentName}\`. Validation failed.`,
+ );
+ }
+ return null;
+ },
+ zoom: PropTypes.number,
+ styles: PropTypes.shape({}),
+ boundary: PropTypes.shape({}),
+ geoJSONStyles: PropTypes.shape({}),
+ onLayerMouseOver: PropTypes.func,
+ featuredLocations: PropTypes.arrayOf(
+ PropTypes.shape({ code: PropTypes.string }),
+ ),
+};
+
+export default Map;
diff --git a/apps/climatemappedafrica/src/components/Hero/index.js b/apps/climatemappedafrica/src/components/Hero/index.js
new file mode 100644
index 000000000..4ca8fd8ad
--- /dev/null
+++ b/apps/climatemappedafrica/src/components/Hero/index.js
@@ -0,0 +1,3 @@
+import Hero from "./Hero";
+
+export default Hero;
diff --git a/apps/climatemappedafrica/src/components/LineClampedRichTypography/LineClampedRichTypography.js b/apps/climatemappedafrica/src/components/LineClampedRichTypography/LineClampedRichTypography.js
new file mode 100644
index 000000000..134af62dd
--- /dev/null
+++ b/apps/climatemappedafrica/src/components/LineClampedRichTypography/LineClampedRichTypography.js
@@ -0,0 +1,54 @@
+import { RichTypography } from "@commons-ui/next";
+import { styled } from "@mui/material/styles";
+import PropTypes from "prop-types";
+import React from "react";
+
+const LineClampedRichTypographyRoot = styled(RichTypography, {
+ shouldForwardProp: (prop) => !["lineClamp"].includes(prop),
+})(({ lineClamp, theme }) => ({
+ ...(lineClamp && {
+ display: "-webkit-box",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ WebkitBoxOrient: "vertical",
+ ...Object.keys(theme.breakpoints.values).reduce((acc, cur) => {
+ let accBreakpoint = acc;
+ if (theme.breakpoints.values[cur]) {
+ accBreakpoint = {};
+ acc[theme.breakpoints.up(cur)] = accBreakpoint;
+ }
+ let lineClampValue;
+ if (typeof lineClamp !== "object") {
+ lineClampValue = lineClamp?.toString();
+ } else if (typeof lineClamp?.[cur] !== "object") {
+ lineClampValue = lineClamp[cur]?.toString();
+ }
+ if (lineClampValue) {
+ accBreakpoint.WebkitLineClamp = lineClampValue;
+ accBreakpoint.lineClamp = lineClampValue;
+ }
+
+ return acc;
+ }, {}),
+ }),
+}));
+
+const LineClampedRichTypography = React.forwardRef(
+ function LineClampedRichTypography(props, ref) {
+ return ;
+ },
+);
+
+LineClampedRichTypography.propTypes = {
+ lineClamp: PropTypes.oneOfType([
+ PropTypes.shape({}),
+ PropTypes.number,
+ PropTypes.string,
+ ]),
+};
+
+LineClampedRichTypography.defaultProps = {
+ lineClamp: undefined,
+};
+
+export default LineClampedRichTypography;
diff --git a/apps/climatemappedafrica/src/components/LineClampedRichTypography/index.js b/apps/climatemappedafrica/src/components/LineClampedRichTypography/index.js
new file mode 100644
index 000000000..71636634f
--- /dev/null
+++ b/apps/climatemappedafrica/src/components/LineClampedRichTypography/index.js
@@ -0,0 +1,3 @@
+import LineClampedRichTypography from "./LineClampedRichTypography";
+
+export default LineClampedRichTypography;
diff --git a/apps/climatemappedafrica/src/lib/data/blockify/hero.js b/apps/climatemappedafrica/src/lib/data/blockify/hero.js
new file mode 100644
index 000000000..69eaccaf0
--- /dev/null
+++ b/apps/climatemappedafrica/src/lib/data/blockify/hero.js
@@ -0,0 +1,33 @@
+import {
+ fetchProfile,
+ fetchProfileGeography,
+} from "@/climatemappedafrica/lib/hurumap";
+
+export default async function hero(block) {
+ const { geometries } = await fetchProfileGeography(
+ block.location?.name?.toLowerCase(),
+ );
+ const { level } = geometries.boundary?.properties ?? {};
+ const childLevelMaps = {
+ continent: "country",
+ country: "region",
+ };
+ const childLevel = childLevelMaps[level];
+ const { locations, preferredChildren } = await fetchProfile();
+ const preferredChildrenPerLevel = preferredChildren[level];
+ const { children } = geometries;
+ const preferredLevel =
+ preferredChildrenPerLevel?.find((l) => children[l]) ?? null;
+ const featuredLocations = locations.filter(
+ (location) => location.level === childLevel,
+ );
+ const boundary = children[preferredLevel];
+ return {
+ ...block,
+ slug: "hero",
+ boundary,
+ featuredLocations,
+ level,
+ properties: geometries.boundary?.properties,
+ };
+}
diff --git a/apps/climatemappedafrica/src/lib/data/blockify/index.js b/apps/climatemappedafrica/src/lib/data/blockify/index.js
index 07d61107f..3aed58b79 100644
--- a/apps/climatemappedafrica/src/lib/data/blockify/index.js
+++ b/apps/climatemappedafrica/src/lib/data/blockify/index.js
@@ -1,7 +1,9 @@
+import hero from "./hero";
import pageHero from "./page-hero";
import team from "./team";
const propsifyBlockBySlug = {
+ hero,
"page-hero": pageHero,
team,
};
diff --git a/apps/climatemappedafrica/src/pages/[[...slug]].js b/apps/climatemappedafrica/src/pages/[[...slugs]].js
similarity index 96%
rename from apps/climatemappedafrica/src/pages/[[...slug]].js
rename to apps/climatemappedafrica/src/pages/[[...slugs]].js
index d5b79793e..24a6d30f9 100644
--- a/apps/climatemappedafrica/src/pages/[[...slug]].js
+++ b/apps/climatemappedafrica/src/pages/[[...slugs]].js
@@ -4,12 +4,14 @@ import { SWRConfig } from "swr";
import AboutTeam from "@/climatemappedafrica/components/AboutTeam";
import Footer from "@/climatemappedafrica/components/Footer";
+import Hero from "@/climatemappedafrica/components/Hero";
import Navigation from "@/climatemappedafrica/components/Navigation";
import PageHero from "@/climatemappedafrica/components/PageHero";
import Summary from "@/climatemappedafrica/components/Summary";
import { getPageServerSideProps } from "@/climatemappedafrica/lib/data";
const componentsBySlugs = {
+ hero: Hero,
"page-hero": PageHero,
summary: Summary,
team: AboutTeam,
diff --git a/apps/climatemappedafrica/src/pages/api/hurumap/profiles.js b/apps/climatemappedafrica/src/pages/api/hurumap/profiles.js
new file mode 100644
index 000000000..ae3ff17a5
--- /dev/null
+++ b/apps/climatemappedafrica/src/pages/api/hurumap/profiles.js
@@ -0,0 +1,25 @@
+import { fetchProfile } from "@/climatemappedafrica/lib/hurumap";
+
+let cache = null;
+let cacheExpiry = 0;
+
+export default async function handler(req, res) {
+ if (req.method === "GET") {
+ const now = Date.now();
+
+ if (cache && now < cacheExpiry) {
+ return res.status(200).json(cache);
+ }
+
+ try {
+ const result = await fetchProfile();
+ cache = result;
+ cacheExpiry = now + 5 * 60 * 1000;
+ return res.status(200).json(result);
+ } catch (err) {
+ return res.status(500).json(err.message);
+ }
+ }
+
+ return res.status(405).end();
+}
diff --git a/apps/climatemappedafrica/src/payload/blocks/Hero.js b/apps/climatemappedafrica/src/payload/blocks/Hero.js
new file mode 100644
index 000000000..ba1b08a90
--- /dev/null
+++ b/apps/climatemappedafrica/src/payload/blocks/Hero.js
@@ -0,0 +1,72 @@
+import LocationSelect, { validateLocation } from "../fields/LocationSelect";
+import richText from "../fields/richText";
+
+const Hero = {
+ slug: "hero",
+ imageURL: "/images/cms/blocks/hero.png",
+ imageAltText: "Used in homepage",
+ labels: {
+ singular: "Hero",
+ plural: "Hero",
+ },
+ fields: [
+ richText({
+ name: "title",
+ required: true,
+ label: "Title",
+ localized: true,
+ }),
+ richText({
+ name: "subtitle",
+ required: true,
+ localized: true,
+ }),
+ {
+ name: "searchLabel",
+ type: "text",
+ label: "Search Label",
+ localized: true,
+ required: true,
+ },
+ {
+ name: "searchPlaceholder",
+ type: "text",
+ label: "Search Placeholder",
+ localized: true,
+ },
+ {
+ name: "location",
+ label: "Featured Location",
+ type: "group",
+ fields: [
+ {
+ name: "name",
+ type: "text",
+ required: true,
+ hasMany: false,
+ defaultValue: "af",
+ validate: validateLocation,
+ localized: true,
+ admin: {
+ components: {
+ Field: LocationSelect,
+ },
+ },
+ },
+ {
+ name: "center",
+ label: "Center Point",
+ type: "point",
+ },
+ ],
+ },
+ {
+ name: "comment",
+ type: "text",
+ label: "Comment",
+ localized: true,
+ },
+ ],
+};
+
+export default Hero;
diff --git a/apps/climatemappedafrica/src/payload/collections/Pages.js b/apps/climatemappedafrica/src/payload/collections/Pages.js
index b12ef87f3..c98d41908 100644
--- a/apps/climatemappedafrica/src/payload/collections/Pages.js
+++ b/apps/climatemappedafrica/src/payload/collections/Pages.js
@@ -1,3 +1,4 @@
+import Hero from "../blocks/Hero";
import PageHero from "../blocks/PageHero";
import Summary from "../blocks/Summary";
import Team from "../blocks/Team";
@@ -30,7 +31,7 @@ const Pages = {
{
name: "blocks",
type: "blocks",
- blocks: [PageHero, Summary, Team],
+ blocks: [Hero, PageHero, Summary, Team],
localized: true,
admin: {
initCollapsed: true,
diff --git a/apps/climatemappedafrica/src/payload/fields/LocationSelect.js b/apps/climatemappedafrica/src/payload/fields/LocationSelect.js
new file mode 100644
index 000000000..7c11e4b49
--- /dev/null
+++ b/apps/climatemappedafrica/src/payload/fields/LocationSelect.js
@@ -0,0 +1,45 @@
+import { Select } from "payload/components/forms";
+import { select } from "payload/dist/fields/validations";
+import { createElement, useMemo } from "react";
+import useSWR from "swr";
+
+const fetcher = (url) => fetch(url).then((res) => res.json());
+const apiUrl = process.env.PAYLOAD_PUBLIC_APP_URL;
+
+export async function validateLocation(value, { hasMany, required, t }) {
+ const response = await fetch(`${apiUrl}/api/hurumap/profiles`);
+ const data = await response.json();
+ const { locations } = data ?? {};
+ const options =
+ locations
+ ?.filter(({ level }) => level !== "region")
+ ?.map((location) => ({
+ label: location.name,
+ value: location.code,
+ })) || [];
+ return select(value, { hasMany, options, required, t });
+}
+
+function LocationSelect(props) {
+ const url = `${apiUrl}/api/hurumap/profiles`;
+ const { data } = useSWR(url, fetcher, {
+ dedupingInterval: 60000,
+ revalidateOnFocus: false,
+ });
+ const { locations } = data ?? {};
+ const options = useMemo(
+ () =>
+ (
+ locations
+ ?.filter(({ level }) => level !== "region")
+ ?.map((location) => ({
+ label: location.name,
+ value: location.code,
+ })) || []
+ ).sort((a, b) => a.label.localeCompare(b.label)),
+ [locations],
+ );
+ return createElement(Select, { ...props, options });
+}
+
+export default LocationSelect;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5fb2fa8d8..15d2511ab 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1175,6 +1175,9 @@ importers:
simplebar-react:
specifier: 'catalog:'
version: 3.2.6(react@18.3.1)
+ slate:
+ specifier: 'catalog:'
+ version: 0.103.0
swr:
specifier: 'catalog:'
version: 2.2.5(react@18.3.1)
@@ -22808,7 +22811,7 @@ snapshots:
dependencies:
debug: 3.2.7
enhanced-resolve: 0.9.1
- eslint-plugin-import: 2.30.0(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1)
+ eslint-plugin-import: 2.30.0(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1)
find-root: 1.1.0
hasown: 2.0.2
interpret: 1.4.0