From cf2c0f90aa149f9f4fddce63e040fdcd73e83c91 Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Thu, 2 Feb 2023 16:34:02 -0800 Subject: [PATCH] Implement basic share images (#30) --- .storybook/preview.tsx | 13 ++-- package.json | 3 +- plopfile.mjs | 66 ++++++++++++++++++- src/components/AppBar/AppBar.tsx | 2 +- .../LocationOverview/LocationOverview.tsx | 2 +- .../ShareImages/ShareImage.styles.ts | 13 ++++ .../ShareImageHomepage.stories.tsx | 17 +++++ .../ShareImages/ShareImageHomepage.tsx | 31 +++++++++ .../ShareImageLocation.stories.tsx | 18 +++++ .../ShareImages/ShareImageLocation.tsx | 31 +++++++++ src/components/ShareImages/index.ts | 2 + src/components/Trends/Trends.tsx | 6 +- src/pages/internal/share-image/homepage.tsx | 34 ++++++++++ src/pages/internal/share-image/location.tsx | 54 +++++++++++++++ src/screens/Homepage/Homepage.tsx | 4 +- src/screens/Location/Location.tsx | 4 +- src/utils/metrics.ts | 6 +- 17 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 src/components/ShareImages/ShareImage.styles.ts create mode 100644 src/components/ShareImages/ShareImageHomepage.stories.tsx create mode 100644 src/components/ShareImages/ShareImageHomepage.tsx create mode 100644 src/components/ShareImages/ShareImageLocation.stories.tsx create mode 100644 src/components/ShareImages/ShareImageLocation.tsx create mode 100644 src/components/ShareImages/index.ts create mode 100644 src/pages/internal/share-image/homepage.tsx create mode 100644 src/pages/internal/share-image/location.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 2d2cd92..75ed81d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,14 +3,19 @@ import React from "react"; import CssBaseline from "@mui/material/CssBaseline"; import { ThemeProvider } from "@mui/material/styles"; +import { MetricCatalogProvider } from "@actnowcoalition/actnow.js"; + import theme from "../src/styles/theme"; +import { metricCatalog } from "../src/utils/metrics"; // Wraps stories with the MUI Theme provider const themeDecorator = (Story) => ( - - - - + + + + + + ); export const decorators = [themeDecorator]; diff --git a/package.json b/package.json index e9f507b..07b057d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "repl": "yarn run-script scripts/repl/repl.ts", "hello": "yarn run-script scripts/hello/index.ts", "generate-data-snapshot": "yarn run-script scripts/data/generate-snapshot.ts", - "create-share-page": "plop share-page" + "create-share-page": "plop share-page", + "create-component": "plop component" }, "lint-staged": { "*.{ts,tsx}": "eslint --max-warnings=0", diff --git a/plopfile.mjs b/plopfile.mjs index cf28b96..4daaec8 100644 --- a/plopfile.mjs +++ b/plopfile.mjs @@ -5,6 +5,41 @@ */ import _ from "lodash"; +const templateComponentMain = prepareTemplate(` +import React from "react"; + +export interface {{pascalCase name}}Props { + +} + +export const {{pascalCase name}} = (props: {{pascalCase name}}Props) => { + return <>{{pascalCase name}}; +};`); + +const templateComponentStories = prepareTemplate(` +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import { {{pascalCase name}} } from "."; + +export default { + title: "Components/{{pascalCase name}}", + component: {{pascalCase name}}, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + <{{pascalCase name}} {...args} /> +); + +export const Example = Template.bind({}); +Example.args = {}; +`); + +const templateComponentIndex = prepareTemplate(` +export * from "./{{pascalCase name}}"; +`); + +const componentBasePath = "src/components"; + const templateSharePage = prepareTemplate(` import { useRef, useState } from "react"; @@ -59,7 +94,7 @@ const templateSharePageListItem = prepareTemplate(` $1`); -export default function (/** @type {import('plop').NodePlopAPI} */ plop) { +function plopConfig(/** @type {import('plop').NodePlopAPI} */ plop) { const internalPagePath = "src/pages/internal"; plop.setGenerator("share-page", { description: @@ -92,8 +127,37 @@ export default function (/** @type {import('plop').NodePlopAPI} */ plop) { }, ], }); + plop.setGenerator("component", { + description: "Creates a component module with stories, styles and index.", + prompts: [ + { + type: "input", + name: "name", + message: "Component name", + }, + ], + actions: [ + { + type: "add", + path: `${componentBasePath}/{{pascalCase name}}/index.ts`, + template: templateComponentIndex, + }, + { + type: "add", + path: `${componentBasePath}/{{pascalCase name}}/{{pascalCase name}}.tsx`, + template: templateComponentMain, + }, + { + type: "add", + path: `${componentBasePath}/{{pascalCase name}}/{{pascalCase name}}.stories.tsx`, + template: templateComponentStories, + }, + ], + }); } +export default plopConfig; + function prepareTemplate(srcTemplate) { return _.trimStart(srcTemplate); } diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index ccc2e13..b83d768 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -25,7 +25,7 @@ import { import { RegionSearch } from "@actnowcoalition/actnow.js"; -import { regions } from "src/utils/regions"; +import { regions } from "../../utils/regions"; interface AppBarItem { href: string; diff --git a/src/components/LocationOverview/LocationOverview.tsx b/src/components/LocationOverview/LocationOverview.tsx index 4a7bc5a..8db7f87 100644 --- a/src/components/LocationOverview/LocationOverview.tsx +++ b/src/components/LocationOverview/LocationOverview.tsx @@ -17,7 +17,7 @@ const LocationOverview = ({ region }: LocationOverviewProps) => { return ( - +
    diff --git a/src/components/ShareImages/ShareImage.styles.ts b/src/components/ShareImages/ShareImage.styles.ts new file mode 100644 index 0000000..d1369d7 --- /dev/null +++ b/src/components/ShareImages/ShareImage.styles.ts @@ -0,0 +1,13 @@ +import { Box } from "@mui/material"; + +import { styled } from "../../styles"; + +export const ratio = 16 / 9; +export const width = 900; +export const height = width / ratio; + +export const ShareImageContainer = styled(Box)` + padding: ${({ theme }) => theme.spacing(4, 5)}; + width: ${width}px; + height: ${height}px; +`; diff --git a/src/components/ShareImages/ShareImageHomepage.stories.tsx b/src/components/ShareImages/ShareImageHomepage.stories.tsx new file mode 100644 index 0000000..f39a2fa --- /dev/null +++ b/src/components/ShareImages/ShareImageHomepage.stories.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import { ComponentMeta, ComponentStory } from "@storybook/react"; + +import { ShareImageHomepage } from "."; + +export default { + title: "Components/ShareImageHomepage", + component: ShareImageHomepage, +} as ComponentMeta; + +const Template: ComponentStory = () => ( + +); + +export const Home = Template.bind({}); +Home.args = {}; diff --git a/src/components/ShareImages/ShareImageHomepage.tsx b/src/components/ShareImages/ShareImageHomepage.tsx new file mode 100644 index 0000000..134b910 --- /dev/null +++ b/src/components/ShareImages/ShareImageHomepage.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { Box, Stack, Typography } from "@mui/material"; + +import { MetricWorldMap } from "@actnowcoalition/actnow.js"; + +import { MetricId } from "../../utils/metrics"; +import { regions } from "../../utils/regions"; +import { ShareImageContainer } from "./ShareImage.styles"; + +export const ShareImageHomepage = () => ( + + + + World Happiness Report 2022 + + theme.palette.common.white, + borderRadius: 2, + }} + > + + + + By Act Now Coalition + + + +); diff --git a/src/components/ShareImages/ShareImageLocation.stories.tsx b/src/components/ShareImages/ShareImageLocation.stories.tsx new file mode 100644 index 0000000..dfe9622 --- /dev/null +++ b/src/components/ShareImages/ShareImageLocation.stories.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import { ComponentMeta, ComponentStory } from "@storybook/react"; + +import { ShareImageLocation } from "."; +import { regions } from "../../utils/regions"; + +export default { + title: "Components/ShareImageLocation", + component: ShareImageLocation, +} as ComponentMeta; + +const Template: ComponentStory = () => ( + +); + +export const Home = Template.bind({}); +Home.args = {}; diff --git a/src/components/ShareImages/ShareImageLocation.tsx b/src/components/ShareImages/ShareImageLocation.tsx new file mode 100644 index 0000000..31643b9 --- /dev/null +++ b/src/components/ShareImages/ShareImageLocation.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { Box, Stack, Typography } from "@mui/material"; + +import { MetricWorldMap, Region } from "@actnowcoalition/actnow.js"; + +import { MetricId } from "../../utils/metrics"; +import { regions } from "../../utils/regions"; +import { ShareImageContainer } from "./ShareImage.styles"; + +export const ShareImageLocation = ({ region }: { region: Region }) => ( + + + + {`Happiness in ${region.shortName}`} + + theme.palette.common.white, + borderRadius: 2, + }} + > + + + + By Act Now Coalition + + + +); diff --git a/src/components/ShareImages/index.ts b/src/components/ShareImages/index.ts new file mode 100644 index 0000000..969b3fc --- /dev/null +++ b/src/components/ShareImages/index.ts @@ -0,0 +1,2 @@ +export * from "./ShareImageHomepage"; +export * from "./ShareImageLocation"; diff --git a/src/components/Trends/Trends.tsx b/src/components/Trends/Trends.tsx index da466ed..b1d308e 100644 --- a/src/components/Trends/Trends.tsx +++ b/src/components/Trends/Trends.tsx @@ -29,7 +29,7 @@ export const Trends = () => { ); const { error, data } = useDataForRegionsAndMetrics(regions.all, [ - MetricId.LIFE_LADDER, + MetricId.HAPPINESS, ]); if (error || !data) { @@ -38,7 +38,7 @@ export const Trends = () => { // Sort countries by happiness score, low to high const countriesByHappiness = sortBy(regions.all, (region) => { - const d = data.metricData(region, MetricId.LIFE_LADDER); + const d = data.metricData(region, MetricId.HAPPINESS); return d.currentValue as number; }); @@ -86,7 +86,7 @@ export const Trends = () => { metrics={ALL_METRICS} regions={regions.all} timePeriods={timePeriods} - initialMetric={MetricId.LIFE_LADDER} + initialMetric={MetricId.HAPPINESS} initialRegions={option.countries} height={600} width={0} diff --git a/src/pages/internal/share-image/homepage.tsx b/src/pages/internal/share-image/homepage.tsx new file mode 100644 index 0000000..a1c4bea --- /dev/null +++ b/src/pages/internal/share-image/homepage.tsx @@ -0,0 +1,34 @@ +import { useRef, useState } from "react"; + +import { Box } from "@mui/material"; +import { NextPage } from "next"; + +import { useMutationObserver } from "@actnowcoalition/actnow.js"; + +import { ScreenshotWrapper } from "components/Containers"; +import { ShareImageHomepage } from "components/ShareImages"; +import { searchDomForClass } from "src/utils/share-pages"; + +// http://localhost:3000/internal/share-image/homepage +const HomeSharePage: NextPage = () => { + const ref = useRef(null); + + const [isLoaded, setIsLoaded] = useState(false); + const handleMutations: MutationCallback = (mutations: MutationRecord[]) => { + for (const mutation of mutations) { + if (mutation.type === "childList") { + searchDomForClass(mutation.target as Element, setIsLoaded); + } + } + }; + useMutationObserver(ref, handleMutations, { childList: true, subtree: true }); + + return ( + + + + + + ); +}; +export default HomeSharePage; diff --git a/src/pages/internal/share-image/location.tsx b/src/pages/internal/share-image/location.tsx new file mode 100644 index 0000000..c117db8 --- /dev/null +++ b/src/pages/internal/share-image/location.tsx @@ -0,0 +1,54 @@ +import { useRef, useState } from "react"; + +import { Box } from "@mui/material"; +import isEmpty from "lodash/isEmpty"; +import { NextPage } from "next"; +import { useRouter } from "next/router"; + +import { + Region, + assert, + useMutationObserver, +} from "@actnowcoalition/actnow.js"; + +import { regions } from "../../../utils/regions"; +import { ScreenshotWrapper } from "components/Containers"; +import { ShareImageLocation } from "components/ShareImages"; +import { searchDomForClass } from "src/utils/share-pages"; + +// http://localhost:3000/internal/share-image/location?regionId=CAN +const LocationSharePage: NextPage = () => { + const router = useRouter(); + const ref = useRef(null); + + const [isLoaded, setIsLoaded] = useState(false); + const handleMutations: MutationCallback = (mutations: MutationRecord[]) => { + for (const mutation of mutations) { + if (mutation.type === "childList") { + searchDomForClass(mutation.target as Element, setIsLoaded); + } + } + }; + useMutationObserver(ref, handleMutations, { childList: true, subtree: true }); + + if (isEmpty(router.query)) { + return ( + + Page loading or no query params were provided. Expects params: regionId + + ); + } + + const { regionId } = router.query; + const region = regions.findByRegionIdStrict(regionId as string); + assert(region instanceof Region, `Region with ID ${regionId} not found`); + + return ( + + + + + + ); +}; +export default LocationSharePage; diff --git a/src/screens/Homepage/Homepage.tsx b/src/screens/Homepage/Homepage.tsx index b4b3344..eec3c6d 100644 --- a/src/screens/Homepage/Homepage.tsx +++ b/src/screens/Homepage/Homepage.tsx @@ -80,14 +80,14 @@ const Homepage: React.FC<{ page: Page }> = ({ page }) => { }} > - + diff --git a/src/screens/Location/Location.tsx b/src/screens/Location/Location.tsx index c99629d..140cae5 100644 --- a/src/screens/Location/Location.tsx +++ b/src/screens/Location/Location.tsx @@ -103,7 +103,7 @@ export const Location: React.FC<{ region: Region; page: Page }> = ({ @@ -121,7 +121,7 @@ export const Location: React.FC<{ region: Region; page: Page }> = ({