diff --git a/apps/climatemappedafrica/src/components/Hero/Hero.js b/apps/climatemappedafrica/src/components/Hero/Hero.js index 6085bc944..91239fb64 100644 --- a/apps/climatemappedafrica/src/components/Hero/Hero.js +++ b/apps/climatemappedafrica/src/components/Hero/Hero.js @@ -4,6 +4,8 @@ import dynamic from "next/dynamic"; import PropTypes from "prop-types"; import React, { useState } from "react"; +import Legend from "./Legend"; + import heroBg from "@/climatemappedafrica/assets/images/bg-map-white.jpg"; import DropdownSearch from "@/climatemappedafrica/components/DropdownSearch"; import Image from "@/climatemappedafrica/components/Image"; @@ -23,6 +25,8 @@ function Hero({ properties, level, explorePageSlug, + averageTemperature, + legend, ...props }) { const isUpLg = useMediaQuery((theme) => theme.breakpoints.up("lg")); @@ -99,45 +103,44 @@ function Hero({ {/* Since map is dynamic-ally loaded, no need for implementation="css" */} - - - {center ? ( - - ) : null} - - - {hoverGeo} - - - - + + + {center ? ( + + ) : null} + + + + {hoverGeo} + + + @@ -153,6 +156,8 @@ Hero.propTypes = { featuredLocations: PropTypes.arrayOf(PropTypes.shape({})), properties: PropTypes.shape({}), level: PropTypes.string, + explorePageSlug: PropTypes.string, + averageTemperature: 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 index 6b4302a5f..a0e855047 100644 --- a/apps/climatemappedafrica/src/components/Hero/Hero.snap.js +++ b/apps/climatemappedafrica/src/components/Hero/Hero.snap.js @@ -90,14 +90,40 @@ exports[` renders unchanged 1`] = `
+ class="MuiBox-root css-0" + > +
+
+
+
+
+
+
+
+
+
diff --git a/apps/climatemappedafrica/src/components/Hero/Hero.test.js b/apps/climatemappedafrica/src/components/Hero/Hero.test.js index ec342ec4f..2f50687e7 100644 --- a/apps/climatemappedafrica/src/components/Hero/Hero.test.js +++ b/apps/climatemappedafrica/src/components/Hero/Hero.test.js @@ -48,6 +48,15 @@ const defaultProps = { }, variant: "explore", icon: null, + legend: [ + { min: 10, max: 13, color: "#021AFE" }, + { min: 13, max: 16, color: "#5455FF" }, + { min: 16, max: 19, color: "#928EFD" }, + { min: 19, max: 22, color: "#B494DF" }, + { min: 22, max: 25, color: "#FA9B9B" }, + { min: 25, max: 28, color: "#F96264" }, + { min: 28, max: 31, color: "#F80701" }, + ], }; describe("", () => { diff --git a/apps/climatemappedafrica/src/components/Hero/Legend.js b/apps/climatemappedafrica/src/components/Hero/Legend.js new file mode 100644 index 000000000..77877f2a5 --- /dev/null +++ b/apps/climatemappedafrica/src/components/Hero/Legend.js @@ -0,0 +1,59 @@ +import { RichTypography } from "@commons-ui/legacy"; +import { Box } from "@mui/material"; +import PropTypes from "prop-types"; +import React, { useState, forwardRef } from "react"; + +const Legend = forwardRef(function Legend({ data, title }, ref) { + const [hoveredValue, setHoveredValue] = useState(null); + + const handleMouseEnter = (value) => { + setHoveredValue(value); + }; + + const handleMouseLeave = () => { + setHoveredValue(null); + }; + + return ( + + + {title} + + + {data.map((item) => ( + handleMouseEnter(`${item.min} - ${item.max}`)} + onMouseLeave={handleMouseLeave} + sx={{ + backgroundColor: item.color, + height: 24, + minWidth: 24, + cursor: "pointer", + }} + /> + ))} + {hoveredValue && ( + + {hoveredValue} + + )} + + + ); +}); + +Legend.propTypes = { + data: PropTypes.arrayOf( + PropTypes.shape({ + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + }), + ).isRequired, +}; + +export default Legend; diff --git a/apps/climatemappedafrica/src/components/Hero/Map.js b/apps/climatemappedafrica/src/components/Hero/Map.js index bd8f60512..c89c1c8a4 100644 --- a/apps/climatemappedafrica/src/components/Hero/Map.js +++ b/apps/climatemappedafrica/src/components/Hero/Map.js @@ -17,82 +17,85 @@ function Map({ geoJSONStyles = { color: "#2A2A2C", weight: 1, - opacity: 1, dashArray: "2", }, onLayerMouseOver, featuredLocations, explorePageSlug, + choropleth, }) { const router = useRouter(); - const countyCodes = featuredLocations?.map(({ code }) => code); - + const regionCodes = featuredLocations?.map(({ code }) => code); const theme = useTheme(); const onEachFeature = (feature, layer) => { + const choroplethColor = choropleth?.find?.( + (c) => c.code.toLowerCase() === feature.properties.code.toLowerCase(), + ); layer.setStyle({ fillColor: theme.palette.background.default, + ...choroplethColor, fillOpacity: 1, }); - if (countyCodes.includes(feature.properties.code?.toLowerCase())) { - layer.setStyle({ - weight: 1.5, - dashArray: 0, - }); + if (regionCodes.includes(feature.properties.code?.toLowerCase())) { + layer.setStyle(geoJSONStyles); layer.on("mouseover", () => { onLayerMouseOver(feature.properties.name.toLowerCase()); - layer.setStyle({ - fillColor: theme.palette.primary.main, - fillOpacity: 0.5, - }); + if (explorePageSlug) { + layer.setStyle({ + fillColor: choroplethColor?.fillColor, + fillOpacity: 0.4, + }); + } }); layer.on("mouseout", () => { onLayerMouseOver(null); - layer.setStyle({ - fillOpacity: 1, - fillColor: theme.palette.background.default, - }); + layer.setStyle({ ...choroplethColor, fillOpacity: 1, weight: 1 }); }); layer.on("click", () => { - router.push( - `/${explorePageSlug}/${feature.properties.code.toLowerCase()}`, - ); + if (explorePageSlug) { + router.push( + `/${explorePageSlug}/${feature.properties.code.toLowerCase()}`, + ); + } }); } }; return ( - - + - - + + + + ); } diff --git a/apps/climatemappedafrica/src/lib/data/blockify/explore-page.js b/apps/climatemappedafrica/src/lib/data/blockify/explore-page.js index 8377f338e..2f2179445 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/explore-page.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/explore-page.js @@ -1,13 +1,19 @@ import { fetchProfileGeography } from "@/climatemappedafrica/lib/hurumap"; -async function explorePage({ block: { slugs }, hurumap, hurumapProfile }) { +/** + * This function will be called only when HURUmap is enabled. + * @see @/climatemappedafrica/lib/data/common/index.js + */ +async function explorePage(block, _api, _context, { hurumap }) { const { - rootGeography, - labels: { dataNotAvailable, scrollToTop: scrollToTopLabel }, items: panelItems, - page: { value }, + labels: { dataNotAvailable, scrollToTop: scrollToTopLabel }, + profile: hurumapProfile, + profilePage, + rootGeography, } = hurumap; const { code: name } = rootGeography; + const { slugs } = block; const code = slugs.length ? slugs[0] : name; const { locations, preferredChildren, mapType, choropleth } = hurumapProfile; @@ -41,7 +47,7 @@ async function explorePage({ block: { slugs }, hurumap, hurumapProfile }) { blockType: "explore-page", choropleth, rootGeography, - explorePagePath: value.slug, + explorePagePath: profilePage.slug, locations, mapType, panel, diff --git a/apps/climatemappedafrica/src/lib/data/blockify/hero.js b/apps/climatemappedafrica/src/lib/data/blockify/hero.js index 66b91a3e0..f225159d4 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/hero.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/hero.js @@ -1,15 +1,21 @@ +import { generateChoropleth } from "@hurumap/next"; + import { fetchProfile, fetchProfileGeography, } from "@/climatemappedafrica/lib/hurumap"; -export default async function hero({ block, hurumap }) { +/** + * This function will be called even when HURUmap is disabled. + * @see @/climatemappedafrica/lib/data/common/index.js + * + */ +export default async function hero(block, _api, _context, { hurumap }) { const { - rootGeography: { center, code, rootGeographyHasData: pinRootGeography }, - page: { - value: { slug: explorePageSlug }, - }, - } = hurumap; + profilePage, + rootGeography: { center, code, hasData: pinRootGeography }, + profile: hurumapProfile, + } = hurumap ?? { rootGeography: {} }; const { geometries } = await fetchProfileGeography(code.toLowerCase()); const { level } = geometries.boundary?.properties ?? {}; const childLevelMaps = { @@ -18,6 +24,12 @@ export default async function hero({ block, hurumap }) { }; const childLevel = childLevelMaps[level]; const { locations, preferredChildren } = await fetchProfile(); + const chloropleth = hurumapProfile?.choropleth ?? null; + const { choropleth, legend } = generateChoropleth( + chloropleth, + locations, + "choropleth", + ); const preferredChildrenPerLevel = preferredChildren[level]; const { children } = geometries; const preferredLevel = @@ -26,15 +38,18 @@ export default async function hero({ block, hurumap }) { (location) => location.level === childLevel, ); const boundary = children[preferredLevel]; + return { ...block, - center, - slug: "hero", boundary, + center, + explorePageSlug: profilePage?.slug || null, featuredLocations, level, - properties: geometries.boundary?.properties, pinRootGeography, - explorePageSlug, + properties: geometries.boundary?.properties, + slug: "hero", + choropleth, + legend, }; } diff --git a/apps/climatemappedafrica/src/lib/data/blockify/index.js b/apps/climatemappedafrica/src/lib/data/blockify/index.js index b89ee1313..40d3247ef 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/index.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/index.js @@ -12,14 +12,13 @@ const propsifyBlockBySlug = { tutorial, }; -export const blockify = async (blocks, api, context, settings) => { - const { hurumap, hurumapProfile } = settings; +export const blockify = async ({ blocks }, api, context, settings) => { const promises = blocks?.map(async (block) => { const slug = block.blockType; const propsifyBlock = propsifyBlockBySlug[slug]; if (propsifyBlock) { - return propsifyBlock({ block, api, context, hurumap, hurumapProfile }); + return propsifyBlock(block, api, context, settings); } return { ...block, diff --git a/apps/climatemappedafrica/src/lib/data/blockify/page-hero.js b/apps/climatemappedafrica/src/lib/data/blockify/page-hero.js index 1be4c04a4..0a732a785 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/page-hero.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/page-hero.js @@ -1,6 +1,6 @@ import { imageFromMedia } from "@/climatemappedafrica/lib/data/utils"; -async function pageHero({ block }) { +async function pageHero(block) { const { background: media, ...others } = block; let background = null; if (media) { diff --git a/apps/climatemappedafrica/src/lib/data/blockify/team.js b/apps/climatemappedafrica/src/lib/data/blockify/team.js index 74c51abf6..ccc51693e 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/team.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/team.js @@ -8,7 +8,7 @@ import { equalsIgnoreCase } from "@/climatemappedafrica/utils"; const getCountryFromCode = (alpha3) => countries.find((c) => equalsIgnoreCase(c.alpha3, alpha3)) ?? null; -async function team({ block, api, context }) { +async function team(block, api, context) { const { query } = context; const data = await getMembers(api, query); let members = null; diff --git a/apps/climatemappedafrica/src/lib/data/blockify/tutorial.js b/apps/climatemappedafrica/src/lib/data/blockify/tutorial.js index c1ba44590..6e8c39fb3 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/tutorial.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/tutorial.js @@ -1,18 +1,19 @@ -async function tutorial({ hurumap }) { - const { - panel: { steps }, - enabled, - } = hurumap; +/** + * This function will be called only when HURUmap tutorial is enabled. + * @see @/climatemappedafrica/lib/data/common/index.js + */ +async function tutorial(block, _api, _context, { hurumap }) { + const { steps, enabled } = hurumap.tutorial; const items = steps.map((step) => ({ ...step, selector: `#${step.selector}`, })); return { - blockType: "tutorial", - id: "tutorial", + ...block, enabled, items, + slug: block.blockType, }; } diff --git a/apps/climatemappedafrica/src/lib/data/common/index.js b/apps/climatemappedafrica/src/lib/data/common/index.js index 33104d6b4..b784c3552 100644 --- a/apps/climatemappedafrica/src/lib/data/common/index.js +++ b/apps/climatemappedafrica/src/lib/data/common/index.js @@ -10,7 +10,7 @@ export function imageFromMedia(alt, url) { return { alt, src: url }; } -function getFooter(siteSettings, variant) { +function getFooter(variant, settings) { const { connect, footerNavigation, @@ -19,10 +19,8 @@ function getFooter(siteSettings, variant) { secondaryLogo, description, title, - } = siteSettings; - + } = settings.site; const { menus: footerMenus, ...footerProps } = footerNavigation; - const media = secondaryLogo || primaryLogo; const footerLogoUrl = typeof media === "string" ? null : media.url; @@ -40,37 +38,46 @@ function getFooter(siteSettings, variant) { }; } -async function getNavBar(siteSettings, variant, { slug }, hurumapProfile) { - const { locations } = hurumapProfile; +async function getNavBar(variant, settings) { + const { hurumap, site } = settings; const { connect: { links = [] }, - primaryNavigation: { menus = [], connect = [] }, - primaryLogo, drawerLogo, + primaryLogo, + primaryNavigation: { menus = [], connect = [] }, title, - } = siteSettings; + } = site; const socialLinks = links?.filter((link) => connect.includes(link.platform)); + let explorePagePath = null; + let locations = null; + if (hurumap?.enabled) { + explorePagePath = hurumap.profilePage.slug; + if (hurumap.profile) { + locations = hurumap.profile.locations; + } + } return { - logo: imageFromMedia(title, primaryLogo.url), drawerLogo: imageFromMedia(title, drawerLogo.url), - explorePagePath: slug, + explorePagePath, + locations, + logo: imageFromMedia(title, primaryLogo.url), menus, socialLinks, variant, - locations, }; } export async function getPagePaths(api) { const hurumapSettings = await api.findGlobal("settings-hurumap"); + let profilePage; + if (hurumapSettings?.enabled) { + profilePage = hurumapSettings.page.value; + } const { docs: pages } = await api.getCollection("pages"); - const { - page: { value: explorePage }, - } = hurumapSettings; const paths = pages.flatMap(({ slug }) => { // TODO(kilemensi): Handle parent > child page relation e.g. /insights/news - if (slug !== explorePage?.slug) { + if (slug !== profilePage?.slug) { return { params: { slugs: [slug === "index" ? "" : slug], @@ -80,7 +87,7 @@ export async function getPagePaths(api) { // HURUmap profile page return GEOGRAPHIES.map((code) => ({ params: { - slugs: [explorePage.slug, code], + slugs: [profilePage.slug, code], }, })); }); @@ -98,53 +105,45 @@ export async function getPageProps(api, context) { const { draftMode = false } = context; const options = { draft: draftMode }; - const hurumapProfile = await fetchProfile(); - const { docs: [page], } = await api.findPage(slug, options); - if (!page) { return null; } - const hurumap = await api.findGlobal("settings-hurumap"); - const { - page: { value: explorePage }, - } = hurumap; - const siteSettings = await api.findGlobal("settings-site"); - - const settings = { - hurumap, - hurumapProfile, - siteSettings, - }; - - let blocks = await blockify(page.blocks, api, context, settings); - const variant = page.slug === explorePage.slug ? "explore" : "default"; - - const footer = getFooter(siteSettings, variant); - const menus = await getNavBar( - siteSettings, - variant, - explorePage, - hurumapProfile, - ); - - if (slug === explorePage.slug) { - // The explore page is a special case. The only block we need to render is map and tutorial. - const explorePageBlocks = [ - { + let variant = "default"; + const settings = {}; + settings.site = (await api.findGlobal("settings-site")) || null; + const hurumapSettings = await api.findGlobal("settings-hurumap"); + if (hurumapSettings?.enabled) { + // TODO(koech): Handle cases when fetching profile fails? + const profile = await fetchProfile(); + const { page: hurumapPage, ...otherHurumapSettings } = hurumapSettings; + const { value: profilePage } = hurumapPage; + if (slug === profilePage.slug) { + variant = "explore"; + const explorePageBlock = { blockType: "explore-page", slugs: slugs.slice(1), - }, - { - blockType: "tutorial", - }, - ]; - blocks = await blockify(explorePageBlocks, api, context, settings); + }; + page.blocks.push(explorePageBlock); + if (hurumapSettings.tutorial?.enabled) { + const tutorialBlock = { blockType: "tutorial" }; + page.blocks.push(tutorialBlock); + } + } + settings.hurumap = { + ...otherHurumapSettings, + profile, + profilePage, + }; } + const blocks = await blockify(page, api, context, settings); + const footer = getFooter(variant, settings); + const menus = await getNavBar(variant, settings); + return { blocks, footer, diff --git a/apps/climatemappedafrica/src/payload/blocks/Hero.js b/apps/climatemappedafrica/src/payload/blocks/Hero.js index 674343824..6c2b6c23c 100644 --- a/apps/climatemappedafrica/src/payload/blocks/Hero.js +++ b/apps/climatemappedafrica/src/payload/blocks/Hero.js @@ -52,6 +52,13 @@ const Hero = { label: "Comment", localized: true, }, + { + name: "averageTemperature", + type: "text", + label: "Average Temperature", + defaultValue: "Average Temperature", + localized: true, + }, ], }; diff --git a/apps/climatemappedafrica/src/payload/globals/HURUMap/PanelOptions.js b/apps/climatemappedafrica/src/payload/globals/HURUMap/DataPanels.js similarity index 93% rename from apps/climatemappedafrica/src/payload/globals/HURUMap/PanelOptions.js rename to apps/climatemappedafrica/src/payload/globals/HURUMap/DataPanels.js index f47e0e99f..cf60493c5 100644 --- a/apps/climatemappedafrica/src/payload/globals/HURUMap/PanelOptions.js +++ b/apps/climatemappedafrica/src/payload/globals/HURUMap/DataPanels.js @@ -1,12 +1,13 @@ import image from "../../fields/image"; -const PanelOptions = { - label: "Panel Options", +const DataPanels = { + label: "Data Panels", fields: [ { name: "items", type: "array", label: "Panel Items", + required: true, fields: [ { type: "select", @@ -61,4 +62,4 @@ const PanelOptions = { ], }; -export default PanelOptions; +export default DataPanels; diff --git a/apps/climatemappedafrica/src/payload/globals/HURUMap/Profile.js b/apps/climatemappedafrica/src/payload/globals/HURUMap/Profile.js index 85f337a4a..7f33ea355 100644 --- a/apps/climatemappedafrica/src/payload/globals/HURUMap/Profile.js +++ b/apps/climatemappedafrica/src/payload/globals/HURUMap/Profile.js @@ -12,7 +12,7 @@ const Profile = { maxDepth: 1, required: true, admin: { - description: "The page to show the interactive map on.", + description: "The page to show the HURUmap profile on.", }, }, ], diff --git a/apps/climatemappedafrica/src/payload/globals/HURUMap/RootGeography.js b/apps/climatemappedafrica/src/payload/globals/HURUMap/RootGeography.js index 34ef7c658..d0b44ae0f 100644 --- a/apps/climatemappedafrica/src/payload/globals/HURUMap/RootGeography.js +++ b/apps/climatemappedafrica/src/payload/globals/HURUMap/RootGeography.js @@ -9,7 +9,6 @@ const RootGeography = { en: "Root Geography", }, type: "group", - localized: true, fields: [ { name: "code", @@ -17,7 +16,6 @@ const RootGeography = { label: { en: "Location Code", }, - localized: true, required: true, hasMany: false, defaultValue: "af", @@ -35,9 +33,8 @@ const RootGeography = { defaultValue: [20.0, 4.25], }, { - name: "rootGeographyHasData", + name: "hasData", type: "checkbox", - localized: true, label: { en: "Root geography has data", }, diff --git a/apps/climatemappedafrica/src/payload/globals/HURUMap/Tutorial.js b/apps/climatemappedafrica/src/payload/globals/HURUMap/Tutorial.js index 9e6852f0e..5aea722b9 100644 --- a/apps/climatemappedafrica/src/payload/globals/HURUMap/Tutorial.js +++ b/apps/climatemappedafrica/src/payload/globals/HURUMap/Tutorial.js @@ -26,25 +26,17 @@ const Tutorial = { label: "Tutorial", fields: [ { - name: "enabled", - label: { - en: "Enable Tutorial", - }, - type: "checkbox", - defaultValue: true, - localized: true, - }, - { - name: "panel", - label: { - en: "Tutorial Panel", - }, + name: "tutorial", type: "group", - localized: true, - admin: { - condition: (_, siblingData) => !!siblingData?.enabled, - }, fields: [ + { + name: "enabled", + label: { + en: "Enable HURUmap Tutorial", + }, + type: "checkbox", + defaultValue: true, + }, { name: "steps", label: { @@ -60,6 +52,7 @@ const Tutorial = { return data?.title || `Step ${String(index).padStart(2, "0")}`; }, }, + condition: (_, siblingData) => !!siblingData?.enabled, }, fields: [ { diff --git a/apps/climatemappedafrica/src/payload/globals/HURUMap/index.js b/apps/climatemappedafrica/src/payload/globals/HURUMap/index.js index 047dd2ce7..b91ebb6a1 100644 --- a/apps/climatemappedafrica/src/payload/globals/HURUMap/index.js +++ b/apps/climatemappedafrica/src/payload/globals/HURUMap/index.js @@ -1,4 +1,4 @@ -import PanelOptions from "./PanelOptions"; +import DataPanels from "./DataPanels"; import Profile from "./Profile"; import RootGeography from "./RootGeography"; import Tutorial from "./Tutorial"; @@ -23,7 +23,7 @@ const HURUMap = { }, { type: "tabs", - tabs: [Profile, RootGeography, Tutorial, PanelOptions], + tabs: [Profile, DataPanels, RootGeography, Tutorial], admin: { condition: (_, siblingData) => !!siblingData?.enableHURUMap, }, diff --git a/packages/hurumap-next/src/index.js b/packages/hurumap-next/src/index.js index 52d900496..9a9fc3d0e 100644 --- a/packages/hurumap-next/src/index.js +++ b/packages/hurumap-next/src/index.js @@ -1,2 +1,3 @@ export { default as Source } from "./Source"; export { default as Map } from "./Map"; +export { generateChoropleth } from "./Map/utils";