diff --git a/.dockerignore b/.dockerignore index 8b4a39e3f..337282801 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,4 @@ README.md **/.turbo **/.vscode **/build +**/media diff --git a/.gitignore b/.gitignore index 7c24dc8a8..e07935c43 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ storybook-static mongo-keyfile -#Google credentials +# Google credentials credentials.json # Sentry Config File @@ -67,3 +67,6 @@ credentials.json # SQLite files **.db **.sqlite + +# Payload media folder +media diff --git a/apps/civicsignalblog/.eslintrc.js b/apps/civicsignalblog/.eslintrc.js index 857290a40..4c66c3da2 100644 --- a/apps/civicsignalblog/.eslintrc.js +++ b/apps/civicsignalblog/.eslintrc.js @@ -1,6 +1,18 @@ module.exports = { root: true, extends: ["eslint-config-commons-ui/next"], + rules: { + "react/jsx-filename-extension": [1, { extensions: [".js", ".tsx"] }], // This rule allows JSX syntax in both .js and tsx files + // Disable requirement for importing file extensions for js and tsx files, without this we cant import custom components in Payload + "import/extensions": [ + "error", + "ignorePackages", + { + js: "never", + tsx: "never", + }, + ], + }, settings: { "import/resolver": { webpack: { diff --git a/apps/civicsignalblog/jest.config.js b/apps/civicsignalblog/jest.config.js index dea4ec013..85bba17cd 100644 --- a/apps/civicsignalblog/jest.config.js +++ b/apps/civicsignalblog/jest.config.js @@ -11,6 +11,8 @@ module.exports = { "/../../packages/commons-ui-core/src/$1", "^@/commons-ui/next/(.*)$": "/../../packages/commons-ui-next/src/$1", + "^@/commons-ui/payload/(.*)$": + "/../../packages/commons-ui-payload/src/$1", }, transformIgnorePatterns: ["/node_modules/(?!camelcase-keys)"], }; diff --git a/apps/civicsignalblog/jsconfig.json b/apps/civicsignalblog/jsconfig.json index 4d7756fda..7a3e94274 100644 --- a/apps/civicsignalblog/jsconfig.json +++ b/apps/civicsignalblog/jsconfig.json @@ -4,7 +4,8 @@ "paths": { "@/civicsignalblog/*": ["./src/*"], "@/commons-ui/core/*": ["../../packages/commons-ui-core/src/*"], - "@/commons-ui/next/*": ["../../packages/commons-ui-next/src/*"] + "@/commons-ui/next/*": ["../../packages/commons-ui-next/src/*"], + "@/commons-ui/payload/*": ["../../packages/commons-ui-payload/src/*"] } }, "exclude": ["node_modules"] diff --git a/apps/civicsignalblog/next.config.js b/apps/civicsignalblog/next.config.js index cb95c7b15..cd6a76170 100644 --- a/apps/civicsignalblog/next.config.js +++ b/apps/civicsignalblog/next.config.js @@ -17,7 +17,11 @@ const nextConfig = { }, pageExtensions: ["page.js"], reactStrictMode: true, - transpilePackages: ["@commons-ui/core", "@commons-ui/next"], + transpilePackages: [ + "@commons-ui/core", + "@commons-ui/next", + "@commons-ui/payload", + ], webpack: (config) => { config.module.rules.push( { diff --git a/apps/civicsignalblog/package.json b/apps/civicsignalblog/package.json index 9d88c16fa..762dd2384 100644 --- a/apps/civicsignalblog/package.json +++ b/apps/civicsignalblog/package.json @@ -41,6 +41,7 @@ "@aws-sdk/lib-storage": "catalog:", "@commons-ui/core": "workspace:*", "@commons-ui/next": "workspace:*", + "@commons-ui/payload": "workspace:*", "@emotion/cache": "catalog:", "@emotion/react": "catalog:", "@emotion/server": "catalog:", diff --git a/apps/civicsignalblog/payload.config.ts b/apps/civicsignalblog/payload.config.ts index 5cfa90220..8a20c6005 100644 --- a/apps/civicsignalblog/payload.config.ts +++ b/apps/civicsignalblog/payload.config.ts @@ -1,28 +1,30 @@ import path from "path"; -import { buildConfig } from "payload/config"; -import { slateEditor } from "@payloadcms/richtext-slate"; -import { mongooseAdapter } from "@payloadcms/db-mongodb"; +import { loadEnvConfig } from "@next/env"; import { webpackBundler } from "@payloadcms/bundler-webpack"; -import { CollectionConfig, GlobalConfig } from "payload/types"; +import { mongooseAdapter } from "@payloadcms/db-mongodb"; import { cloudStorage } from "@payloadcms/plugin-cloud-storage"; +import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3"; +import nestedDocs from "@payloadcms/plugin-nested-docs"; import { sentry } from "@payloadcms/plugin-sentry"; import seo from "@payloadcms/plugin-seo"; -import nestedDocs from "@payloadcms/plugin-nested-docs"; -import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3"; -import { loadEnvConfig } from "@next/env"; +import { slateEditor } from "@payloadcms/richtext-slate"; +import { Request, Response, NextFunction } from "express"; +import { buildConfig } from "payload/config"; +import { CollectionConfig, GlobalConfig } from "payload/types"; -import Actions from "./src/payload/components/actions"; +import MediaData from "./src/payload/collections/Main/MediaData"; +import CivicSignalPages from "./src/payload/collections/Main/Pages"; import Authors from "./src/payload/collections/Research/Authors"; import Media from "./src/payload/collections/Research/Media"; import Pages from "./src/payload/collections/Research/Pages"; -import CivicSignalPages from "./src/payload/collections/Main/Pages"; import Posts from "./src/payload/collections/Research/Posts"; -import Publication from "./src/payload/globals/Publication"; -import Research from "./src/payload/globals/Site/research"; -import Main from "./src/payload/globals/Site/main"; import Tags from "./src/payload/collections/Research/Tags"; import Users from "./src/payload/collections/Users"; +import Actions from "./src/payload/components/actions"; +import Publication from "./src/payload/globals/Publication"; +import Main from "./src/payload/globals/Site/main"; +import Research from "./src/payload/globals/Site/research"; import { applicationPages } from "./src/payload/lib/data/common/applications"; import { defaultLocale, locales } from "./src/payload/utils/locales"; @@ -37,6 +39,8 @@ const cors = ?.map((d) => d.trim()) ?.filter(Boolean) ?? []; +const customHeaders: string[] = ["CS-App"]; + const csrf = process?.env?.PAYLOAD_CSRF?.split(",") ?.map((d) => d.trim()) @@ -67,6 +71,7 @@ export default buildConfig({ Posts, Tags, CivicSignalPages, + MediaData, Users, ] as CollectionConfig[], globals: [Publication, Research, Main] as GlobalConfig[], @@ -153,4 +158,19 @@ export default buildConfig({ }), ] as any[], telemetry: process?.env?.NODE_ENV !== "production", + // We need to add a postMiddleware function to add support for custom headers in Payload + express: { + postMiddleware: [ + (_req: Request, res: Response, next: NextFunction) => { + const existingHeaders = + res.getHeader("Access-Control-Allow-Headers") || ""; + const controlHeaders = customHeaders + .concat((existingHeaders as string).split(",")) + .filter((h) => h) + .join(", "); + res.header("Access-Control-Allow-Headers", controlHeaders); + next(); + }, + ], + }, }); diff --git a/apps/civicsignalblog/public/images/cms/blocks/web_tools_page_header.png b/apps/civicsignalblog/public/images/cms/blocks/web_tools_page_header.png new file mode 100644 index 000000000..3457e6555 Binary files /dev/null and b/apps/civicsignalblog/public/images/cms/blocks/web_tools_page_header.png differ diff --git a/apps/civicsignalblog/server.ts b/apps/civicsignalblog/server.ts index 3b7d8418b..f7bfe2ce6 100644 --- a/apps/civicsignalblog/server.ts +++ b/apps/civicsignalblog/server.ts @@ -1,11 +1,12 @@ -import path from "path"; import { spawn } from "child_process"; +import path from "path"; + +import { loadEnvConfig } from "@next/env"; import express from "express"; import next from "next"; import nodemailerSendgrid from "nodemailer-sendgrid"; import payload from "payload"; import { Payload } from "payload/dist/payload"; -import { loadEnvConfig } from "@next/env"; const dev = process.env.NODE_ENV !== "production"; const projectDir = process.cwd(); diff --git a/apps/civicsignalblog/src/components/CMSContent/CMSContent.snap.js b/apps/civicsignalblog/src/components/CMSContent/CMSContent.snap.js index 4f8818320..451e90550 100644 --- a/apps/civicsignalblog/src/components/CMSContent/CMSContent.snap.js +++ b/apps/civicsignalblog/src/components/CMSContent/CMSContent.snap.js @@ -13,7 +13,7 @@ exports[` renders unchanged 1`] = ` > Women make up only 22% of the people seen, heard or read about in the news in Africa, the results of the  { const { richTextBlockFields: { content } = {} } = props; diff --git a/apps/civicsignalblog/src/components/LongFormRichText/LongFormRichText.snap.js b/apps/civicsignalblog/src/components/LongFormRichText/LongFormRichText.snap.js index 075b9d127..713fe9d07 100644 --- a/apps/civicsignalblog/src/components/LongFormRichText/LongFormRichText.snap.js +++ b/apps/civicsignalblog/src/components/LongFormRichText/LongFormRichText.snap.js @@ -10,7 +10,7 @@ exports[` renders unchanged 1`] = ` > Women make up only 22% of the people seen, heard or read about in the news in Africa, the results of the  {title} - - {subtitle} - + ); diff --git a/apps/civicsignalblog/src/components/PageHeader/PageHeader.snap.js b/apps/civicsignalblog/src/components/PageHeader/PageHeader.snap.js index 6fe4aa58c..36f2fabce 100644 --- a/apps/civicsignalblog/src/components/PageHeader/PageHeader.snap.js +++ b/apps/civicsignalblog/src/components/PageHeader/PageHeader.snap.js @@ -13,11 +13,23 @@ exports[` renders unchanged 1`] = ` > Contact -

- Let’s start something together! -

+

+ Let's + + start + + something + + together + +

+ diff --git a/apps/civicsignalblog/src/components/PageHeader/PageHeader.test.js b/apps/civicsignalblog/src/components/PageHeader/PageHeader.test.js index 983b3da9b..e447ac4fd 100644 --- a/apps/civicsignalblog/src/components/PageHeader/PageHeader.test.js +++ b/apps/civicsignalblog/src/components/PageHeader/PageHeader.test.js @@ -10,7 +10,16 @@ const render = createRender({ theme }); const defaultProps = { title: "Contact", - subtitle: "Let’s start something together!", + subtitle: [ + { + children: [ + { text: "Let's", children: null }, + { children: null, bold: true, text: " start " }, + { children: null, text: "something" }, + { children: null, bold: true, text: " together" }, + ], + }, + ], }; describe("", () => { diff --git a/apps/civicsignalblog/src/components/RichText/RichText.js b/apps/civicsignalblog/src/components/RichText/RichText.js deleted file mode 100644 index 3d3a9c3c2..000000000 --- a/apps/civicsignalblog/src/components/RichText/RichText.js +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable react/no-array-index-key */ -import { Link, RichTypography } from "@commons-ui/next"; -import { Box } from "@mui/material"; -import { deepmerge } from "@mui/utils"; -import React, { Fragment } from "react"; -import { Text } from "slate"; - -const DEFAULT_PROPS = { - html: false, -}; - -const serialize = (children, props) => - children?.map((node, i) => { - if (Text.isText(node)) { - let { text } = node; - if (node.bold) { - text = {text}; - } - if (node.code) { - text = {text}; - } - if (node.italic) { - text = {text}; - } - - // Handle other leaf types here... - - return {text}; - } - - if (!node) { - return null; - } - const nodeProps = deepmerge(DEFAULT_PROPS, props, { clone: true }); - // TODO(kilemensi): handle node.type === indent - switch (node.type) { - case "h1": - return ( - - {serialize(node.children)} - - ); - case "h2": - return ( - - {serialize(node.children)} - - ); - case "h3": - return ( - - {serialize(node.children)} - - ); - case "h4": - return ( - - {serialize(node.children)} - - ); - case "h5": - return ( - - {serialize(node.children)} - - ); - case "h6": - return ( - - {serialize(node.children)} - - ); - case "quote": - return
{serialize(node.children)}
; - case "link": - return ( - - {serialize(node.children)} - - ); - default: - return ( - - {serialize(node.children, props)} - - ); - } - }); - -const RichText = React.forwardRef(function RichText(props, ref) { - const { elements, variant, typographyProps, ...other } = props; - - if (!elements?.length) { - return null; - } - return ( - - {serialize(elements, typographyProps)} - - ); -}); - -export default RichText; diff --git a/apps/civicsignalblog/src/components/RichText/RichText.snap.js b/apps/civicsignalblog/src/components/RichText/RichText.snap.js deleted file mode 100644 index 8b0ddbe8e..000000000 --- a/apps/civicsignalblog/src/components/RichText/RichText.snap.js +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders unchanged 1`] = ` -
-`; diff --git a/apps/civicsignalblog/src/components/RichText/RichText.test.js b/apps/civicsignalblog/src/components/RichText/RichText.test.js deleted file mode 100644 index d8ddb6821..000000000 --- a/apps/civicsignalblog/src/components/RichText/RichText.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import { createRender } from "@commons-ui/testing-library"; -import React from "react"; - -import RichText from "./RichText"; - -import theme from "@/civicsignalblog/theme"; - -// eslint-disable-next-line testing-library/render-result-naming-convention -const render = createRender({ theme }); - -const defaultProps = { - elements: [ - { - children: [ - { - text: "The Charter Project is a pan-African initiative by a coalition of watchdog organisations that use civic technologies to strengthen democracy.", - children: null, - }, - ], - }, - { - children: [ - { - text: "We do this by helping digital activists and democracy changemakers leverage the African Union’s Charter on Democracy, Elections and Governance (ACDEG).", - children: null, - }, - ], - }, - { - children: [ - { - text: "The project currently supports initiatives in 11 countries. Find out more ", - children: null, - }, - { - type: "link", - linkType: "internal", - doc: { - value: "63887cf05bc566facccee049", - relationTo: "pages", - }, - children: [ - { - text: "here", - children: null, - }, - ], - href: "/", - }, - { - text: "", - children: null, - }, - ], - }, - ], -}; - -describe("", () => { - it("renders unchanged", () => { - const { container } = render(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/apps/civicsignalblog/src/components/RichText/index.js b/apps/civicsignalblog/src/components/RichText/index.js deleted file mode 100644 index 49b1d7e02..000000000 --- a/apps/civicsignalblog/src/components/RichText/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import RichText from "./RichText"; - -export default RichText; diff --git a/apps/civicsignalblog/src/payload/access/applications/main.js b/apps/civicsignalblog/src/payload/access/applications/main.js index 87b8cb19c..3cbb046a9 100644 --- a/apps/civicsignalblog/src/payload/access/applications/main.js +++ b/apps/civicsignalblog/src/payload/access/applications/main.js @@ -1,8 +1,8 @@ import canAccessApplication from "#civicsignalblog/payload/access/canAccessApplication"; import { MAIN } from "#civicsignalblog/payload/lib/data/common/applications"; -const canRead = ({ req: { user } }) => { - return canAccessApplication(user, MAIN); +const canRead = ({ req }) => { + return canAccessApplication(req, MAIN); }; export default canRead; diff --git a/apps/civicsignalblog/src/payload/access/applications/research.js b/apps/civicsignalblog/src/payload/access/applications/research.js index dccecce0d..807b219d6 100644 --- a/apps/civicsignalblog/src/payload/access/applications/research.js +++ b/apps/civicsignalblog/src/payload/access/applications/research.js @@ -1,8 +1,8 @@ import canAccessApplication from "#civicsignalblog/payload/access/canAccessApplication"; import { RESEARCH } from "#civicsignalblog/payload/lib/data/common/applications"; -const canRead = ({ req: { user } }) => { - return canAccessApplication(user, RESEARCH); +const canRead = ({ req }) => { + return canAccessApplication(req, RESEARCH); }; export default canRead; diff --git a/apps/civicsignalblog/src/payload/access/canAccessApplication.js b/apps/civicsignalblog/src/payload/access/canAccessApplication.js index faf206265..c191e1fef 100644 --- a/apps/civicsignalblog/src/payload/access/canAccessApplication.js +++ b/apps/civicsignalblog/src/payload/access/canAccessApplication.js @@ -1,7 +1,12 @@ -export default function canAccessApplication(user, searchString) { +export default async function canAccessApplication(req, searchString) { + const { user, headers } = req; + if (user) { - const app = user.currentApp || user.defaultApp; - return app === searchString.toLowerCase(); + const app = headers["CS-App"] || user.currentApp || user.defaultApp; + return ( + app.toLowerCase() === searchString.toLowerCase() && + user.allowedApps.includes(app) + ); } return false; } diff --git a/apps/civicsignalblog/src/payload/access/isAdminOrEditor.js b/apps/civicsignalblog/src/payload/access/isAdminOrEditor.js new file mode 100644 index 000000000..527acbb98 --- /dev/null +++ b/apps/civicsignalblog/src/payload/access/isAdminOrEditor.js @@ -0,0 +1,11 @@ +import { ROLE_ADMIN, ROLE_EDITOR } from "./roles"; + +const isAdminOrEditor = ({ req: { user } }) => { + // Return true or false based on if the user has an admin or editor role + return ( + Boolean(user?.roles?.includes(ROLE_ADMIN)) || + Boolean(user?.roles?.includes(ROLE_EDITOR)) + ); +}; + +export default isAdminOrEditor; diff --git a/apps/civicsignalblog/src/payload/access/roles.js b/apps/civicsignalblog/src/payload/access/roles.js index d060bbf8e..651cd2429 100644 --- a/apps/civicsignalblog/src/payload/access/roles.js +++ b/apps/civicsignalblog/src/payload/access/roles.js @@ -1,7 +1,9 @@ export const ROLE_ADMIN = "admin"; export const ROLE_EDITOR = "editor"; +export const ROLE_SUBSCRIBER = "subscriber"; export const ROLE_DEFAULT = ROLE_EDITOR; export const ROLE_OPTIONS = [ { label: "Admin", value: ROLE_ADMIN }, + { label: "Subscriber", value: ROLE_SUBSCRIBER }, { label: "Editor", value: ROLE_EDITOR }, ]; diff --git a/apps/civicsignalblog/src/payload/blocks/PageHeader.js b/apps/civicsignalblog/src/payload/blocks/PageHeader.js index 37d810022..0c31a80a9 100644 --- a/apps/civicsignalblog/src/payload/blocks/PageHeader.js +++ b/apps/civicsignalblog/src/payload/blocks/PageHeader.js @@ -1,5 +1,13 @@ +import { slateEditor } from "@payloadcms/richtext-slate"; + +import richText from "#civicsignalblog/payload/fields/richText"; + const PageHeader = { slug: "page-header", + labels: { + singular: "Page Header", + plural: "Page Headers", + }, imageURL: "/images/cms/blocks/page_header.jpg", imageAltText: "Header for content pages such as contact page.", fields: [ @@ -8,11 +16,15 @@ const PageHeader = { required: true, type: "text", }, - { + richText({ name: "subtitle", - required: true, - type: "text", - }, + editor: slateEditor({ + admin: { + elements: ["link"], + leaves: ["bold", "italic", "underline"], + }, + }), + }), ], }; diff --git a/apps/civicsignalblog/src/payload/collections/Main/MediaData.js b/apps/civicsignalblog/src/payload/collections/Main/MediaData.js new file mode 100644 index 000000000..5e9ae2c56 --- /dev/null +++ b/apps/civicsignalblog/src/payload/collections/Main/MediaData.js @@ -0,0 +1,55 @@ +import { slateEditor } from "@payloadcms/richtext-slate"; + +import canRead from "#civicsignalblog/payload/access/applications/main"; +import isAdminOrEditor from "#civicsignalblog/payload/access/isAdminOrEditor"; +import document from "#civicsignalblog/payload/fields/document"; +import image from "#civicsignalblog/payload/fields/image"; +import richText from "#civicsignalblog/payload/fields/richText"; + +const MediaData = { + slug: "media-data", + access: { + read: canRead, + update: isAdminOrEditor, + create: isAdminOrEditor, + delete: isAdminOrEditor, + }, + admin: { + defaultColumns: ["title", "updatedAt"], + enableRichTextLink: false, + group: "Publication", + useAsTitle: "title", + }, + fields: [ + { + name: "title", + type: "text", + required: true, + localized: true, + }, + image({ + overrides: { + name: "thumbnail", + required: true, + localized: true, + }, + }), + document({ + overrides: { + name: "document", + required: true, + }, + }), + richText({ + name: "description", + editor: slateEditor({ + admin: { + elements: ["link"], + leaves: ["bold", "italic", "underline"], + }, + }), + }), + ], +}; + +export default MediaData; diff --git a/apps/civicsignalblog/src/payload/collections/Main/Pages.js b/apps/civicsignalblog/src/payload/collections/Main/Pages.js index 20fc0f9ec..bfd5e1f1d 100644 --- a/apps/civicsignalblog/src/payload/collections/Main/Pages.js +++ b/apps/civicsignalblog/src/payload/collections/Main/Pages.js @@ -1,10 +1,6 @@ import canRead from "#civicsignalblog/payload/access/applications/main"; -import CustomPageHeader from "#civicsignalblog/payload/blocks/CustomPageHeader"; -import Error from "#civicsignalblog/payload/blocks/Error"; -import FeaturedStories from "#civicsignalblog/payload/blocks/FeaturedStories"; -import LongForm from "#civicsignalblog/payload/blocks/LongForm"; +import isAdminOrEditor from "#civicsignalblog/payload/access/isAdminOrEditor"; import PageHeader from "#civicsignalblog/payload/blocks/PageHeader"; -import Posts from "#civicsignalblog/payload/blocks/Posts"; import { MAIN } from "#civicsignalblog/payload/lib/data/common/applications"; import pages from "#civicsignalblog/payload/utils/createPagesCollection"; @@ -13,16 +9,12 @@ const Pages = pages({ label: "Pages", group: "Publication", defaultColumns: ["fullTitle", "updatedAt"], - blocks: [ - Error, - FeaturedStories, - PageHeader, - Posts, - CustomPageHeader, - LongForm, - ], + blocks: [PageHeader], access: { read: canRead, + update: isAdminOrEditor, + create: isAdminOrEditor, + delete: isAdminOrEditor, }, }); diff --git a/apps/civicsignalblog/src/payload/collections/Research/Authors.js b/apps/civicsignalblog/src/payload/collections/Research/Authors.js index 02e870655..cc1348552 100644 --- a/apps/civicsignalblog/src/payload/collections/Research/Authors.js +++ b/apps/civicsignalblog/src/payload/collections/Research/Authors.js @@ -1,9 +1,13 @@ import canRead from "#civicsignalblog/payload/access/applications/research"; +import isAdminOrEditor from "#civicsignalblog/payload/access/isAdminOrEditor"; const Authors = { slug: "author", access: { read: canRead, + update: isAdminOrEditor, + create: isAdminOrEditor, + delete: isAdminOrEditor, }, admin: { defaultColumns: ["fullName", "updatedAt"], diff --git a/apps/civicsignalblog/src/payload/collections/Research/Media.js b/apps/civicsignalblog/src/payload/collections/Research/Media.js index bbd5b3040..67dd31c0f 100644 --- a/apps/civicsignalblog/src/payload/collections/Research/Media.js +++ b/apps/civicsignalblog/src/payload/collections/Research/Media.js @@ -1,3 +1,5 @@ +import isAdminOrEditor from "#civicsignalblog/payload/access/isAdminOrEditor"; + const Media = { slug: "media", admin: { @@ -8,6 +10,9 @@ const Media = { }, access: { read: () => true, // Everyone can read Media + update: isAdminOrEditor, + create: isAdminOrEditor, + delete: isAdminOrEditor, }, upload: { staticURL: "/media", diff --git a/apps/civicsignalblog/src/payload/collections/Research/Pages.js b/apps/civicsignalblog/src/payload/collections/Research/Pages.js index b12f8f5c2..df11533c3 100644 --- a/apps/civicsignalblog/src/payload/collections/Research/Pages.js +++ b/apps/civicsignalblog/src/payload/collections/Research/Pages.js @@ -1,4 +1,5 @@ import canRead from "#civicsignalblog/payload/access/applications/research"; +import isAdminOrEditor from "#civicsignalblog/payload/access/isAdminOrEditor"; import CustomPageHeader from "#civicsignalblog/payload/blocks/CustomPageHeader"; import Error from "#civicsignalblog/payload/blocks/Error"; import FeaturedStories from "#civicsignalblog/payload/blocks/FeaturedStories"; @@ -23,6 +24,9 @@ const Pages = pages({ ], access: { read: canRead, + update: isAdminOrEditor, + create: isAdminOrEditor, + delete: isAdminOrEditor, }, adminOptions: { description: "Research", diff --git a/apps/civicsignalblog/src/payload/collections/Research/Posts.js b/apps/civicsignalblog/src/payload/collections/Research/Posts.js index 0edd10398..0a796365b 100644 --- a/apps/civicsignalblog/src/payload/collections/Research/Posts.js +++ b/apps/civicsignalblog/src/payload/collections/Research/Posts.js @@ -1,4 +1,5 @@ import canRead from "#civicsignalblog/payload/access/applications/research"; +import isAdminOrEditor from "#civicsignalblog/payload/access/isAdminOrEditor"; import authors from "#civicsignalblog/payload/fields/authors"; import content from "#civicsignalblog/payload/fields/content"; import image from "#civicsignalblog/payload/fields/image"; @@ -19,6 +20,9 @@ const Posts = { }, access: { read: canRead, + update: isAdminOrEditor, + create: isAdminOrEditor, + delete: isAdminOrEditor, }, admin: { defaultColumns: ["title", "authors", "publishedOn"], diff --git a/apps/civicsignalblog/src/payload/collections/Research/Tags.js b/apps/civicsignalblog/src/payload/collections/Research/Tags.js index e2c29b87a..b3e23b49e 100644 --- a/apps/civicsignalblog/src/payload/collections/Research/Tags.js +++ b/apps/civicsignalblog/src/payload/collections/Research/Tags.js @@ -1,4 +1,5 @@ import canRead from "#civicsignalblog/payload/access/applications/research"; +import isAdminOrEditor from "#civicsignalblog/payload/access/isAdminOrEditor"; import slug from "#civicsignalblog/payload/fields/slug/index"; const Tags = { @@ -11,6 +12,9 @@ const Tags = { }, access: { read: canRead, + update: isAdminOrEditor, + create: isAdminOrEditor, + delete: isAdminOrEditor, }, fields: [ { diff --git a/apps/civicsignalblog/src/payload/collections/Users.js b/apps/civicsignalblog/src/payload/collections/Users.js index 05e4dc624..1e0052782 100644 --- a/apps/civicsignalblog/src/payload/collections/Users.js +++ b/apps/civicsignalblog/src/payload/collections/Users.js @@ -12,6 +12,8 @@ import { ROLE_DEFAULT, ROLE_OPTIONS, } from "#civicsignalblog/payload/access/roles"; +import CurrentAppSelectField from "#civicsignalblog/payload/fields/customSelect/currentApp"; +import DefaultAppSelectField from "#civicsignalblog/payload/fields/customSelect/defaultApp"; import applications, { RESEARCH, } from "#civicsignalblog/payload/lib/data/common/applications"; @@ -32,6 +34,7 @@ const Users = { }, auth: { verify: true, + useAPIKey: true, }, fields: [ { @@ -64,25 +67,15 @@ const Users = { }, options: ROLE_OPTIONS, }, + DefaultAppSelectField, + CurrentAppSelectField, { - name: "defaultApp", - type: "select", - defaultValue: RESEARCH, - hasMany: false, - admin: { - isClearable: true, - isSortable: true, - }, - options: applications, - }, - { - name: "currentApp", + name: "allowedApps", defaultValue: RESEARCH, type: "select", - hasMany: false, + hasMany: true, admin: { isClearable: true, - isSortable: true, }, options: applications, }, diff --git a/apps/civicsignalblog/src/payload/components/actions/index.tsx b/apps/civicsignalblog/src/payload/components/actions/index.tsx index b31ee262f..14fdfa6a2 100644 --- a/apps/civicsignalblog/src/payload/components/actions/index.tsx +++ b/apps/civicsignalblog/src/payload/components/actions/index.tsx @@ -15,6 +15,7 @@ function BeforeDashboard() { ); const [loading, setLoading] = useState(false); + const allowedApps = Array.isArray(user.allowedApps) ? user.allowedApps : []; useEffect(() => { const updateCurrentApp = async () => { @@ -85,11 +86,14 @@ function BeforeDashboard() { }} > - {applications.map((app) => ( - - ))} + {applications.map( + (app) => + allowedApps.includes(app.value) && ( + + ), + )} )} diff --git a/apps/civicsignalblog/src/payload/components/allowedAppSelect/index.tsx b/apps/civicsignalblog/src/payload/components/allowedAppSelect/index.tsx new file mode 100644 index 000000000..4c9dccbca --- /dev/null +++ b/apps/civicsignalblog/src/payload/components/allowedAppSelect/index.tsx @@ -0,0 +1,42 @@ +import { SelectInput, useField } from "payload/components/forms"; +import { useAuth } from "payload/components/utilities"; +import React, { useState, useEffect } from "react"; + +import applications from "#civicsignalblog/payload/lib/data/common/applications"; + +function CustomSelectComponent({ path, label }) { + const { user } = useAuth(); + const { value, setValue } = useField({ path }); + const [options, setOptions] = useState([]); + + useEffect(() => { + const allowedApps = user?.allowedApps || []; // We could cast this using as string[]; but this can't be parsed without adding a ts parser + if (Array.isArray(allowedApps)) { + const filteredApps = applications + .filter((app) => allowedApps.includes(app.value)) + .map((app) => ({ + label: app.label, + value: app.value, + })); + setOptions(filteredApps); + } + }, [user?.allowedApps]); + + return ( +
+ + setValue(e.value)} + /> +
+ ); +} + +export default CustomSelectComponent; diff --git a/apps/civicsignalblog/src/payload/fields/customSelect/currentApp.js b/apps/civicsignalblog/src/payload/fields/customSelect/currentApp.js new file mode 100644 index 000000000..7f2a68d28 --- /dev/null +++ b/apps/civicsignalblog/src/payload/fields/customSelect/currentApp.js @@ -0,0 +1,14 @@ +import CustomSelectComponent from "#civicsignalblog/payload/components/allowedAppSelect/index"; + +const CurrentAppSelectField = { + name: "currentApp", + type: "text", + admin: { + components: { + Field: CustomSelectComponent, + }, + }, + label: "Current App", +}; + +export default CurrentAppSelectField; diff --git a/apps/civicsignalblog/src/payload/fields/customSelect/defaultApp.js b/apps/civicsignalblog/src/payload/fields/customSelect/defaultApp.js new file mode 100644 index 000000000..2ba2d669e --- /dev/null +++ b/apps/civicsignalblog/src/payload/fields/customSelect/defaultApp.js @@ -0,0 +1,14 @@ +import CustomSelectComponent from "#civicsignalblog/payload/components/allowedAppSelect/index"; + +const DefaultAppSelectField = { + name: "defaultApp", + type: "text", + admin: { + components: { + Field: CustomSelectComponent, + }, + }, + label: "Default App", +}; + +export default DefaultAppSelectField; diff --git a/apps/civicsignalblog/src/payload/fields/document.js b/apps/civicsignalblog/src/payload/fields/document.js new file mode 100644 index 000000000..0c3cc8043 --- /dev/null +++ b/apps/civicsignalblog/src/payload/fields/document.js @@ -0,0 +1,19 @@ +import { deepmerge } from "@mui/utils"; + +function document({ overrides = undefined } = {}) { + const documentResult = { + name: "document", + type: "upload", + relationTo: "media", + filterOptions: { + mimeType: { contains: "application/pdf" }, // Restricts to PDF files + }, + admin: { + description: "Upload PDF files only", + }, + }; + + return deepmerge(documentResult, overrides); +} + +export default document; diff --git a/apps/civicsignalblog/src/payload/globals/Site/main.js b/apps/civicsignalblog/src/payload/globals/Site/main.js index 733c873e9..c2f063386 100644 --- a/apps/civicsignalblog/src/payload/globals/Site/main.js +++ b/apps/civicsignalblog/src/payload/globals/Site/main.js @@ -13,6 +13,10 @@ const Main = settings({ access: { read: canRead, }, + auth: { + verify: true, + useAPIKey: true, + }, tabs: [GeneralTab, NavigationTab, EngagementTab], }); diff --git a/apps/civicsignalblog/src/payload/lib/data/common/applications.js b/apps/civicsignalblog/src/payload/lib/data/common/applications.js index ce78f5b58..dfbbe9e74 100644 --- a/apps/civicsignalblog/src/payload/lib/data/common/applications.js +++ b/apps/civicsignalblog/src/payload/lib/data/common/applications.js @@ -1,7 +1,7 @@ -export const MAIN = "main"; +export const MAIN = "tools"; export const EXPLORER = "explorer"; -export const TOPIC_MAPPER = "topic-mapper"; -export const SOURCE_MAPPER = "source-manager"; +export const TOPIC_MAPPER = "topics"; +export const SOURCE_MAPPER = "sources"; export const RESEARCH = "research"; const applicationLabels = { diff --git a/apps/civicsignalblog/tsconfig.json b/apps/civicsignalblog/tsconfig.json index b160775f0..7f5e2fa58 100644 --- a/apps/civicsignalblog/tsconfig.json +++ b/apps/civicsignalblog/tsconfig.json @@ -18,6 +18,7 @@ "@/civicsignalblog/*": ["./src/*"], "@/commons-ui/core/*": ["../../packages/commons-ui-core/src/*"], "@/commons-ui/next/*": ["../../packages/commons-ui-next/src/*"], + "@/commons-ui/payload/*": ["../../packages/commons-ui-payload/src/*"], "#civicsignalblog*": ["./src/*"] } }, diff --git a/apps/climatemappedafrica/.env.template b/apps/climatemappedafrica/.env.template index 61c5a0d8b..600a19c60 100644 --- a/apps/climatemappedafrica/.env.template +++ b/apps/climatemappedafrica/.env.template @@ -1,12 +1,4 @@ -# Learn more about ENV variables at https://github.com/WebDevStudios/nextjs-wordpress-starter/wiki/env-variables - -# Tells Next.js we're in development mode. You do not need a Vercel account for this. -VERCEL_ENV="development" - -NEXT_PUBLIC_APP_URL = 'http://localhost:3000' - -# Allows Node to work with local, self-signed certificates. -NODE_TLS_REJECT_UNAUTHORIZED="0" +NEXT_PUBLIC_APP_URL="http://localhost:3000" # The domain where your images are hosted on. # See https://nextjs.org/docs/basic-features/image-optimization#domains @@ -16,9 +8,6 @@ NEXT_PUBLIC_IMAGE_DOMAINS="cms.dev.codeforafrica.org" # See https://vega.github.io/vega/docs/api/view/#view_toImageURL NEXT_PUBLIC_IMAGE_SCALE_FACTOR=2 -# HURUmap URL -HURUMAP_API_URL="https://ng.hurumap.org/api/v1/" - # openAFRICA domains # A comma-separated list of domains to openAFRICA (or any CKAN-based site domain) NEXT_PUBLIC_OPENAFRICA_DOMAINS= @@ -28,11 +17,19 @@ NEXT_PUBLIC_OPENAFRICA_DOMAINS= # based site domain) NEXT_PUBLIC_SOURCEAFRICA_DOMAINS= +# Google Analytics +NEXT_PUBLIC_GOOGLE_ANALYTICS_ID = "G-xxxxxxxx" + # AWS S3 bucket for storing images -S3_UPLOAD_KEY=AAAAAAAAAAAAAAAAAAAA -S3_UPLOAD_SECRET=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -S3_UPLOAD_BUCKET=name-of-s3-bucket -S3_UPLOAD_REGION=bucket-region-us-east-1 +S3_ACCESS_KEY_ID=AAAAAAAAAAAAAAAAAAAA +S3_SECRET_ACCESS_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +S3_BUCKET=name-of-s3-bucket +S3_REGION=bucket-region-us-east-1 -#Analytics -NEXT_PUBLIC_GOOGLE_ANALYTICS_ID = "G-xxxxxxxx" +# Payload +MONGO_URL="mongodb+srv://:@host/db" +PAYLOAD_PUBLIC_APP_URL="http://localhost:3000" +PAYLOAD_SECRET="secure random string" + +# HURUmap +HURUMAP_API_URL="https://ng.hurumap.org/api/v1/" diff --git a/apps/climatemappedafrica/next.config.js b/apps/climatemappedafrica/next.config.js index 9bab1d826..f48c38659 100644 --- a/apps/climatemappedafrica/next.config.js +++ b/apps/climatemappedafrica/next.config.js @@ -13,6 +13,8 @@ module.exports = { domains: process.env.NEXT_PUBLIC_IMAGE_DOMAINS?.split(",") ?.map((d) => d.trim()) ?.filter((d) => d), + loader: "custom", + loaderFile: "./payload.image.loader.js", unoptimized: process.env.NEXT_PUBLIC_IMAGE_UNOPTIMIZED?.trim()?.toLowerCase() === "true", diff --git a/apps/climatemappedafrica/payload.config.ts b/apps/climatemappedafrica/payload.config.ts index 70ac036cd..1b06958af 100644 --- a/apps/climatemappedafrica/payload.config.ts +++ b/apps/climatemappedafrica/payload.config.ts @@ -34,12 +34,16 @@ const csrf = ?.map((d) => d.trim()) ?.filter(Boolean) ?? []; -const adapter = s3Adapter({ +const accessKeyId = process?.env?.S3_ACCESS_KEY_ID; +const secretAccessKey = process?.env?.S3_SECRET_ACCESS_KEY; +const enableCloudStorage = Boolean(accessKeyId && secretAccessKey); + +const cloudStorageAdapter = s3Adapter({ config: { region: process?.env?.S3_REGION, credentials: { - accessKeyId: process?.env?.S3_ACCESS_KEY_ID, - secretAccessKey: process?.env?.S3_SECRET_ACCESS_KEY, + accessKeyId, + secretAccessKey, }, }, bucket: process?.env?.S3_BUCKET, @@ -106,11 +110,11 @@ export default buildConfig({ }, plugins: [ cloudStorage({ + enabled: enableCloudStorage, collections: { media: { - adapter, - // TODO(kilemensi): Toogle this depending on ENV? - disableLocalStorage: false, + adapter: cloudStorageAdapter, + disableLocalStorage: enableCloudStorage, prefix: "media", }, }, diff --git a/apps/climatemappedafrica/payload.image.loader.js b/apps/climatemappedafrica/payload.image.loader.js new file mode 100644 index 000000000..acb0916bc --- /dev/null +++ b/apps/climatemappedafrica/payload.image.loader.js @@ -0,0 +1,11 @@ +"use client"; + +import site from "@/climatemappedafrica/utils/site"; + +export default function payloadImageLoader({ src }) { + // Handle relative paths (/media) only + if (src?.startsWith("/media")) { + return `${site.url}${src}`; + } + return src; +} diff --git a/apps/climatemappedafrica/src/components/DropdownSearch/DownloadSearch.js b/apps/climatemappedafrica/src/components/DropdownSearch/DownloadSearch.js new file mode 100644 index 000000000..dc9f31362 --- /dev/null +++ b/apps/climatemappedafrica/src/components/DropdownSearch/DownloadSearch.js @@ -0,0 +1,186 @@ +import { + IconButton, + InputBase, + Typography, + List, + ListItem, + SvgIcon, + Box, +} from "@mui/material"; +import { useRouter } from "next/router"; +import PropTypes from "prop-types"; +import React, { useEffect, useState } from "react"; + +import SearchIcon from "@/climatemappedafrica/assets/icons/search.svg"; +import Link from "@/climatemappedafrica/components/Link"; + +function DropdownSearch({ + href: hrefProp = "/explore", + label = "Search for a location", + locations, + onClick, + icon: IconProp = SearchIcon, + placeholder, + variant, + ...props +}) { + const router = useRouter(); + const [query, setQuery] = useState(""); + const [selectedLocation, setSelectedLocation] = useState(null); + const [suggestions, setSuggestions] = useState([]); + + const handleChange = (e) => { + setQuery(e.target.value); + setSelectedLocation(null); + }; + + const handleSelect = (code, name) => { + setQuery(name.toLowerCase()); + setSelectedLocation(code); + if (code && hrefProp?.length) { + router.push(`${hrefProp}/${code}`); + } + }; + + useEffect(() => { + if (query?.length > 0 && !selectedLocation) { + const matchedGeo = locations?.filter(({ name }) => + name.toLowerCase()?.startsWith(query.toLowerCase()), + ); + setSuggestions(matchedGeo); + } else { + setSuggestions([]); + } + }, [locations, selectedLocation, query]); + + const handleClickSearch = () => { + if (onClick) { + onClick(selectedLocation); + } else if (selectedLocation) { + const href = `${hrefProp}/${selectedLocation}`; + router.push(href); + } else if (query) { + router.push("/404"); + } + }; + + let iconComponent = SearchIcon; + let iconBorder; + if (variant === "explore") { + iconComponent = IconProp; + iconBorder = { + borderRadius: "50%", + border: "2px solid #fff", + }; + } + const searchIconButton = ( + ({ + padding: 0, + ml: 2, + })} + > + + + ); + + return ( + + ({ + color: palette.text.primary, + marginBottom: typography.pxToRem(10), + })} + > + {label} + + ({ + borderRadius: typography.pxToRem(10), + color: palette.primary.main, + border: `2px solid ${palette.text.hint}`, + width: typography.pxToRem(278), + backgroundColor: "inherit", + height: typography.pxToRem(48), + padding: `0 ${typography.pxToRem(20)}`, + "&.MuiInputBase-input": { + backgroundColor: "inherit", + height: typography.pxToRem(48), + borderRadius: typography.pxToRem(10), + padding: `0 ${typography.pxToRem(20)}`, + textTransform: "capitalize", + }, + "&.Mui-focused": { + border: `2px solid ${palette.primary.main}`, + }, + ...props.sx, + })} + endAdornment={variant === "explore" ? searchIconButton : null} + /> + {variant !== "explore" && searchIconButton} + + + {suggestions?.length > 0 && ( + ({ + width: typography.pxToRem(278), + position: "absolute", + marginTop: typography.pxToRem(5), + zIndex: 10, + background: palette.background.default, + border: `2px solid ${palette.grey.main}`, + borderRadius: typography.pxToRem(10), + padding: 0, + textTransform: "capitalize", + })} + > + {suggestions.map(({ name, code }) => ( + handleSelect(code, name)} + sx={({ typography, palette }) => ({ + paddingLeft: typography.pxToRem(20), + color: palette.text.hint, + })} + key={code} + > + {name.toLowerCase()} + + ))} + + )} + + + ); +} + +DropdownSearch.propTypes = { + label: PropTypes.string, + href: PropTypes.string, + onClick: PropTypes.func, + icon: PropTypes.elementType, + locations: PropTypes.arrayOf(PropTypes.shape({})), + variant: PropTypes.string, + placeholder: PropTypes.string, +}; + +export default DropdownSearch; diff --git a/apps/climatemappedafrica/src/components/DropdownSearch/DropdownSearch.snap.js b/apps/climatemappedafrica/src/components/DropdownSearch/DropdownSearch.snap.js new file mode 100644 index 000000000..d8dd3412a --- /dev/null +++ b/apps/climatemappedafrica/src/components/DropdownSearch/DropdownSearch.snap.js @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders unchanged 1`] = ` +
+ +`; diff --git a/apps/climatemappedafrica/src/components/DropdownSearch/DropdownSearch.test.js b/apps/climatemappedafrica/src/components/DropdownSearch/DropdownSearch.test.js new file mode 100644 index 000000000..c50350da4 --- /dev/null +++ b/apps/climatemappedafrica/src/components/DropdownSearch/DropdownSearch.test.js @@ -0,0 +1,25 @@ +import { createRender } from "@commons-ui/testing-library"; +import React from "react"; + +import DropdownSearch from "./DownloadSearch"; + +import theme from "@/climatemappedafrica/theme"; + +// eslint-disable-next-line testing-library/render-result-naming-convention +const render = createRender({ theme }); + +const defaultProps = { + href: "/explore", + label: "Search for a location", + locations: [], + icon: null, + placeholder: "Search for a location", + variant: "explore", +}; + +describe("", () => { + it("renders unchanged", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/apps/climatemappedafrica/src/components/DropdownSearch/index.js b/apps/climatemappedafrica/src/components/DropdownSearch/index.js index dfb33c3f3..958811b94 100644 --- a/apps/climatemappedafrica/src/components/DropdownSearch/index.js +++ b/apps/climatemappedafrica/src/components/DropdownSearch/index.js @@ -1,195 +1,3 @@ -import { - IconButton, - InputBase, - Typography, - List, - ListItem, - SvgIcon, -} from "@mui/material"; -import makeStyles from "@mui/styles/makeStyles"; -import { useRouter } from "next/router"; -import PropTypes from "prop-types"; -import React, { useEffect, useState } from "react"; - -import SearchIcon from "@/climatemappedafrica/assets/icons/search.svg"; -import Link from "@/climatemappedafrica/components/Link"; - -const useStyles = makeStyles(({ palette, typography }) => ({ - root: {}, - inputRoot: { - borderRadius: typography.pxToRem(10), - color: palette.primary.main, - border: `2px solid ${palette.text.hint}`, - width: typography.pxToRem(278), - }, - focused: { - border: `2px solid ${palette.primary.main}`, - }, - label: { - color: palette.text.secondary, - marginBottom: typography.pxToRem(10), - }, - button: { - padding: 0, - marginLeft: typography.pxToRem(15), - }, - input: { - backgroundColor: "inherit", - height: typography.pxToRem(48), - borderRadius: typography.pxToRem(10), - padding: `0 ${typography.pxToRem(20)}`, - textTransform: "capitalize", - }, - suggestions: { - position: "relative", - }, - selectMenu: { - width: typography.pxToRem(278), - position: "absolute", - marginTop: typography.pxToRem(5), - zIndex: 10, - background: palette.background.default, - border: `2px solid ${palette.grey.main}`, - borderRadius: typography.pxToRem(10), - padding: 0, - textTransform: "capitalize", - }, - menuList: {}, - menuItem: { - paddingLeft: typography.pxToRem(20), - color: palette.text.hint, - }, -})); - -function DropdownSearch({ - href: hrefProp = "/explore", - label = "Search for a location", - locations, - onClick, - icon: IconProp = SearchIcon, - placeholder, - variant, - ...props -}) { - const classes = useStyles(props); - const router = useRouter(); - const [query, setQuery] = useState(""); - const [selectedLocation, setSelectedLocation] = useState(null); - const [suggestions, setSuggestions] = useState([]); - - const handleChange = (e) => { - setQuery(e.target.value); - setSelectedLocation(null); - }; - - const handleSelect = (code, name) => { - setQuery(name.toLowerCase()); - setSelectedLocation(code); - if (code && hrefProp?.length) { - router.push(`${hrefProp}/${code}`); - } - }; - - useEffect(() => { - if (query?.length > 0 && !selectedLocation) { - const matchedGeo = locations?.filter(({ name }) => - name.toLowerCase()?.startsWith(query.toLowerCase()), - ); - setSuggestions(matchedGeo); - } else { - setSuggestions([]); - } - }, [locations, selectedLocation, query]); - - const handleClickSearch = () => { - if (onClick) { - onClick(selectedLocation); - } else if (selectedLocation) { - const href = `${hrefProp}/${selectedLocation}`; - router.push(href); - } else if (query) { - router.push("/404"); - } - }; - - let iconComponent = SearchIcon; - let iconBorder; - if (variant === "explore") { - iconComponent = IconProp; - iconBorder = { - borderRadius: "50%", - border: "2px solid #fff", - }; - } - const searchIconButton = ( - - - - ); - - return ( - - ); -} - -DropdownSearch.propTypes = { - label: PropTypes.string, - href: PropTypes.string, - onClick: PropTypes.func, - icon: PropTypes.elementType, - locations: PropTypes.arrayOf(PropTypes.shape({})), - variant: PropTypes.string, - placeholder: PropTypes.string, -}; +import DropdownSearch from "./DownloadSearch"; export default DropdownSearch; diff --git a/apps/climatemappedafrica/src/components/DropdownSearch/index.stories.js b/apps/climatemappedafrica/src/components/DropdownSearch/index.stories.js deleted file mode 100644 index eabebcb54..000000000 --- a/apps/climatemappedafrica/src/components/DropdownSearch/index.stories.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from "react"; - -import DropdownSearch from "."; - -export default { - title: "Components/DropdownSearch", - argTypes: { - title: { - control: { - type: "string", - }, - }, - counties: { - control: { - type: "object", - }, - }, - }, -}; - -function Template({ ...args }) { - return ; -} - -export const Default = Template.bind({}); - -Default.args = { - label: "Search for Location", - counties: [ - { - name: "Nairobi", - code: 47, - }, - { - name: "Marsabit", - code: 10, - }, - { - name: "Meru", - code: 6, - }, - ], -}; diff --git a/apps/climatemappedafrica/src/components/ExplorePage/index.js b/apps/climatemappedafrica/src/components/ExplorePage/index.js index 051dcee4b..4296a53b3 100644 --- a/apps/climatemappedafrica/src/components/ExplorePage/index.js +++ b/apps/climatemappedafrica/src/components/ExplorePage/index.js @@ -11,17 +11,37 @@ import useStyles from "./useStyles"; import Panel from "@/climatemappedafrica/components/HURUmap/Panel"; -function initialState(profiles, onClick) { +function initialState( + profiles, + onClick, + explorePagePath, + initialLocationCode, + pinInitialLocation, +) { return { profiles: Array.isArray(profiles) ? profiles : [profiles], options: [ { color: "primary", onClick }, { color: "secondary", onClick }, ], + explorePagePath, + initialLocationCode, + pinInitialLocation, }; } -function ExplorePage({ panelProps, profile: profileProp, apiUri, ...props }) { +function ExplorePage({ + initialLocation, + explorePagePath, + panel: PanelProps = {}, + profile: profileProp, + ...props +}) { + const { + center, + name: initialLocationCode, + pinInitialLocation, + } = initialLocation; const theme = useTheme(); const classes = useStyles(props); // NOTE: This setState and the corresponding useEffect are "hacks" since at @@ -32,23 +52,43 @@ function ExplorePage({ panelProps, profile: profileProp, apiUri, ...props }) { setGeoCode(code); }; const [state, dispatch] = useExplore( - initialState(profileProp, handleClickTag), + initialState( + profileProp, + handleClickTag, + explorePagePath, + initialLocationCode, + pinInitialLocation, + ), ); useEffect(() => { dispatch({ type: "reset", - payload: initialState(profileProp, handleClickTag), + payload: initialState( + profileProp, + handleClickTag, + explorePagePath, + initialLocationCode, + pinInitialLocation, + ), }); - }, [dispatch, profileProp]); + }, [ + dispatch, + profileProp, + explorePagePath, + initialLocationCode, + pinInitialLocation, + ]); useEffect(() => { if (geoCode) { dispatch({ type: "fetch", payload: { code: geoCode } }); } }, [dispatch, geoCode]); + const router = useRouter(); const shouldFetch = () => (state.primary.shouldFetch && state.primary.code) || (state.secondary?.shouldFetch && state.secondary?.code); + const { data, error } = useProfileGeography(shouldFetch); useEffect(() => { if (data) { @@ -59,13 +99,23 @@ function ExplorePage({ panelProps, profile: profileProp, apiUri, ...props }) { } }, [dispatch, data]); + // Update URL when state.slug changes + useEffect(() => { + if (state.slug) { + const href = `/${explorePagePath}/${state.slug}`; + router.push(href, href, { shallow: true }); + } + // router shouldn't part of useEffect dependencies: https://nextjs.org/docs/api-reference/next/router#userouter + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.slug]); + const handleSelectLocation = (payload) => { const { code } = payload; const newPath = state.isPinning || state.isCompare ? `${state.primary.geography.code}-vs-${code}` : `${code}`; - const href = `/explore/${newPath.toLowerCase()}`; + const href = `/${explorePagePath}/${newPath.toLowerCase()}`; router.push(href, href, { shallow: true }); const type = state.isPinning && state.isCompare ? "compare" : "fetch"; dispatch({ type, payload }); @@ -86,14 +136,6 @@ function ExplorePage({ panelProps, profile: profileProp, apiUri, ...props }) { } dispatch({ type: "unpin", payload }); }; - useEffect(() => { - if (state.slug) { - const href = `/explore/${state.slug}`; - router.push(href, href, { shallow: true }); - } - // router shouldn't part of useEffect dependencies: https://nextjs.org/docs/api-reference/next/router#userouter - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.slug]); const isLoading = shouldFetch() && !(data || error); const { @@ -119,7 +161,7 @@ function ExplorePage({ panelProps, profile: profileProp, apiUri, ...props }) { >
); } ExplorePage.propTypes = { - apiUri: PropTypes.string, - panelProps: PropTypes.shape({}), + center: PropTypes.arrayOf(PropTypes.number), + initialLocation: PropTypes.shape({ + center: PropTypes.arrayOf(PropTypes.number), + name: PropTypes.string, + pinInitialLocation: PropTypes.bool, + }), + explorePagePath: PropTypes.string, + panel: PropTypes.shape({}), profile: PropTypes.oneOfType([ PropTypes.shape({ geography: PropTypes.shape({}), diff --git a/apps/climatemappedafrica/src/components/ExplorePage/useExplore.js b/apps/climatemappedafrica/src/components/ExplorePage/useExplore.js index f82569823..c621b93f5 100644 --- a/apps/climatemappedafrica/src/components/ExplorePage/useExplore.js +++ b/apps/climatemappedafrica/src/components/ExplorePage/useExplore.js @@ -2,7 +2,7 @@ import { useReducer } from "react"; import Link from "@/climatemappedafrica/components/Link"; -function extendProfileTags(profile, options) { +function extendProfileTags(profile, options, explorePagePath) { const { tags: originalTags, ...other } = profile || {}; if (!originalTags) { return profile; @@ -12,7 +12,7 @@ function extendProfileTags(profile, options) { ...otherTags, code, component: Link, - href: `/explore/${code.toLowerCase()}`, + href: `/${explorePagePath}/${code.toLowerCase()}`, shallow: true, underline: "none", ...options, @@ -20,19 +20,29 @@ function extendProfileTags(profile, options) { return { ...other, tags }; } -function initializer({ profiles, options }) { +function initializer({ + explorePagePath, + initialLocationCode, + profiles, + pinInitialLocation, + options, +}) { const [primary, secondary] = profiles; const [primaryOptions, secondaryOptions] = options; return { isPinning: false, isCompare: !!(primary && secondary), - primary: extendProfileTags(primary, primaryOptions), - secondary: extendProfileTags(secondary, secondaryOptions), + primary: extendProfileTags(primary, primaryOptions, explorePagePath), + secondary: extendProfileTags(secondary, secondaryOptions, explorePagePath), + explorePagePath, + initialLocationCode, + pinInitialLocation, }; } function reducer(state, action) { + const { explorePagePath, initialLocationCode, pinInitialLocation } = state; switch (action.type) { case "fetch": { const code = action.payload?.code; @@ -67,10 +77,14 @@ function reducer(state, action) { ); if (profileType) { const newState = { ...state }; - newState[profileType] = extendProfileTags(profile, { - ...others, - color: profileType, - }); + newState[profileType] = extendProfileTags( + profile, + { + ...others, + color: profileType, + }, + explorePagePath, + ); return newState; } } @@ -78,10 +92,10 @@ function reducer(state, action) { return state; } case "pin": - if (state.primary.geography.code.toLowerCase() !== "af") { + if (state.primary.geography.code.toLowerCase() !== initialLocationCode) { return { ...state, isPinning: true }; } - return { ...state, isPinning: false }; + return { ...state, isPinning: pinInitialLocation }; case "compare": { const code = action.payload?.code; if (code) { @@ -101,9 +115,13 @@ function reducer(state, action) { newState.secondary = undefined; } else if (state.primary?.geography?.code === code && state.secondary) { // NOTE: need to reset color from secondary back to primary as well - newState.primary = extendProfileTags(state.secondary, { - color: "primary", - }); + newState.primary = extendProfileTags( + state.secondary, + { + color: "primary", + }, + explorePagePath, + ); newState.secondary = undefined; } newState.secondary = undefined; diff --git a/apps/climatemappedafrica/src/components/HURUmap/LocationHeader/useStyles.js b/apps/climatemappedafrica/src/components/HURUmap/LocationHeader/useStyles.js index 344921df1..2f17071f0 100644 --- a/apps/climatemappedafrica/src/components/HURUmap/LocationHeader/useStyles.js +++ b/apps/climatemappedafrica/src/components/HURUmap/LocationHeader/useStyles.js @@ -1,9 +1,12 @@ import makeStyles from "@mui/styles/makeStyles"; -const useStyles = makeStyles(({ typography, palette }) => ({ +const useStyles = makeStyles(({ breakpoints, palette, typography }) => ({ root: { borderBottom: `solid 1px ${palette.divider}`, paddingTop: typography.pxToRem(20), + [breakpoints.up("lg")]: { + position: "relative", + }, }, titleContent: { display: "flex", @@ -33,6 +36,10 @@ const useStyles = makeStyles(({ typography, palette }) => ({ height: typography.pxToRem(44), minWidth: typography.pxToRem(44), boxShadow: "none", + [breakpoints.up("lg")]: { + // quick fix to ensure print button aligns with rich data/pin buttons + marginTop: "10px", + }, }, closeButton: { marginLeft: typography.pxToRem(20), diff --git a/apps/climatemappedafrica/src/components/HURUmap/Panel/DesktopPanel/PanelButtons.js b/apps/climatemappedafrica/src/components/HURUmap/Panel/DesktopPanel/PanelButtons.js index 697cf25ba..f2b21397c 100644 --- a/apps/climatemappedafrica/src/components/HURUmap/Panel/DesktopPanel/PanelButtons.js +++ b/apps/climatemappedafrica/src/components/HURUmap/Panel/DesktopPanel/PanelButtons.js @@ -139,7 +139,8 @@ function PanelButtons({ }), ({ widths }) => open && { - left: `max(calc((100vw - ${widths.values.lg}px)/2 + 833px),1100px)`, + // must match min width of TreeView + Profile + left: `max(calc((100vw - ${widths.values.lg}px)/2 + 79px + 800px),1100px)`, }, ]} /> diff --git a/apps/climatemappedafrica/src/components/HURUmap/Panel/MobilePanel/MobilePanel.js b/apps/climatemappedafrica/src/components/HURUmap/Panel/MobilePanel/MobilePanel.js index dc6587f6d..228435b0a 100644 --- a/apps/climatemappedafrica/src/components/HURUmap/Panel/MobilePanel/MobilePanel.js +++ b/apps/climatemappedafrica/src/components/HURUmap/Panel/MobilePanel/MobilePanel.js @@ -4,7 +4,7 @@ import React from "react"; import RichData from "./RichData"; -import PrintIcon from "@/climatemappedafrica/assets/icons/print.svg"; +import printIcon from "@/climatemappedafrica/assets/icons/print.svg?url"; import TopIcon from "@/climatemappedafrica/assets/icons/top.svg"; import LocationHeader from "@/climatemappedafrica/components/HURUmap/LocationHeader"; import PinAndCompare from "@/climatemappedafrica/components/HURUmap/PinAndCompare"; @@ -52,7 +52,7 @@ function MobilePanel({ activeType, scrollToTopLabel, sx, ...props }) {
diff --git a/apps/climatemappedafrica/src/components/HURUmap/Panel/Profile.js b/apps/climatemappedafrica/src/components/HURUmap/Panel/Profile.js index 5d21637e8..03defc9f0 100644 --- a/apps/climatemappedafrica/src/components/HURUmap/Panel/Profile.js +++ b/apps/climatemappedafrica/src/components/HURUmap/Panel/Profile.js @@ -4,7 +4,7 @@ import React, { forwardRef } from "react"; import ProfileItems from "./ProfileItems"; -import Print from "@/climatemappedafrica/assets/icons/print.svg"; +import printIcon from "@/climatemappedafrica/assets/icons/print.svg?url"; import LocationHeader from "@/climatemappedafrica/components/HURUmap/LocationHeader"; import PinAndCompare from "@/climatemappedafrica/components/HURUmap/PinAndCompare"; import Loading from "@/climatemappedafrica/components/Loading"; @@ -85,11 +85,15 @@ const Profile = forwardRef(function Profile( return ( ({ + ({ palette, typography, widths, zIndex }) => ({ backgroundColor: palette.background.default, + // must match min width of TreeView + left: { + lg: `max(calc((100vw - ${widths.values.lg}px)/2 + 79px), 300px)`, + }, marginLeft: { xs: typography.pxToRem(20), - lg: `max(calc((100vw - 1160px)/2 + 79px), 300px)`, + lg: 0, }, marginRight: { xs: typography.pxToRem(20), @@ -104,10 +108,11 @@ const Profile = forwardRef(function Profile( md: typography.pxToRem(80), lg: typography.pxToRem(17), }, - width: { lg: typography.pxToRem(800) }, - minHeight: { lg: "100%" }, paddingTop: { lg: typography.pxToRem(67.7) }, paddingRight: { lg: typography.pxToRem(17) }, + position: { lg: "absolute" }, + width: { lg: typography.pxToRem(800) }, + minHeight: { lg: "100%" }, zIndex: { lg: zIndex.drawer }, }), ...(Array.isArray(sx) ? sx : [sx]), @@ -117,7 +122,7 @@ const Profile = forwardRef(function Profile( {isLoading && } import("./Map"), { ssr: false }); function Hero({ + center, comment, title, subtitle, @@ -20,7 +21,6 @@ function Hero({ featuredLocations, searchPlaceholder, properties, - location: { center }, level, ...props }) { @@ -49,24 +49,16 @@ function Hero({
- renders unchanged 1`] = ` />
-
@@ -32,7 +29,7 @@ exports[` renders unchanged 1`] = ` class="MuiBox-root css-0" >

renders unchanged 1`] = `

diff --git a/apps/climatemappedafrica/src/components/Navigation/ExploreNavigation/index.js b/apps/climatemappedafrica/src/components/Navigation/ExploreNavigation/index.js index f74bde844..98ed5f461 100644 --- a/apps/climatemappedafrica/src/components/Navigation/ExploreNavigation/index.js +++ b/apps/climatemappedafrica/src/components/Navigation/ExploreNavigation/index.js @@ -41,7 +41,7 @@ const useStyles = makeStyles(({ palette, typography }) => ({ }, })); -function ExploreNavigation({ logo, variant }) { +function ExploreNavigation({ explorePagePath, logo, variant }) { const classes = useStyles(); const { setIsOpen } = useTour(); @@ -71,6 +71,7 @@ function ExploreNavigation({ logo, variant }) { > ; }); -function MobileNavigation({ drawerLogo, logo, menus, socialLinks, ...props }) { +function MobileNavigation({ + drawerLogo, + explorePagePath, + logo, + menus, + socialLinks, + ...props +}) { const classes = useStyles(props); const [open, setOpen] = useState(false); const router = useRouter(); @@ -277,6 +284,7 @@ function MobileNavigation({ drawerLogo, logo, menus, socialLinks, ...props }) { }} > locationCode); + const geoCodes = code + .split("-vs-") + .map((c) => c.trim()) + .filter((c) => c); + if (!geoCodes.every((gC) => locationCodes.includes(gC))) { + return { + notFound: true, + }; + } + + const [primaryCode, secondaryCode] = geoCodes; + const primaryProfile = await fetchProfileGeography(primaryCode); + const profile = [primaryProfile]; + if (secondaryCode) { + const secondaryProfile = await fetchProfileGeography(secondaryCode); + profile.push(secondaryProfile); + } + + // TODO: Move this to a PayloadCMS + const panel = { + panelItems: [ + { + value: "rich-data", + icon: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2021/11/Group-4505.svg", + iconProps: { + src: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2021/11/Group-4505.svg", + width: 44, + height: 44, + type: "svg", + blurDataURL: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGElEQVR4nGNgQAP/T///f/o/jHMWiQMHACIVCyeABSwfAAAAAElFTkSuQmCC", + placeholder: "blur", + }, + }, + { + value: "pin", + icon: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/01/Path-210-1-1.svg", + pin: true, + iconProps: { + src: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/01/Path-210-1-1.svg", + width: 44, + height: 44, + type: "svg", + blurDataURL: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAH0lEQVR4nGNgQAP/L/z/f/r//4P/wZzT//+fBbOQAQBvnQ3r6iVM4QAAAABJRU5ErkJggg==", + placeholder: "blur", + }, + }, + ], + scrollToTopLabel: "Back To Top", + dataNotAvailable: "— DATA NOT AVAILABLE", + lazyblock: { + slug: "lazyblock/panel", + }, + align: "", + anchor: "", + blockId: "20amuc", + blockUniqueClass: "lazyblock-panel-20amuc", + ghostkitSpacings: "", + ghostkitSR: "", + }; + const res = { + id: "explore-page", + blockType: "explore-page", + choropleth, + initialLocation, + explorePagePath: value.slug, + locations, + mapType, + panel, + profile, + variant: "explore", + preferredChildren, + }; + + return res; +} + +export default explorePage; diff --git a/apps/climatemappedafrica/src/lib/data/blockify/hero.js b/apps/climatemappedafrica/src/lib/data/blockify/hero.js index 69eaccaf0..f6c7cefbc 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/hero.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/hero.js @@ -3,10 +3,11 @@ import { fetchProfileGeography, } from "@/climatemappedafrica/lib/hurumap"; -export default async function hero(block) { - const { geometries } = await fetchProfileGeography( - block.location?.name?.toLowerCase(), - ); +export default async function hero({ block, hurumap }) { + const { + initialLocation: { center, name }, + } = hurumap; + const { geometries } = await fetchProfileGeography(name.toLowerCase()); const { level } = geometries.boundary?.properties ?? {}; const childLevelMaps = { continent: "country", @@ -24,6 +25,7 @@ export default async function hero(block) { const boundary = children[preferredLevel]; return { ...block, + center, slug: "hero", boundary, featuredLocations, diff --git a/apps/climatemappedafrica/src/lib/data/blockify/index.js b/apps/climatemappedafrica/src/lib/data/blockify/index.js index 3aed58b79..d8a83f9fe 100644 --- a/apps/climatemappedafrica/src/lib/data/blockify/index.js +++ b/apps/climatemappedafrica/src/lib/data/blockify/index.js @@ -1,20 +1,24 @@ +import explorePage from "./explore-page"; import hero from "./hero"; import pageHero from "./page-hero"; import team from "./team"; +import tutorial from "./tutorial"; const propsifyBlockBySlug = { + "explore-page": explorePage, hero, "page-hero": pageHero, team, + tutorial, }; -export const blockify = async (blocks, api, context) => { +export const blockify = async (blocks, api, context, hurumap) => { const promises = blocks?.map(async (block) => { const slug = block.blockType; const propsifyBlock = propsifyBlockBySlug[slug]; if (propsifyBlock) { - return propsifyBlock(block, api, context); + return propsifyBlock({ block, api, context, hurumap }); } 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 0a732a785..1be4c04a4 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 ccc51693e..74c51abf6 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 new file mode 100644 index 000000000..781934ccc --- /dev/null +++ b/apps/climatemappedafrica/src/lib/data/blockify/tutorial.js @@ -0,0 +1,88 @@ +async function tutorial() { + // TODO: Move this to Payload CMS + return { + blockType: "tutorial", + id: "tutorial", + items: [ + { + title: "SELECT LOCATION", + description: + "Select the County or Municipality you want to explore, by clicking on the search field and the dropdown menu.

Once you have made your selection, explore the visualisations, change location or pin to compare it to a second location.", + selector: "#location-search", + image: + "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-1.png", + imageProps: { + src: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-1.png", + width: 694, + height: 572, + type: "png", + blurDataURL: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMElEQVR4nGOoqW09f/7Shw8fzp07x8AuoC4qqdXf311YWMTAL6YVE5ve2tpcV18PAHqlETRE6fa/AAAAAElFTkSuQmCC", + placeholder: "blur", + }, + }, + { + description: + "Explore the map to confirm or change your selection. You can also pin your location if you want to compare two places.

Once a location is confirmed, click on the “Rich Data” button (on the left hand-side) to display the data visualisations.", + title: "EXPLORE THE MAP", + selector: "#none", + image: + "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-2.png", + imageProps: { + src: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-2.png", + width: 751, + height: 589, + type: "png", + blurDataURL: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMElEQVR4nGP49u3rxEnTDxw4sn//fobCggJeYVU+YaWCvByGzMxMPmElWwfPgvw8AH1vD9GRbZHGAAAAAElFTkSuQmCC", + placeholder: "blur", + }, + }, + { + title: "BROWSE THE CHARTS", + description: + "Continue to open the Rich Data dashboard, using the button on the left.

Browse the charts by scrolling the data dashboard. You can share and download the data using the buttons on the side of each chart.", + selector: "#rich-data", + image: + "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-3a.png", + imageProps: { + src: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-3a.png", + width: 670, + height: 439, + type: "png", + blurDataURL: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMklEQVR4nAEnANj/ANbW1s3NyVtbWrKyswDOztFdY38JCxe5ubcA3t7ffIKf4uPq////vgcX8ZIA2dgAAAAASUVORK5CYII=", + placeholder: "blur", + }, + }, + { + title: "PIN AND COMPARE", + description: + "There are two ways to pin and compare a second location:

1) From the data dashboard: look for the “pin” icon and select a second location from the dropdown menu.

2) From the map: pin your selected location by clicking on the ”pin” icon, then select a second location, which will appear in a different colour.", + selector: "#pin", + image: + "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-4.png", + imageProps: { + src: "https://cms.dev.codeforafrica.org/pesayetu/wp-content/uploads/sites/2/2022/04/PesaYetu-Tutorial-4.png", + width: 675, + height: 491, + type: "png", + blurDataURL: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMklEQVR4nAEnANj/APj491lWYA8QI+zs6wC/wMHXzMmLi5uztLYAu7u7w8XF39/eu7u7x7oYwYnBuWcAAAAASUVORK5CYII=", + placeholder: "blur", + }, + }, + ], + lazyblock: { + slug: "lazyblock/tutorial", + }, + align: "", + anchor: "", + blockId: "Z1npKaH", + blockUniqueClass: "lazyblock-tutorial-Z1npKaH", + ghostkitSpacings: "", + ghostkitSR: "", + }; +} + +export default tutorial; diff --git a/apps/climatemappedafrica/src/lib/data/common/index.js b/apps/climatemappedafrica/src/lib/data/common/index.js index e2d6d37fa..fb7918025 100644 --- a/apps/climatemappedafrica/src/lib/data/common/index.js +++ b/apps/climatemappedafrica/src/lib/data/common/index.js @@ -34,7 +34,7 @@ function getFooter(siteSettings, variant) { }; } -function getNavBar(siteSettings, variant) { +function getNavBar(siteSettings, variant, { slug }) { const { connect: { links = [] }, primaryNavigation: { menus = [], connect = [] }, @@ -47,6 +47,7 @@ function getNavBar(siteSettings, variant) { return { logo: imageFromMedia(title, primaryLogo.url), drawerLogo: imageFromMedia(title, drawerLogo.url), + explorePagePath: slug, menus, socialLinks, variant, @@ -74,12 +75,26 @@ export async function getPageProps(api, context) { page: { value: explorePage }, } = hurumap; - const blocks = await blockify(page.blocks, api, context); + let blocks = await blockify(page.blocks, api, context, hurumap); const variant = page.slug === explorePage.slug ? "explore" : "default"; const siteSettings = await api.findGlobal("settings-site"); const footer = getFooter(siteSettings, variant); - const menus = getNavBar(siteSettings, variant); + const menus = getNavBar(siteSettings, variant, explorePage); + + if (slug === explorePage.slug) { + // The explore page is a special case. The only block we need to render is map and tutorial. + const explorePageBlocks = [ + { + blockType: "explore-page", + slugs: slugs.slice(1), + }, + { + blockType: "tutorial", + }, + ]; + blocks = await blockify(explorePageBlocks, api, context, hurumap); + } return { blocks, diff --git a/apps/climatemappedafrica/src/pages/[[...slugs]].js b/apps/climatemappedafrica/src/pages/[[...slugs]].js index 5115ccff3..f000d92d5 100644 --- a/apps/climatemappedafrica/src/pages/[[...slugs]].js +++ b/apps/climatemappedafrica/src/pages/[[...slugs]].js @@ -1,3 +1,4 @@ +import { useRouter } from "next/router"; import { NextSeo } from "next-seo"; import React from "react"; import { SWRConfig } from "swr"; @@ -5,9 +6,11 @@ import { SWRConfig } from "swr"; import AboutTeam from "@/climatemappedafrica/components/AboutTeam"; import DataIndicators from "@/climatemappedafrica/components/DataIndicators"; import DataVisualisationGuide from "@/climatemappedafrica/components/DataVisualisationGuide"; +import ExplorePage from "@/climatemappedafrica/components/ExplorePage"; import Footer from "@/climatemappedafrica/components/Footer"; import Hero from "@/climatemappedafrica/components/Hero"; import HowItWorks from "@/climatemappedafrica/components/HowItWorks"; +import Tutorial from "@/climatemappedafrica/components/HURUmap/Tutorial"; import Navigation from "@/climatemappedafrica/components/Navigation"; import PageHero from "@/climatemappedafrica/components/PageHero"; import Summary from "@/climatemappedafrica/components/Summary"; @@ -16,6 +19,7 @@ import { getPageServerSideProps } from "@/climatemappedafrica/lib/data"; const componentsBySlugs = { "data-indicators": DataIndicators, "data-visualisation-guide": DataVisualisationGuide, + "explore-page": ExplorePage, hero: Hero, "how-it-works": HowItWorks, "page-hero": PageHero, @@ -24,6 +28,10 @@ const componentsBySlugs = { }; function Index({ blocks, menus, footer: footerProps, seo = {}, fallback }) { + const { + query: { showTutorial }, + } = useRouter(); + const pageSeo = {}; pageSeo.title = seo?.title || null; pageSeo.description = seo?.metaDesc || null; @@ -43,6 +51,17 @@ function Index({ blocks, menus, footer: footerProps, seo = {}, fallback }) { } } + let TutorialComponent = React.Fragment; + let TutorialComponentProps; + const tutorialBlock = blocks.find((block) => block.blockType === "tutorial"); + if (tutorialBlock) { + TutorialComponent = Tutorial; + TutorialComponentProps = { + ...tutorialBlock, + defaultOpen: Number.parseInt(showTutorial, 10) === 1, + }; + } + let PageConfig = React.Fragment; let pageConfigProps; if (fallback) { @@ -50,7 +69,7 @@ function Index({ blocks, menus, footer: footerProps, seo = {}, fallback }) { pageConfigProps = { value: { fallback } }; } return ( - <> +