From 7cb2d4f882680714ab7a4036e7c402bcb7ec6704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ra=C4=8D=C3=A1k?= Date: Mon, 2 Dec 2024 11:18:46 +0100 Subject: [PATCH] Add people-rows ArchieML component (#4235) * The new component allows us to have rows of people in two columns * Add socials to person block --- db/gdocTests.test.ts | 1 + db/model/Gdoc/GdocBase.ts | 1 + db/model/Gdoc/enrichedToMarkdown.ts | 5 ++ db/model/Gdoc/enrichedToRaw.ts | 19 +++- db/model/Gdoc/exampleEnrichedBlocks.ts | 10 ++- db/model/Gdoc/gdocUtils.ts | 1 + db/model/Gdoc/rawToArchie.ts | 17 ++++ db/model/Gdoc/rawToEnriched.ts | 90 +++++++++++-------- .../types/src/gdocTypes/ArchieMlComponents.ts | 20 ++++- packages/@ourworldindata/types/src/index.ts | 2 + packages/@ourworldindata/utils/src/Util.ts | 6 ++ site/gdocs/components/ArticleBlock.tsx | 17 +++- site/gdocs/components/Image.tsx | 1 + site/gdocs/components/People.scss | 11 ++- site/gdocs/components/People.tsx | 16 +++- site/gdocs/components/Person.scss | 26 +++++- site/gdocs/components/Person.tsx | 7 ++ site/gdocs/components/Socials.scss | 6 ++ site/gdocs/components/Socials.tsx | 24 ++--- site/gdocs/pages/AboutPage.scss | 3 +- site/gdocs/pages/AboutPage.tsx | 7 +- site/gdocs/pages/Author.scss | 2 - site/owid.scss | 1 + 23 files changed, 231 insertions(+), 62 deletions(-) create mode 100644 site/gdocs/components/Socials.scss diff --git a/db/gdocTests.test.ts b/db/gdocTests.test.ts index 4a96b58f8bd..7249a16c398 100644 --- a/db/gdocTests.test.ts +++ b/db/gdocTests.test.ts @@ -294,6 +294,7 @@ level: 2 url: "mailto:edouard@ourworldindata.org", text: "Edouard's email", type: SocialLinkType.Email, + parseErrors: [], }, ], parseErrors: [], diff --git a/db/model/Gdoc/GdocBase.ts b/db/model/Gdoc/GdocBase.ts index 03a5f6445cd..0efb3f349e6 100644 --- a/db/model/Gdoc/GdocBase.ts +++ b/db/model/Gdoc/GdocBase.ts @@ -555,6 +555,7 @@ export class GdocBase implements OwidGdocBaseInterface { "missing-data", "numbered-list", "people", + "people-rows", "person", "pull-quote", "sdg-grid", diff --git a/db/model/Gdoc/enrichedToMarkdown.ts b/db/model/Gdoc/enrichedToMarkdown.ts index 7cba71488f8..70e4c9085b0 100644 --- a/db/model/Gdoc/enrichedToMarkdown.ts +++ b/db/model/Gdoc/enrichedToMarkdown.ts @@ -162,6 +162,11 @@ ${items} .map((item) => enrichedBlockToMarkdown(item, exportComponents)) .join("\n") ) + .with({ type: "people-rows" }, (b): string | undefined => + b.people + .map((item) => enrichedBlockToMarkdown(item, exportComponents)) + .join("\n") + ) .with({ type: "person" }, (b): string | undefined => { const items = [ b.image && diff --git a/db/model/Gdoc/enrichedToRaw.ts b/db/model/Gdoc/enrichedToRaw.ts index 9a2ada59b0c..35d6ffe6027 100644 --- a/db/model/Gdoc/enrichedToRaw.ts +++ b/db/model/Gdoc/enrichedToRaw.ts @@ -45,6 +45,7 @@ import { RawBlockLatestDataInsights, RawBlockSocials, RawBlockPeople, + RawBlockPeopleRows, RawBlockPerson, } from "@ourworldindata/types" import { spanToHtmlString } from "./gdocUtils.js" @@ -191,6 +192,18 @@ export function enrichedBlockToRawBlock( value: b.items.map(enrichedBlockToRawBlock) as RawBlockPerson[], }) ) + .with( + { type: "people-rows" }, + (b): RawBlockPeopleRows => ({ + type: b.type, + value: { + columns: b.columns, + people: b.people.map( + enrichedBlockToRawBlock + ) as RawBlockPerson[], + }, + }) + ) .with( { type: "person" }, (b): RawBlockPerson => ({ @@ -549,7 +562,11 @@ export function enrichedBlockToRawBlock( .with({ type: "socials" }, (b): RawBlockSocials => { return { type: "socials", - value: b.links, + value: b.links.map(({ url, text, type }) => ({ + url, + text, + type, + })), } }) .exhaustive() diff --git a/db/model/Gdoc/exampleEnrichedBlocks.ts b/db/model/Gdoc/exampleEnrichedBlocks.ts index 84329f12c45..ccae879baee 100644 --- a/db/model/Gdoc/exampleEnrichedBlocks.ts +++ b/db/model/Gdoc/exampleEnrichedBlocks.ts @@ -190,6 +190,12 @@ export const enrichedBlockExamples: Record< items: [enrichedBlockPerson, enrichedBlockPerson], parseErrors: [], }, + "people-rows": { + type: "people-rows", + columns: "2", + people: [enrichedBlockPerson, enrichedBlockPerson], + parseErrors: [], + }, person: enrichedBlockPerson, "pull-quote": { type: "pull-quote", @@ -672,18 +678,20 @@ export const enrichedBlockExamples: Record< url: "https://twitter.com/OurWorldInData", text: "@OurWorldInData", type: SocialLinkType.X, + parseErrors: [], }, { url: "https://facebook.com/OurWorldInData", text: "OurWorldInData", type: SocialLinkType.Facebook, + parseErrors: [], }, { url: "https://ourworldindata.org", text: "ourworldindata.org", + parseErrors: [], }, ], - parseErrors: [], }, } diff --git a/db/model/Gdoc/gdocUtils.ts b/db/model/Gdoc/gdocUtils.ts index 39339fc5563..da5d2faa110 100644 --- a/db/model/Gdoc/gdocUtils.ts +++ b/db/model/Gdoc/gdocUtils.ts @@ -237,6 +237,7 @@ export function extractFilenamesFromBlock( "missing-data", "numbered-list", "people", + "people-rows", "pill-row", "pull-quote", "recirc", diff --git a/db/model/Gdoc/rawToArchie.ts b/db/model/Gdoc/rawToArchie.ts index d8ee8aa4a17..fee661107db 100644 --- a/db/model/Gdoc/rawToArchie.ts +++ b/db/model/Gdoc/rawToArchie.ts @@ -44,6 +44,7 @@ import { RawBlockLatestDataInsights, RawBlockSocials, RawBlockPeople, + RawBlockPeopleRows, RawBlockPerson, } from "@ourworldindata/types" import { isArray } from "@ourworldindata/utils" @@ -227,6 +228,21 @@ function* rawBlockPeopleToArchieMLString( yield "[]" } +function* rawBlockPeopleRowsToArchieMLString( + block: RawBlockPeopleRows +): Generator { + yield "{.people-rows}" + yield* propertyToArchieMLString("columns", block.value) + yield "[.+people]" + if (typeof block.value.people !== "string") { + for (const person of block.value.people) { + yield* OwidRawGdocBlockToArchieMLStringGenerator(person) + } + } + yield "[]" + yield "{}" +} + function* rawBlockPersonToArchieMLString( block: RawBlockPerson ): Generator { @@ -803,6 +819,7 @@ export function* OwidRawGdocBlockToArchieMLStringGenerator( .with({ type: "list" }, rawBlockListToArchieMLString) .with({ type: "numbered-list" }, rawBlockNumberedListToArchieMLString) .with({ type: "people" }, rawBlockPeopleToArchieMLString) + .with({ type: "people-rows" }, rawBlockPeopleRowsToArchieMLString) .with({ type: "person" }, rawBlockPersonToArchieMLString) .with({ type: "pull-quote" }, rawBlockPullQuoteToArchieMLString) .with( diff --git a/db/model/Gdoc/rawToEnriched.ts b/db/model/Gdoc/rawToEnriched.ts index e6e3eb0e3b0..0eaf2438c52 100644 --- a/db/model/Gdoc/rawToEnriched.ts +++ b/db/model/Gdoc/rawToEnriched.ts @@ -114,6 +114,7 @@ import { EnrichedBlockHomepageIntro, RawBlockHomepageIntro, EnrichedBlockHomepageIntroPost, + RawSocialLink, RawBlockSocials, EnrichedBlockSocials, EnrichedSocialLink, @@ -124,6 +125,8 @@ import { EnrichedBlockPeople, RawBlockPerson, EnrichedBlockPerson, + RawBlockPeopleRows, + EnrichedBlockPeopleRows, } from "@ourworldindata/types" import { traverseEnrichedSpan, @@ -171,6 +174,7 @@ export function parseRawBlocksToEnrichedBlocks( .with({ type: "list" }, parseList) .with({ type: "numbered-list" }, parseNumberedList) .with({ type: "people" }, parsePeople) + .with({ type: "people-rows" }, parsePeopleRows) .with({ type: "person" }, parsePerson) .with({ type: "pull-quote" }, parsePullQuote) .with( @@ -808,12 +812,23 @@ const parsePerson = (raw: RawBlockPerson): EnrichedBlockPerson => { return createError({ message: "Person must have a name" }) } + console.log("value", raw.value) return { type: "person", image: raw.value.image, name: raw.value.name, title: raw.value.title, text: raw.value.text.map(parseText), + socials: raw.value.socials?.map(parseSocialLink), + parseErrors: [], + } +} + +const parsePeopleRows = (raw: RawBlockPeopleRows): EnrichedBlockPeopleRows => { + return { + type: "people-rows", + columns: raw.value.columns, + people: raw.value.people.map(parsePerson), parseErrors: [], } } @@ -2310,6 +2325,44 @@ function parseHomepageIntro( } } +export const parseSocialLink = (raw: RawSocialLink): EnrichedSocialLink => { + const createError = (error: ParseError): EnrichedSocialLink => ({ + url: "", + text: "", + type: undefined, + parseErrors: [error], + }) + + const url = extractUrl(raw.url) + if (!url) { + return createError({ + message: "Link is missing a url", + }) + } + if (!raw.text) { + return createError({ + message: "Link is missing text", + }) + } + if (raw.type && Object.values(SocialLinkType).indexOf(raw.type) === -1) { + return createError({ + message: `Link type must be one of ${Object.values( + SocialLinkType + ).join(", ")}`, + }) + } + + return { + url: + raw.type === "email" && !url.startsWith("mailto:") + ? `mailto:${url}` + : url, + text: raw.text, + type: raw.type, + parseErrors: [], + } +} + export const parseSocials = (raw: RawBlockSocials): EnrichedBlockSocials => { const createError = (error: ParseError): EnrichedBlockSocials => ({ type: "socials", @@ -2328,44 +2381,9 @@ export const parseSocials = (raw: RawBlockSocials): EnrichedBlockSocials => { }) } - const links: EnrichedSocialLink[] = [] - - for (const link of raw.value) { - const url = extractUrl(link.url) - if (!url) { - return createError({ - message: "Link is missing a url", - }) - } - if (!link.text) { - return createError({ - message: "Link is missing text", - }) - } - if ( - link.type && - Object.values(SocialLinkType).indexOf(link.type) === -1 - ) { - return createError({ - message: `Link type must be one of ${Object.values( - SocialLinkType - ).join(", ")}`, - }) - } - - links.push({ - url: - link.type === "email" && !url.startsWith("mailto:") - ? `mailto:${url}` - : url, - text: link.text, - type: link.type, - }) - } - return { type: "socials", - links, + links: raw.value.map(parseSocialLink), parseErrors: [], } } diff --git a/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts b/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts index 7d839f78099..139c67400af 100644 --- a/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts +++ b/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts @@ -245,6 +245,14 @@ export type EnrichedBlockNumberedList = { items: EnrichedBlockText[] } & EnrichedBlockWithParseErrors +export type RawBlockPeopleRows = { + type: "people-rows" + value: { + columns: "2" + people: RawBlockPerson[] + } +} + export type RawBlockPeople = { type: "people" value: RawBlockPerson[] | ArchieMLUnexpectedNonObjectValue @@ -257,9 +265,16 @@ export type RawBlockPerson = { name: string title?: string text: RawBlockText[] + socials?: RawSocialLink[] } } +export type EnrichedBlockPeopleRows = { + type: "people-rows" + columns: "2" + people: EnrichedBlockPerson[] +} & EnrichedBlockWithParseErrors + export type EnrichedBlockPeople = { type: "people" items: EnrichedBlockPerson[] @@ -271,6 +286,7 @@ export type EnrichedBlockPerson = { name: string title?: string text: EnrichedBlockText[] + socials?: EnrichedSocialLink[] } & EnrichedBlockWithParseErrors export type RawBlockPullQuote = { @@ -897,7 +913,7 @@ export type EnrichedSocialLink = { text: string url: string type?: SocialLinkType -} +} & EnrichedBlockWithParseErrors export type EnrichedBlockSocials = { type: "socials" @@ -916,6 +932,7 @@ export type OwidRawGdocBlock = | RawBlockVideo | RawBlockList | RawBlockPeople + | RawBlockPeopleRows | RawBlockPerson | RawBlockPullQuote | RawBlockRecirc @@ -964,6 +981,7 @@ export type OwidEnrichedGdocBlock = | EnrichedBlockVideo | EnrichedBlockList | EnrichedBlockPeople + | EnrichedBlockPeopleRows | EnrichedBlockPerson | EnrichedBlockPullQuote | EnrichedBlockRecirc diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts index 39644a16320..65f15ca7d40 100644 --- a/packages/@ourworldindata/types/src/index.ts +++ b/packages/@ourworldindata/types/src/index.ts @@ -185,6 +185,7 @@ export { type RawBlockMissingData, type RawBlockNumberedList, type RawBlockPeople, + type RawBlockPeopleRows, type RawBlockPerson, type RawBlockPosition, type RawBlockProminentLink, @@ -240,6 +241,7 @@ export { type EnrichedBlockMissingData, type EnrichedBlockNumberedList, type EnrichedBlockPeople, + type EnrichedBlockPeopleRows, type EnrichedBlockPerson, type EnrichedBlockProminentLink, type EnrichedBlockPullQuote, diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index cd3ecbe0f18..3367f8673d9 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -1687,6 +1687,12 @@ export function traverseEnrichedBlock( traverseEnrichedBlock(item, callback, spanCallback) } }) + .with({ type: "people-rows" }, (peopleRows) => { + callback(peopleRows) + for (const person of peopleRows.people) { + traverseEnrichedBlock(person, callback, spanCallback) + } + }) .with({ type: "person" }, (person) => { callback(person) for (const node of person.text) { diff --git a/site/gdocs/components/ArticleBlock.tsx b/site/gdocs/components/ArticleBlock.tsx index d6719aeb68a..7bd7d8151bc 100644 --- a/site/gdocs/components/ArticleBlock.tsx +++ b/site/gdocs/components/ArticleBlock.tsx @@ -56,6 +56,7 @@ export type Container = | "summary" | "datapage" | "key-insight" + | "about-page" | "author-header" // Each container must have a default layout, usually just full-width @@ -116,6 +117,10 @@ const layouts: { [key in Container]: Layouts} = { ["default"]: "col-start-2 span-cols-6 col-lg-start-2 span-lg-cols-7 col-md-start-2 span-md-cols-10 col-sm-start-1 span-sm-cols-12", ["chart"]: "span-cols-8 span-lg-cols-9 span-md-cols-12", }, + ["about-page"]: { + ["default"]: "grid col-start-2 span-cols-12", + ["people"]: "col-start-2 span-cols-8 col-md-start-2 span-md-cols-12", + }, ["author-header"]: { ["default"]: "span-cols-8", ["image"]: "span-cols-2 span-md-cols-3", @@ -294,6 +299,16 @@ export default function ArticleBlock({ ))} )) + .with({ type: "people-rows" }, (block) => ( + + {block.people.map((block, index) => ( + + ))} + + )) .with({ type: "person" }, (block) => ) .with({ type: "pull-quote" }, (block) => ( ( )) .exhaustive() diff --git a/site/gdocs/components/Image.tsx b/site/gdocs/components/Image.tsx index 447f513dc44..a30a77dc477 100644 --- a/site/gdocs/components/Image.tsx +++ b/site/gdocs/components/Image.tsx @@ -55,6 +55,7 @@ const containerSizes: Record = { ["datapage"]: gridSpan6, ["full-width"]: "100vw", ["key-insight"]: gridSpan5, + ["about-page"]: gridSpan8, ["author-byline"]: "48px", ["author-header"]: gridSpan2, ["person"]: gridSpan2, diff --git a/site/gdocs/components/People.scss b/site/gdocs/components/People.scss index d9df395736b..627df4f0710 100644 --- a/site/gdocs/components/People.scss +++ b/site/gdocs/components/People.scss @@ -1,6 +1,9 @@ .people { - display: flex; - flex-direction: column; - gap: 24px; - max-width: 845px; + gap: var(--grid-gap); + + @include md-up { + .article-block__text { + @include body-3-medium; + } + } } diff --git a/site/gdocs/components/People.tsx b/site/gdocs/components/People.tsx index 9000d699ffe..7f984df0b36 100644 --- a/site/gdocs/components/People.tsx +++ b/site/gdocs/components/People.tsx @@ -3,10 +3,24 @@ import * as React from "react" export default function People({ className, + columns, children, }: { className?: string + columns?: "2" children: React.ReactNode }) { - return
{children}
+ return ( +
+ {children} +
+ ) } diff --git a/site/gdocs/components/Person.scss b/site/gdocs/components/Person.scss index 2d4f8af0771..6e9dedea7a2 100644 --- a/site/gdocs/components/Person.scss +++ b/site/gdocs/components/Person.scss @@ -1,6 +1,6 @@ .person { display: flex; - gap: 24px; + gap: var(--grid-gap); @include sm-only { flex-direction: column; @@ -30,6 +30,9 @@ } .person-header { + display: flex; + flex-direction: column; + gap: 2px; margin-bottom: 8px; } @@ -40,3 +43,24 @@ .person-title { color: $blue-60; } + +.person-socials { + margin-top: 16px; + margin-bottom: var(--grid-gap); + + ul { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + list-style-type: none; + list-style-position: inside; + } + + svg { + color: $blue-60; + } + + a { + @include owid-link-90; + } +} diff --git a/site/gdocs/components/Person.tsx b/site/gdocs/components/Person.tsx index ee879a937dc..e3c9f93dbc9 100644 --- a/site/gdocs/components/Person.tsx +++ b/site/gdocs/components/Person.tsx @@ -5,6 +5,7 @@ import { ArticleBlocks } from "./ArticleBlocks.js" import Image from "./Image.js" import { useMediaQuery } from "usehooks-ts" import { SMALL_BREAKPOINT_MEDIA_QUERY } from "../../SiteConstants.js" +import { Socials } from "./Socials.js" export default function Person({ person }: { person: EnrichedBlockPerson }) { const isSmallScreen = useMediaQuery(SMALL_BREAKPOINT_MEDIA_QUERY) @@ -34,6 +35,12 @@ export default function Person({ person }: { person: EnrichedBlockPerson }) {
{(!person.image || !isSmallScreen) && header} + {person.socials && ( + + )}
) diff --git a/site/gdocs/components/Socials.scss b/site/gdocs/components/Socials.scss new file mode 100644 index 00000000000..67befa02558 --- /dev/null +++ b/site/gdocs/components/Socials.scss @@ -0,0 +1,6 @@ +.social-link { + svg { + margin-right: 8px; + width: 1em; + } +} diff --git a/site/gdocs/components/Socials.tsx b/site/gdocs/components/Socials.tsx index e3e7b409d1c..039cbf8c042 100644 --- a/site/gdocs/components/Socials.tsx +++ b/site/gdocs/components/Socials.tsx @@ -1,8 +1,4 @@ -import { - EnrichedBlockSocials, - EnrichedSocialLink, - SocialLinkType, -} from "@ourworldindata/types" +import { EnrichedSocialLink, SocialLinkType } from "@ourworldindata/types" import React from "react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" import { @@ -33,21 +29,27 @@ function SocialLink({ url, text, type }: EnrichedSocialLink) { } return ( -
  • +
  • - + {text}
  • ) } -export function Socials(props: EnrichedBlockSocials & { className?: string }) { +export function Socials({ + className, + links, +}: { + className?: string + links: EnrichedSocialLink[] +}) { return ( -
    +
      - {props.links.map((link) => ( - + {links.map((link) => ( + ))}
    diff --git a/site/gdocs/pages/AboutPage.scss b/site/gdocs/pages/AboutPage.scss index 4dfb8fe079a..a1ca8e822a7 100644 --- a/site/gdocs/pages/AboutPage.scss +++ b/site/gdocs/pages/AboutPage.scss @@ -3,6 +3,7 @@ } .about-nav { + margin-bottom: 32px; border-bottom: 1px solid $blue-20; } @@ -44,7 +45,7 @@ } h2:first-of-type { - margin-top: 32px; + margin-top: 0; } hr { diff --git a/site/gdocs/pages/AboutPage.tsx b/site/gdocs/pages/AboutPage.tsx index a5f91ed4868..5380e0f3d96 100644 --- a/site/gdocs/pages/AboutPage.tsx +++ b/site/gdocs/pages/AboutPage.tsx @@ -20,8 +20,11 @@ export default function AboutPage({ content, slug }: OwidGdocAboutInterface) { About -
    - +
    +
    ) diff --git a/site/gdocs/pages/Author.scss b/site/gdocs/pages/Author.scss index 6efb1b05e86..c0df779f2eb 100644 --- a/site/gdocs/pages/Author.scss +++ b/site/gdocs/pages/Author.scss @@ -61,9 +61,7 @@ } } svg { - margin-right: 8px; color: $blue-50; - width: 1em; } } diff --git a/site/owid.scss b/site/owid.scss index 056589b9feb..3f2fd54aafa 100644 --- a/site/owid.scss +++ b/site/owid.scss @@ -107,6 +107,7 @@ @import "./gdocs/components/PillRow.scss"; @import "./gdocs/components/People.scss"; @import "./gdocs/components/Person.scss"; +@import "./gdocs/components/Socials.scss"; @import "./AboutThisData.scss"; @import "./DataInsightsNewsletterBanner.scss"; @import "./DataPage.scss";