From 0231d2cd78e16d759a0b71764fbc65b7f0fc72d0 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 10 Oct 2023 14:25:15 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=8E=89=20archie=20video=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/model/Gdoc/Gdoc.ts | 10 ++++ db/model/Gdoc/enrichedToRaw.ts | 13 ++++ db/model/Gdoc/exampleEnrichedBlocks.ts | 8 +++ db/model/Gdoc/rawToArchie.ts | 13 ++++ db/model/Gdoc/rawToEnriched.ts | 60 +++++++++++++++++++ packages/@ourworldindata/utils/src/Util.ts | 1 + packages/@ourworldindata/utils/src/index.ts | 2 + .../@ourworldindata/utils/src/owidTypes.ts | 20 +++++++ site/gdocs/ArticleBlock.tsx | 19 ++++-- site/gdocs/Video.tsx | 33 ++++++++++ site/gdocs/centered-article.scss | 10 ++++ 11 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 site/gdocs/Video.tsx diff --git a/db/model/Gdoc/Gdoc.ts b/db/model/Gdoc/Gdoc.ts index 92c075c1ae0..0ec188aff64 100644 --- a/db/model/Gdoc/Gdoc.ts +++ b/db/model/Gdoc/Gdoc.ts @@ -595,6 +595,16 @@ export class Gdoc extends BaseEntity implements OwidGdocInterface { ) } ) + .with({ type: "video" }, (video) => { + return [ + Link.createFromUrl({ + url: video.url, + source: this, + componentType: video.type, + text: spansToSimpleString(video.caption || []), + }), + ] + }) .with( { // no urls directly on any of these components diff --git a/db/model/Gdoc/enrichedToRaw.ts b/db/model/Gdoc/enrichedToRaw.ts index 5874eee862c..0ea822ada83 100644 --- a/db/model/Gdoc/enrichedToRaw.ts +++ b/db/model/Gdoc/enrichedToRaw.ts @@ -33,6 +33,7 @@ import { RawBlockResearchAndWritingLink, RawBlockAlign, RawBlockEntrySummary, + RawBlockVideo, } from "@ourworldindata/utils" import { spanToHtmlString } from "./gdocUtils.js" import { match, P } from "ts-pattern" @@ -148,6 +149,18 @@ export function enrichedBlockToRawBlock( }, }) ) + .with( + { type: "video" }, + (b): RawBlockVideo => ({ + type: b.type, + value: { + url: b.url, + filename: b.filename, + caption: b.caption ? spansToHtmlText(b.caption) : undefined, + shouldLoop: String(b.shouldLoop), + }, + }) + ) .with( { type: "list" }, (b): RawBlockList => ({ diff --git a/db/model/Gdoc/exampleEnrichedBlocks.ts b/db/model/Gdoc/exampleEnrichedBlocks.ts index 78c58d70e1c..594333ac721 100644 --- a/db/model/Gdoc/exampleEnrichedBlocks.ts +++ b/db/model/Gdoc/exampleEnrichedBlocks.ts @@ -154,6 +154,14 @@ export const enrichedBlockExamples: Record< size: BlockImageSize.Wide, parseErrors: [], }, + video: { + type: "video", + url: "https://ourworldindata.org/assets/videos/example.mp4", + filename: "https://ourworldindata.org/assets/images/example-poster.mp4", + caption: boldLinkExampleText, + shouldLoop: true, + parseErrors: [], + }, list: { type: "list", items: [enrichedBlockText], diff --git a/db/model/Gdoc/rawToArchie.ts b/db/model/Gdoc/rawToArchie.ts index 59a943423dc..59d901d76f4 100644 --- a/db/model/Gdoc/rawToArchie.ts +++ b/db/model/Gdoc/rawToArchie.ts @@ -9,6 +9,7 @@ import { RawBlockHorizontalRule, RawBlockHtml, RawBlockImage, + RawBlockVideo, RawBlockList, RawBlockNumberedList, RawBlockPosition, @@ -164,6 +165,17 @@ function* rawBlockImageToArchieMLString( yield "{}" } +function* rawBlockVideoToArchieMLString( + block: RawBlockVideo +): Generator { + yield "{.video}" + yield* propertyToArchieMLString("url", block.value) + yield* propertyToArchieMLString("filename", block.value) + yield* propertyToArchieMLString("shouldLoop", block.value) + yield* propertyToArchieMLString("caption", block.value) + yield "{}" +} + function* listToArchieMLString( items: string[] | string, blockName: string @@ -566,6 +578,7 @@ export function* OwidRawGdocBlockToArchieMLStringGenerator( .with({ type: "callout" }, rawBlockCalloutToArchieMLString) .with({ type: "chart-story" }, rawBlockChartStoryToArchieMLString) .with({ type: "image" }, rawBlockImageToArchieMLString) + .with({ type: "video" }, rawBlockVideoToArchieMLString) .with({ type: "list" }, rawBlockListToArchieMLString) .with({ type: "numbered-list" }, rawBlockNumberedListToArchieMLString) .with({ type: "pull-quote" }, rawBlockPullQuoteToArchieMLString) diff --git a/db/model/Gdoc/rawToEnriched.ts b/db/model/Gdoc/rawToEnriched.ts index 74912f0c06d..2c78e707a14 100644 --- a/db/model/Gdoc/rawToEnriched.ts +++ b/db/model/Gdoc/rawToEnriched.ts @@ -13,6 +13,7 @@ import { EnrichedBlockHorizontalRule, EnrichedBlockHtml, EnrichedBlockImage, + EnrichedBlockVideo, EnrichedBlockKeyInsights, EnrichedBlockList, EnrichedBlockNumberedList, @@ -47,6 +48,7 @@ import { RawBlockHeading, RawBlockHtml, RawBlockImage, + RawBlockVideo, RawBlockKeyInsights, RawBlockList, RawBlockNumberedList, @@ -125,6 +127,7 @@ export function parseRawBlocksToEnrichedBlocks( .with({ type: "scroller" }, parseScroller) .with({ type: "chart-story" }, parseChartStory) .with({ type: "image" }, parseImage) + .with({ type: "video" }, parseVideo) .with({ type: "list" }, parseList) .with({ type: "numbered-list" }, parseNumberedList) .with({ type: "pull-quote" }, parsePullQuote) @@ -548,6 +551,63 @@ const parseImage = (image: RawBlockImage): EnrichedBlockImage => { } } +const parseVideo = (raw: RawBlockVideo): EnrichedBlockVideo => { + const createError = ( + error: ParseError, + url: string = "", + filename: string = "", + shouldLoop: boolean = false, + caption?: Span[] + ): EnrichedBlockVideo => ({ + type: "video", + url, + filename, + shouldLoop, + caption, + parseErrors: [error], + }) + + const url = raw.value.url + if (!url) { + return createError({ + message: "url property is missing or empty", + }) + } + + if (!url.endsWith(".mp4")) { + return createError({ + message: "video must have a .mp4 file extension", + }) + } + + const filename = raw.value.filename + if (!filename) { + return createError({ + message: "filename property is missing or empty", + }) + } + + const shouldLoop = raw.value.shouldLoop + if (!!shouldLoop && shouldLoop !== "true" && shouldLoop !== "false") { + return createError({ + message: "If specified, shouldLoop property must be true or false", + }) + } + + const caption = raw.value.caption + ? htmlToSpans(raw.value.caption) + : undefined + + return { + type: "video", + url, + filename, + caption, + shouldLoop: shouldLoop === "true", + parseErrors: [], + } +} + const parseNumberedList = ( raw: RawBlockNumberedList ): EnrichedBlockNumberedList => { diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 64c34647b4d..a6e596534a4 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -1610,6 +1610,7 @@ export function traverseEnrichedBlocks( "horizontal-rule", "html", "image", + "video", "missing-data", "prominent-link", "pull-quote", diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index 54bae698c47..f9811fb19e9 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -47,6 +47,7 @@ export { type EnrichedBlockHorizontalRule, type EnrichedBlockHtml, type EnrichedBlockImage, + type EnrichedBlockVideo, type EnrichedBlockKeyInsights, type EnrichedBlockKeyInsightsSlide, type EnrichedBlockList, @@ -132,6 +133,7 @@ export { type RawBlockHorizontalRule, type RawBlockHtml, type RawBlockImage, + type RawBlockVideo, type RawBlockKeyInsights, type RawBlockList, type RawBlockMissingData, diff --git a/packages/@ourworldindata/utils/src/owidTypes.ts b/packages/@ourworldindata/utils/src/owidTypes.ts index ad58cbce635..400deadf005 100644 --- a/packages/@ourworldindata/utils/src/owidTypes.ts +++ b/packages/@ourworldindata/utils/src/owidTypes.ts @@ -651,6 +651,24 @@ export type EnrichedBlockImage = { size: BlockImageSize } & EnrichedBlockWithParseErrors +export type RawBlockVideo = { + type: "video" + value: { + url?: string + caption?: string + shouldLoop?: string + filename?: string + } +} + +export type EnrichedBlockVideo = { + type: "video" + url: string + shouldLoop: boolean + filename: string + caption?: Span[] +} & EnrichedBlockWithParseErrors + // TODO: This is what lists staring with * are converted to in archieToEnriched // It might also be what is used inside recirc elements but there it's not a simple // string IIRC - check this @@ -1107,6 +1125,7 @@ export type OwidRawGdocBlock = | RawBlockScroller | RawBlockChartStory | RawBlockImage + | RawBlockVideo | RawBlockList | RawBlockPullQuote | RawBlockRecirc @@ -1142,6 +1161,7 @@ export type OwidEnrichedGdocBlock = | EnrichedBlockScroller | EnrichedBlockChartStory | EnrichedBlockImage + | EnrichedBlockVideo | EnrichedBlockList | EnrichedBlockPullQuote | EnrichedBlockRecirc diff --git a/site/gdocs/ArticleBlock.tsx b/site/gdocs/ArticleBlock.tsx index 1c658427d8d..1ff60cba899 100644 --- a/site/gdocs/ArticleBlock.tsx +++ b/site/gdocs/ArticleBlock.tsx @@ -32,6 +32,7 @@ import { TopicPageIntro } from "./TopicPageIntro.js" import { KeyInsights } from "./KeyInsights.js" import { ResearchAndWriting } from "./ResearchAndWriting.js" import { AllCharts } from "./AllCharts.js" +import Video from "./Video.js" export type Container = | "default" @@ -57,15 +58,15 @@ const layouts: { [key in Container]: Layouts} = { ["aside-right"]: "col-start-11 span-cols-3 span-md-cols-10 col-md-start-3", ["chart-story"]: "col-start-4 span-cols-8 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", ["chart"]: "col-start-4 span-cols-8 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", - ["explorer"]: "col-start-2 span-cols-12 span-md-cols-12 col-md-start-2", ["default"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", ["divider"]: "col-start-2 span-cols-12", + ["explorer"]: "col-start-2 span-cols-12 span-md-cols-12 col-md-start-2", ["gray-section"]: "span-cols-14 grid grid-cols-12-full-width", ["heading"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", ["horizontal-rule"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", ["html"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", - ["image--narrow"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 col-sm-start-2 span-sm-cols-12 ", - ["image--wide"]: "col-start-4 span-cols-8 col-md-start-2 span-md-cols-12 col-sm-start-2 span-sm-cols-12 ", + ["image--narrow"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 col-sm-start-2 span-sm-cols-12", + ["image--wide"]: "col-start-4 span-cols-8 col-md-start-2 span-md-cols-12", ["image-caption"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", ["key-insights"]: "col-start-2 span-cols-12", ["list"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", @@ -76,7 +77,6 @@ const layouts: { [key in Container]: Layouts} = { ["research-and-writing"]: "col-start-2 span-cols-12", ["scroller"]: "grid span-cols-12 col-start-2", ["sdg-grid"]: "grid col-start-2 span-cols-12 col-lg-start-3 span-lg-cols-10 span-sm-cols-12 col-sm-start-2", - ["toc"]: "grid grid-cols-8 col-start-4 span-cols-8 grid-md-cols-10 col-md-start-3 span-md-cols-10 grid-sm-cols-12 span-sm-cols-12 col-sm-start-2", ["side-by-side"]: "grid span-cols-12 col-start-2", ["sticky-left-left-column"]: "grid grid-cols-7 span-cols-7 span-md-cols-10 grid-md-cols-10", ["sticky-left-right-column"]: "grid grid-cols-5 span-cols-5 span-md-cols-10 grid-md-cols-10", @@ -85,7 +85,9 @@ const layouts: { [key in Container]: Layouts} = { ["sticky-right-right-column"]: "span-cols-7 grid-cols-7 span-md-cols-10 grid-md-cols-10 col-md-start-2 span-sm-cols-12 grid-sm-cols-12 col-sm-start-1", ["sticky-right"]: "grid span-cols-12 col-start-2", ["text"]: "col-start-5 span-cols-6 col-md-start-3 span-md-cols-10 span-sm-cols-12 col-sm-start-2", + ["toc"]: "grid grid-cols-8 col-start-4 span-cols-8 grid-md-cols-10 col-md-start-3 span-md-cols-10 grid-sm-cols-12 span-sm-cols-12 col-sm-start-2", ["topic-page-intro"]: "grid col-start-2 span-cols-12", + ["video"]: "col-start-4 span-cols-8 col-md-start-2 span-md-cols-12", }, ["datapage"]: { ["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", @@ -233,6 +235,15 @@ export default function ArticleBlock({ ) : null} )) + .with({ type: "video" }, (block) => ( +