diff --git a/baker/SiteBaker.tsx b/baker/SiteBaker.tsx index 8704abf9fb5..4f9c51798c2 100644 --- a/baker/SiteBaker.tsx +++ b/baker/SiteBaker.tsx @@ -105,6 +105,7 @@ import { import { getBakePath } from "@ourworldindata/components" import { GdocAuthor, getMinimalAuthors } from "../db/model/Gdoc/GdocAuthor.js" import { DATA_INSIGHTS_ATOM_FEED_NAME } from "../site/gdocs/utils.js" +import { getRedirectsFromDb } from "../db/model/Redirect.js" type PrefetchedAttachments = { linkedAuthors: DbEnrichedAuthor[] @@ -509,14 +510,18 @@ export class SiteBaker { private async bakePosts(knex: db.KnexReadonlyTransaction) { if (!this.bakeSteps.has("wordpressPosts")) return - // TODO: the knex instance should be handed down as a parameter const alreadyPublishedViaGdocsSlugsSet = await db.getSlugsWithPublishedGdocsSuccessors(knex) + const redirects = await getRedirectsFromDb(knex) const postsApi = await getPostsFromSnapshots( knex, undefined, - (postrow) => !alreadyPublishedViaGdocsSlugsSet.has(postrow.slug) + (postrow) => + // Exclude posts that are already published via GDocs + !alreadyPublishedViaGdocsSlugsSet.has(postrow.slug) && + // Exclude posts that are redirect sources + !redirects.some((row) => row.source.slice(1) === postrow.slug) ) await pMap( diff --git a/baker/formatWordpressPost.tsx b/baker/formatWordpressPost.tsx index 1f251cb07e8..5dd782df98b 100644 --- a/baker/formatWordpressPost.tsx +++ b/baker/formatWordpressPost.tsx @@ -4,19 +4,16 @@ import React from "react" import ReactDOMServer from "react-dom/server.js" import { HTTPS_ONLY } from "../settings/serverSettings.js" import { GrapherExports } from "../baker/GrapherBakingUtils.js" -import { AllCharts, renderAllCharts } from "../site/blocks/AllCharts.js" import { FormattingOptions } from "@ourworldindata/types" import { FormattedPost, FullPost, TocHeading, - WP_BlockType, parseKeyValueArgs, } from "@ourworldindata/utils" import { Footnote } from "../site/Footnote.js" import { LoadingIndicator } from "@ourworldindata/grapher" import { PROMINENT_LINK_CLASSNAME } from "../site/blocks/ProminentLink.js" -import { countryProfileSpecs } from "../site/countryProfileProjects.js" import { DataToken } from "../site/DataToken.js" import { DEEP_LINK_CLASS, formatImages } from "./formatting.js" import { replaceIframesWithExplorerRedirectsInWordPressPost } from "./replaceExplorerRedirects.js" @@ -27,16 +24,10 @@ import { } from "../site/blocks/AdditionalInformation.js" import { renderHelp } from "../site/blocks/Help.js" import { renderCodeSnippets } from "@ourworldindata/components" -import { renderExpandableParagraphs } from "../site/blocks/ExpandableParagraph.js" -import { - formatUrls, - getBodyHtml, - SUMMARY_CLASSNAME, -} from "../site/formatting.js" +import { formatUrls, getBodyHtml } from "../site/formatting.js" import { GRAPHER_PREVIEW_CLASS } from "../site/SiteConstants.js" import { INTERACTIVE_ICON_SVG } from "../site/InteractionNotice.js" -import { renderKeyInsights, renderProminentLinks } from "./siteRenderers.js" -import { KEY_INSIGHTS_CLASS_NAME } from "../site/blocks/KeyInsights.js" +import { renderProminentLinks } from "./siteRenderers.js" import { RELATED_CHARTS_CLASS_NAME } from "../site/blocks/RelatedCharts.js" import { KnexReadonlyTransaction } from "../db/db.js" @@ -48,15 +39,6 @@ export const formatWordpressPost = async ( ): Promise => { let html = post.content - // Inject key insights early so they can be formatted by the embedding - // article. Another option would be to format the content independently, - // which would allow for inclusion further down the formatting pipeline. - // This is however creating issues by running non-idempotent formatting - // functions twice on the same content (e.g. table processing double wraps - // in "tableContainer" divs). On the other hand, rendering key insights last - // would require special care for footnotes. - html = await renderKeyInsights(html, post.id) - // Standardize urls html = formatUrls(html) @@ -111,42 +93,11 @@ export const formatWordpressPost = async ( const cheerioEl = cheerio.load(html) - // Related charts - if ( - !countryProfileSpecs.some( - (spec) => post.slug === spec.landingPageSlug - ) && - post.relatedCharts?.length && - // Render fallback "All charts" block at the top of entries only if - // manual "All charts" block not present in the rest of the document. - // This is to help transitioning towards topic pages, where this block - // is manually added in the content. In that case, we don't want to - // inject it at the top too. - !cheerioEl(`block[type='${WP_BlockType.AllCharts}']`).length - ) { - // Mimicking SSR output of additional information block from PHP - const allCharts = ` - - - ${ReactDOMServer.renderToStaticMarkup()} - - - ` - const $summary = cheerioEl(`.${SUMMARY_CLASSNAME}`) - if ($summary.length !== 0) { - $summary.after(allCharts) - } else { - cheerioEl("body > h2:first-of-type, body > h3:first-of-type") - .first() - .before(allCharts) - } - } - // SSR rendering of Gutenberg blocks, before hydration on client // // - Note: any post-processing on these blocks runs the risk of hydration // discrepancies. E.g. the ToC post-processing further below add an "id" - // attribute to elibigle heading tags. In an unbridled version of that + // attribute to eligible heading tags. In an unbridled version of that // script, the AdditionalInformation block title (h3) would be altered and // receive an "id" attribute (

). When this block is // then hydrated on the client, the "id" attribute is missing, since it @@ -155,10 +106,8 @@ export const formatWordpressPost = async ( // perspective, the server rendered version is different from the client // one, hence the discrepancy. renderAdditionalInformation(cheerioEl) - renderExpandableParagraphs(cheerioEl) renderCodeSnippets(cheerioEl) renderHelp(cheerioEl) - renderAllCharts(cheerioEl, post) await renderProminentLinks(cheerioEl, post.id, knex) // Extract inline styling @@ -407,27 +356,12 @@ export const formatWordpressPost = async ( $heading.attr("id", slug) - if ($heading.closest(`.${KEY_INSIGHTS_CLASS_NAME}`).length) return - $heading.append(``) }) - // Extracting the useful information from the HTML - const stickyNavLinks: { text: string; target: string }[] = [] - const $stickyNavContents = cheerioEl(".sticky-nav-contents") - const $stickyNavLinks = $stickyNavContents.children().children() - $stickyNavLinks.each((_, element) => { - const $elem = cheerioEl(element) - const text = $elem.text() - const target = $elem.attr("href") - if (text && target) stickyNavLinks.push({ text, target }) - }) - $stickyNavContents.remove() - return { ...post, supertitle, - stickyNavLinks, lastUpdated, byline, info, diff --git a/baker/siteRenderers.tsx b/baker/siteRenderers.tsx index 734a355c615..4ba95f88428 100644 --- a/baker/siteRenderers.tsx +++ b/baker/siteRenderers.tsx @@ -41,7 +41,6 @@ import { FormattedPost, FullPost, JsonError, - KeyInsight, Url, IndexPost, mergePartialGrapherConfigs, @@ -63,12 +62,7 @@ import { } from "../db/db.js" import { getPageOverrides, isPageOverridesCitable } from "./pageOverrides.js" import { ProminentLink } from "../site/blocks/ProminentLink.js" -import { - KeyInsightsThumbs, - KeyInsightsSlides, - KEY_INSIGHTS_CLASS_NAME, -} from "../site/blocks/KeyInsights.js" -import { formatUrls, KEY_INSIGHTS_H2_CLASSNAME } from "../site/formatting.js" +import { formatUrls } from "../site/formatting.js" import { GrapherProgrammaticInterface } from "@ourworldindata/grapher" import { ExplorerProgram } from "../explorer/ExplorerProgram.js" @@ -832,97 +826,3 @@ const renderExplorerDefaultThumbnail = (): string => { ) } - -export const renderKeyInsights = async ( - html: string, - containerPostId: number -): Promise => { - const $ = cheerio.load(html) - - for (const block of Array.from($("block[type='key-insights']"))) { - const $block = $(block) - // only selecting and <slug> from direct children, to not match - // titles and slugs from individual key insights slides. - const title = $block.find("> title").text() - const slug = $block.find("> slug").text() - if (!title || !slug) { - void logErrorAndMaybeSendToBugsnag( - new JsonError( - `Title or anchor missing for key insights block, content removed in wordpress post with id ${containerPostId}.` - ) - ) - $block.remove() - continue - } - - const keyInsights = extractKeyInsights($, $block, containerPostId) - if (!keyInsights.length) { - void logErrorAndMaybeSendToBugsnag( - new JsonError( - `No valid key insights found within block, content removed in wordpress post with id ${containerPostId}` - ) - ) - $block.remove() - continue - } - const titles = keyInsights.map((keyInsight) => keyInsight.title) - - const rendered = ReactDOMServer.renderToString( - <> - <h2 id={slug} className={KEY_INSIGHTS_H2_CLASSNAME}> - {title} - </h2> - <div className={`${KEY_INSIGHTS_CLASS_NAME}`}> - <div className="block-wrapper"> - <KeyInsightsThumbs titles={titles} /> - </div> - <KeyInsightsSlides insights={keyInsights} /> - </div> - </> - ) - - $block.replaceWith(rendered) - } - return $.html() -} - -export const extractKeyInsights = ( - $: CheerioStatic, - $wrapper: Cheerio, - containerPostId: number -): KeyInsight[] => { - const keyInsights: KeyInsight[] = [] - - for (const block of Array.from( - $wrapper.find("block[type='key-insight']") - )) { - const $block = $(block) - // restrictive children selector not strictly necessary here for now but - // kept for consistency and evolutions of the block. In the future, key - // insights could host other blocks with <title> tags - const $title = $block.find("> title") - const title = $title.text() - const isTitleHidden = $title.attr("is-hidden") === "1" - const slug = $block.find("> slug").text() - const content = $block.find("> content").html() - - // "!content" is taken literally here. An empty paragraph will return - // "\n\n<p></p>\n\n" and will not trigger an error. This can be seen - // both as an unexpected behaviour or a feature, depending on the stage - // of work (published or WIP). - if (!title || !slug || !content) { - void logErrorAndMaybeSendToBugsnag( - new JsonError( - `Missing title, slug or content for key insight ${ - title || slug - }, content removed in wordpress post with id ${containerPostId}.` - ) - ) - continue - } - - keyInsights.push({ title, isTitleHidden, content, slug }) - } - - return keyInsights -} diff --git a/db/db.ts b/db/db.ts index 0d5a620e2b6..27185b2e62b 100644 --- a/db/db.ts +++ b/db/db.ts @@ -159,8 +159,8 @@ export const knexRawInsert = async ( /** * In the backporting workflow, the users create gdoc posts for posts. As long as these are not yet published, * we still want to bake them from the WP posts. Once the users presses publish there though, we want to stop - * baking them from the wordpress post. This funciton fetches all the slugs of posts that have been published via gdocs, - * to help us exclude them from the baking process. + * baking them from the wordpress post. This function fetches all the slugs of posts that have been published via gdocs, + * to help us exclude them from the baking process. This query used to rely on the gdocSuccessorId field but that fell out of sync. */ export const getSlugsWithPublishedGdocsSuccessors = async ( knex: KnexReadonlyTransaction @@ -168,12 +168,16 @@ export const getSlugsWithPublishedGdocsSuccessors = async ( return knexRaw( knex, `-- sql - SELECT - slug - FROM - posts_with_gdoc_publish_status - WHERE - isGdocPublished = TRUE` + SELECT + p.slug + FROM + posts p + LEFT JOIN posts_gdocs g on + p.slug = g.slug + WHERE + p.status = "publish" + AND g.published = TRUE + ` ).then((rows) => new Set(rows.map((row: any) => row.slug))) } diff --git a/packages/@ourworldindata/types/src/domainTypes/Site.ts b/packages/@ourworldindata/types/src/domainTypes/Site.ts index ad8e8e097cd..646f66b7c8a 100644 --- a/packages/@ourworldindata/types/src/domainTypes/Site.ts +++ b/packages/@ourworldindata/types/src/domainTypes/Site.ts @@ -6,10 +6,3 @@ export interface BreadcrumbItem { export interface KeyValueProps { [key: string]: string | boolean | undefined } - -export interface KeyInsight { - title: string - isTitleHidden?: boolean - content: string - slug: string -} diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts index c8fb7940465..e070b627e50 100644 --- a/packages/@ourworldindata/types/src/index.ts +++ b/packages/@ourworldindata/types/src/index.ts @@ -20,11 +20,7 @@ export { type UserCountryInformation, type QueryParams, } from "./domainTypes/Various.js" -export { - type BreadcrumbItem, - type KeyInsight, - type KeyValueProps, -} from "./domainTypes/Site.js" +export { type BreadcrumbItem, type KeyValueProps } from "./domainTypes/Site.js" export { type FormattedPost, type IndexPost, @@ -129,7 +125,6 @@ export { } from "./domainTypes/ContentGraph.js" export { WP_BlockClass, - WP_BlockType, WP_ColumnStyle, WP_PostType, type PostRestApi, diff --git a/packages/@ourworldindata/types/src/wordpressTypes/WordpressTypes.ts b/packages/@ourworldindata/types/src/wordpressTypes/WordpressTypes.ts index 0d87c93630d..f13db574032 100644 --- a/packages/@ourworldindata/types/src/wordpressTypes/WordpressTypes.ts +++ b/packages/@ourworldindata/types/src/wordpressTypes/WordpressTypes.ts @@ -71,10 +71,6 @@ export enum WP_BlockClass { FullContentWidth = "wp-block-full-content-width", // not an actual WP block yet } -export enum WP_BlockType { - AllCharts = "all-charts", -} - export interface FormattingOptions { toc?: boolean hideAuthors?: boolean diff --git a/site/LongFormPage.tsx b/site/LongFormPage.tsx index 40fe66203c3..12d33986b77 100644 --- a/site/LongFormPage.tsx +++ b/site/LongFormPage.tsx @@ -422,7 +422,6 @@ export const LongFormPage = (props: { pageTitle, // hideSubheadings: true })}) - runRelatedCharts(${JSON.stringify(post.relatedCharts)}) `, }} /> diff --git a/site/blocks/AllCharts.tsx b/site/blocks/AllCharts.tsx deleted file mode 100644 index 584dd5d08c6..00000000000 --- a/site/blocks/AllCharts.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react" -import ReactDOMServer from "react-dom/server.js" -import { FullPost, WP_BlockClass, WP_BlockType } from "@ourworldindata/utils" -import { RelatedCharts } from "./RelatedCharts.js" - -export const AllCharts = ({ post }: { post: FullPost }) => { - if (!post.relatedCharts?.length) return null - return ( - <> - <h3>Interactive charts on {post.title}</h3> - <div className={WP_BlockClass.FullContentWidth}> - <RelatedCharts - showKeyChartsOnly={true} - charts={post.relatedCharts} - /> - </div> - </> - ) -} - -export const renderAllCharts = (cheerioEl: CheerioStatic, post: FullPost) => - cheerioEl(`block[type='${WP_BlockType.AllCharts}']`).each(function ( - this: CheerioElement - ) { - const $block = cheerioEl(this) - - const rendered = ReactDOMServer.renderToStaticMarkup( - <AllCharts post={post} /> - ) - $block.after(rendered) - $block.remove() - }) diff --git a/site/blocks/ExpandableParagraph.tsx b/site/blocks/ExpandableParagraph.tsx index 0ed303a4d81..46ed47be1e7 100644 --- a/site/blocks/ExpandableParagraph.tsx +++ b/site/blocks/ExpandableParagraph.tsx @@ -1,7 +1,5 @@ import React, { CSSProperties, useRef, useState } from "react" import cx from "classnames" -import ReactDOM from "react-dom" -import ReactDOMServer from "react-dom/server.js" export const ExpandableParagraph = ( props: @@ -91,41 +89,3 @@ export const ExpandableParagraph = ( </div> ) } - -export const hydrateExpandableParagraphs = () => { - const expandableParagraphs = document.querySelectorAll( - ".expandable-paragraph" - ) - - expandableParagraphs.forEach((eP) => { - const innerHTML = eP.innerHTML - ReactDOM.hydrate( - <ExpandableParagraph - dangerouslySetInnerHTML={{ __html: innerHTML }} - buttonVariant="slim" - />, - eP.parentElement - ) - }) -} - -export const renderExpandableParagraphs = ($: CheerioStatic) => { - const expandableParagraphs = $('block[type="expandable-paragraph"]') - expandableParagraphs.each((_, eP) => { - const $el = $(eP) - const $dry = $( - ReactDOMServer.renderToStaticMarkup( - <div> - <ExpandableParagraph - dangerouslySetInnerHTML={{ - __html: $el.html() || "", - }} - buttonVariant="slim" - /> - </div> - ) - ) - $el.after($dry) - $el.remove() - }) -} diff --git a/site/blocks/KeyInsights.tsx b/site/blocks/KeyInsights.tsx deleted file mode 100644 index 74e36931ac3..00000000000 --- a/site/blocks/KeyInsights.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import React, { useCallback, useContext, useEffect, useState } from "react" -import ReactDOM from "react-dom" -import { ScrollMenu, VisibilityContext } from "react-horizontal-scrolling-menu" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" -import { faAngleRight } from "@fortawesome/free-solid-svg-icons" -import { KeyInsight, getWindowUrl, setWindowUrl } from "@ourworldindata/utils" - -export const KEY_INSIGHTS_CLASS_NAME = "wp-block-owid-key-insights" -export const KEY_INSIGHTS_INSIGHT_PARAM = "insight" -export const KEY_INSIGHTS_THUMBS_CLASS_NAME = "thumbs" -export const KEY_INSIGHTS_THUMB_CLASS_NAME = "thumb" -export const KEY_INSIGHTS_SLIDES_CLASS_NAME = "slides" -export const KEY_INSIGHTS_SLIDE_CLASS_NAME = "slide" -export const KEY_INSIGHTS_SLIDE_CONTENT_CLASS_NAME = "content" - -type scrollVisibilityApiType = React.ContextType<typeof VisibilityContext> - -export enum ArrowDirection { - prev = "prev", - next = "next", -} - -const Thumb = ({ - title, - onClick, - selected, -}: { - title: string - onClick: () => void - itemId: string // needed by react-horizontal-scrolling-menu, see lib's examples - selected: boolean -}) => { - return ( - <button - aria-label={`Go to slide: ${title}`} - onClick={onClick} - role="tab" - aria-selected={selected} - className={KEY_INSIGHTS_THUMB_CLASS_NAME} - > - {title} - </button> - ) -} - -/** - * Tab-based switcher for key insights - * - * NB: this component has only received limited efforts towards accessibility. - * - * A next possible step would be to managage focus via arrow keys (see - * https://w3c.github.io/aria/#managingfocus). A good implementation of - * accessibility practices for this kind of widget is available at - * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role - */ -export const KeyInsightsThumbs = ({ titles }: { titles: string[] }) => { - const [selectedId, setSelectedId] = useState<string>("0") - const [slides, setSlides] = useState<HTMLElement | null>(null) - const [slug, setSlug] = useState<string>("") - const apiRef = React.useRef({} as scrollVisibilityApiType) - - // Not using useRef() here so that the "select slide based on hash" effect, - // running on page load only, runs after the ref has been attached (and not - // on first render, which would be before) - // https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node - const thumbsRef = useCallback((node) => { - if (node !== null) { - const keyInsightsNode = node.parentElement?.parentElement - - const tempSlides = keyInsightsNode?.querySelector( - `.${KEY_INSIGHTS_SLIDES_CLASS_NAME}` - ) - setSlides(tempSlides) - // get slug from previous <h3> - setSlug(keyInsightsNode?.previousElementSibling?.getAttribute("id")) - } - }, []) - - // Select active slide based on URL - useEffect(() => { - if (!slides) return - - const windowUrl = getWindowUrl() - if (!windowUrl.queryParams.insight) return - - // find the slide containing the h4 with the id matching the ?insight query param - const selectedSlideIdx = Array.from( - slides.querySelectorAll(`.${KEY_INSIGHTS_SLIDE_CLASS_NAME}`) - ).findIndex((slide) => - slide.querySelector( - `#${windowUrl.queryParams[KEY_INSIGHTS_INSIGHT_PARAM]}` - ) - ) - - if (selectedSlideIdx === -1) return - setSelectedId(selectedSlideIdx.toString()) - }, [slides]) - - // Scroll to selected item - useEffect(() => { - const item = apiRef.current.getItemById(selectedId) - if (!item) return - - apiRef.current.scrollToItem(item, "smooth", "center", "nearest") - }, [selectedId]) - - // Select active slide when corresponding thumb selected - useEffect(() => { - if (!slides) return - - // A less imperative, more React way to do this would be preferred. To - // switch between slides, I aimed to keep their content untouched - // (including event listeners hydrated by other components), while only - // updating their wrappers. Managing the switching logic through React - // would have required hydrating KeyInsightsSlides as well as all - // possible content components within them - even though they would have - // been already hydrated at the page level. From that perspective, the - // gain is not clear, and the approach not necessarily cleaner, so I - // stuck with the imperative approach. - - slides - .querySelectorAll(`.${KEY_INSIGHTS_SLIDE_CLASS_NAME}`) - .forEach((slide, idx) => { - if (idx === Number(selectedId)) { - slide.setAttribute("data-active", "true") - const windowUrl = getWindowUrl() - const anchor = slide.querySelector("h4")?.getAttribute("id") - if (!anchor) return - setWindowUrl( - windowUrl - .updateQueryParams({ - [KEY_INSIGHTS_INSIGHT_PARAM]: anchor, - }) - // When a key insight slug is changed, links - // pointing to that key insight soft-break and take - // readers to the top of the page. Adding an anchor - // pointing to the the block title (h3) serves as a - // stopgap, taking readers to the key insights block - // instead but without selecting a particular - // insight. - // - // This also improves the UX of readers coming - // through shared insights URL. e.g. - // /key-insights-demo?insight=insight-1#key-insights - // shows the whole insights block, including its - // titles (the target of the anchor). - .update({ hash: `#${slug}` }) - ) - } else { - slide.setAttribute("data-active", "false") - } - }) - // see https://github.com/owid/owid-grapher/pull/1435#discussion_r888058198 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedId]) - - return ( - <div - className={KEY_INSIGHTS_THUMBS_CLASS_NAME} - role="tablist" - ref={thumbsRef} - > - <ScrollMenu - LeftArrow={LeftArrow} - RightArrow={RightArrow} - transitionDuration={200} - apiRef={apiRef} - > - {titles.map((title, i) => { - const itemId = `${i}` - return ( - <Thumb - title={title} - key={itemId} - itemId={itemId} - onClick={() => { - setSelectedId(itemId) - }} - selected={itemId === selectedId} - /> - ) - })} - </ScrollMenu> - </div> - ) -} - -export const KeyInsightsSlides = ({ insights }: { insights: KeyInsight[] }) => ( - <div className={KEY_INSIGHTS_SLIDES_CLASS_NAME}> - {insights.map(({ title, isTitleHidden, slug, content }, idx) => ( - <div - key={idx} - className={KEY_INSIGHTS_SLIDE_CLASS_NAME} - data-active={idx === 0} - role="tabpanel" - tabIndex={0} - > - <h4 style={isTitleHidden ? { display: "none" } : {}} id={slug}> - {title} - </h4> - <div - className={KEY_INSIGHTS_SLIDE_CONTENT_CLASS_NAME} - dangerouslySetInnerHTML={{ __html: content }} - /> - </div> - ))} - </div> -) - -const Arrow = ({ - children, - disabled, - onClick, - className, - direction, -}: { - children: React.ReactNode - disabled: boolean - onClick: VoidFunction - className?: string - direction: ArrowDirection -}) => { - const classes = ["arrow", className] - return ( - <button - aria-label={`Scroll to ${ - direction === ArrowDirection.next ? "next" : "previous" - } slide`} - disabled={disabled} - onClick={onClick} - className={classes.join(" ")} - > - {children} - </button> - ) -} - -const LeftArrow = () => { - const { - isFirstItemVisible, - scrollPrev, - visibleItemsWithoutSeparators, - initComplete, - } = useContext(VisibilityContext) - - const [disabled, setDisabled] = useState( - !initComplete || (initComplete && isFirstItemVisible) - ) - useEffect(() => { - // NOTE: detect if whole component visible - if (visibleItemsWithoutSeparators.length) { - setDisabled(isFirstItemVisible) - } - }, [isFirstItemVisible, visibleItemsWithoutSeparators]) - - return !disabled ? ( - <Arrow - disabled={false} - onClick={() => scrollPrev()} - className="left" - direction={ArrowDirection.prev} - > - <FontAwesomeIcon icon={faAngleRight} flip="horizontal" /> - </Arrow> - ) : null -} - -const RightArrow = () => { - const { isLastItemVisible, scrollNext, visibleItemsWithoutSeparators } = - useContext(VisibilityContext) - - const [disabled, setDisabled] = useState( - !visibleItemsWithoutSeparators.length && isLastItemVisible - ) - useEffect(() => { - if (visibleItemsWithoutSeparators.length) { - setDisabled(isLastItemVisible) - } - }, [isLastItemVisible, visibleItemsWithoutSeparators]) - - return !disabled ? ( - <Arrow - disabled={false} - onClick={() => scrollNext()} - className="right" - direction={ArrowDirection.next} - > - <FontAwesomeIcon icon={faAngleRight} /> - </Arrow> - ) : null -} - -export const hydrateKeyInsights = () => { - document - .querySelectorAll<HTMLElement>( - `.${KEY_INSIGHTS_CLASS_NAME} .${KEY_INSIGHTS_THUMBS_CLASS_NAME}` - ) - .forEach((block) => { - const titles = Array.from( - block.querySelectorAll(`.${KEY_INSIGHTS_THUMB_CLASS_NAME}`) - ).map((thumb) => thumb.textContent || "") - if (!titles.length) return - - const blockWrapper = block.parentElement - ReactDOM.hydrate( - <KeyInsightsThumbs titles={titles} />, - blockWrapper - ) - }) -} diff --git a/site/blocks/RelatedCharts.tsx b/site/blocks/RelatedCharts.tsx index bc0222c5982..d0a70b22ca8 100644 --- a/site/blocks/RelatedCharts.tsx +++ b/site/blocks/RelatedCharts.tsx @@ -1,5 +1,4 @@ import React, { useState, useRef } from "react" -import ReactDOM from "react-dom" import { orderBy, RelatedChart } from "@ourworldindata/utils" import { useEmbedChart } from "../hooks.js" import { GalleryArrow, GalleryArrowDirection } from "./GalleryArrow.js" @@ -126,16 +125,3 @@ export const RelatedCharts = ({ return charts.length === 1 ? singleChartView : multipleChartsView } - -export const runRelatedCharts = (charts: RelatedChart[]) => { - const relatedChartsEl = document.querySelector<HTMLElement>( - `.${RELATED_CHARTS_CLASS_NAME}` - ) - if (relatedChartsEl) { - const relatedChartsWrapper = relatedChartsEl.parentElement - ReactDOM.hydrate( - <RelatedCharts showKeyChartsOnly={true} charts={charts} />, - relatedChartsWrapper - ) - } -} diff --git a/site/blocks/StickyNav.tsx b/site/blocks/StickyNav.tsx index 269d78792df..9cebb6629ed 100644 --- a/site/blocks/StickyNav.tsx +++ b/site/blocks/StickyNav.tsx @@ -1,5 +1,4 @@ import React, { createRef } from "react" -import ReactDOM from "react-dom" import cx from "classnames" import { throttle } from "@ourworldindata/utils" @@ -203,20 +202,3 @@ class StickyNav extends React.Component< } export default StickyNav - -export const hydrateStickyNav = () => { - const wrapper = document.querySelector(".sticky-nav") - if (wrapper) { - const anchorTags = - document.querySelectorAll<HTMLAnchorElement>(".sticky-nav a") - const links: StickyNavLink[] = [] - - for (const anchorTag of anchorTags) { - const text = anchorTag.innerText - const target = anchorTag.hash - links.push({ text, target }) - } - - ReactDOM.hydrate(<StickyNav links={links} />, wrapper) - } -} diff --git a/site/blocks/TechnicalText.scss b/site/blocks/TechnicalText.scss deleted file mode 100644 index b5ce79a2335..00000000000 --- a/site/blocks/TechnicalText.scss +++ /dev/null @@ -1,11 +0,0 @@ -.wp-block-owid-technical-text { - h5 { - text-transform: uppercase; - } - color: $grey-text-color; -} - -.article-content > section .wp-block-columns .wp-block-owid-technical-text h5 { - font-family: $sans-serif-font-stack; - font-size: 1rem; -} diff --git a/site/blocks/summary.scss b/site/blocks/summary.scss deleted file mode 100644 index e3c1890b109..00000000000 --- a/site/blocks/summary.scss +++ /dev/null @@ -1,39 +0,0 @@ -.wp-block-owid-summary { - padding: 24px 32px; - margin: 32px 0; - - border: 1px solid $oxford-blue; - border-top-width: 5px; - color: $blue-100; - background-color: #f9f4e4; - - h2 { - margin-top: 0; - font-family: $serif-font-stack; - font-size: 2rem; - text-align: center; - color: inherit; - } - li { - margin-bottom: 0.5rem; - a { - display: block; - color: inherit; - transition: all 150ms; - &:hover, - &:visited { - color: inherit; - } - &:hover::after { - color: $vermillion; - } - &::after { - display: inline-block; - content: "\2193\a0\a0 jump to section"; - margin-left: 0.5rem; - font-size: 0.8rem; - color: $blue-40; - } - } - } -} diff --git a/site/css/topic-page.scss b/site/css/topic-page.scss deleted file mode 100644 index e3a3e549f20..00000000000 --- a/site/css/topic-page.scss +++ /dev/null @@ -1,98 +0,0 @@ -// added to the body of a page via formattingOptions -// scopes styles just to topic pages when necessary -.topic-page { - .article-content > section:not(:first-of-type), - section { - margin: 0; - padding: 0; - } - - .authors-byline { - @include body-1-regular; - } - - @include sm-only { - .large-banner .article-header { - padding: 24px 16px 16px 16px; - .authors-byline { - font-size: 0.875rem; - } - } - } - - .article-header .tools { - display: none; - } - - h2 { - @include h1-semibold; - } - - .front-matter { - p { - @include body-1-regular; - } - } - - .article-content > section > h3 { - @include h1-semibold; - border-top: none; - text-align: center; - padding-bottom: 40px; - margin-bottom: 0; - - // Explorer and All Our Charts sections - &[id*="explore"] { - // faking a page-wide gray background until sections are reworked - background: $gray-10; - box-shadow: - min(50vw, 1280px) 0px 0 0 $gray-10, - max(-50vw, -1280px) 0px 0 0 $gray-10; - + figure, - + div { - background: $gray-10; - box-shadow: - min(50vw, 1280px) 0px 0 0 $gray-10, - max(-50vw, -1280px) 0px 0 0 $gray-10; - padding-bottom: 48px; - - .expandable-paragraph { - p { - @include body-2-regular; - } - - &::after { - background: linear-gradient( - rgba(255, 255, 255, 0), - $gray-10 - ); - } - } - } - } - - &[id*="all-our"] { - // faking a page-wide gray background until sections are reworked - background: $gray-10; - box-shadow: - 50vw 0px 0 0 $gray-10, - -50vw 0px 0 0 $gray-10; - + .wp-block-full-content-width .related-charts { - background: $gray-10; - box-shadow: - 50vw 0px 0 0 $gray-10, - -50vw 0px 0 0 $gray-10; - padding-bottom: 48px; - } - } - } - - .key-insights-heading { - margin-bottom: 32px; - } - - .article-footer { - border-top: none; - padding-top: 0; - } -} diff --git a/site/formatting.tsx b/site/formatting.tsx index a4d148e33c8..c3c275ec5c1 100644 --- a/site/formatting.tsx +++ b/site/formatting.tsx @@ -11,20 +11,13 @@ import { } from "@ourworldindata/utils" import { BAKED_BASE_URL } from "../settings/serverSettings.js" import { bakeGlobalEntitySelector } from "./bakeGlobalEntitySelector.js" -import { - KEY_INSIGHTS_CLASS_NAME, - KEY_INSIGHTS_SLIDE_CLASS_NAME, - KEY_INSIGHTS_SLIDE_CONTENT_CLASS_NAME, -} from "./blocks/KeyInsights.js" import { PROMINENT_LINK_CLASSNAME } from "./blocks/ProminentLink.js" import { Byline } from "./Byline.js" import { SectionHeading } from "./SectionHeading.js" import { FormattingOptions } from "@ourworldindata/types" import { GRAPHER_PREVIEW_CLASS } from "./SiteConstants.js" -export const SUMMARY_CLASSNAME = "wp-block-owid-summary" export const RESEARCH_AND_WRITING_CLASSNAME = "wp-block-research-and-writing" -export const KEY_INSIGHTS_H2_CLASSNAME = "key-insights-heading" export const formatUrls = (html: string) => { const formatted = html @@ -157,43 +150,6 @@ export const splitContentIntoSectionsAndColumns = ( } } - class KeyInsightsHandler extends AbstractHandler { - handle(el: CheerioElement, context: ColumnsContext) { - const $el = cheerioEl(el) - if ($el.hasClass(`${KEY_INSIGHTS_CLASS_NAME}`)) { - flushAndResetColumns(context) - - // Split the content of each slide into columns - $el.find(`.${KEY_INSIGHTS_SLIDE_CLASS_NAME}`).each( - (_, slide) => { - const $slide = cheerioEl(slide) - const $title = $slide.find("h4") - const $slideContent = $slide.find( - `.${KEY_INSIGHTS_SLIDE_CONTENT_CLASS_NAME}` - ) - const slideContentHtml = $slideContent.html() - - if (!slideContentHtml) return - const $ = cheerio.load(slideContentHtml) - splitContentIntoSectionsAndColumns($) - $slideContent.html(getBodyHtml($)) - - // the h4 title creates an (undesirable here) column set - // in splitContentIntoSectionsAndColumns(). So we inject - // it into the first column after processing move the - $slide - .find(".wp-block-column:first-child") - .prepend($title) - } - ) - - context.$section.append($el) - return null - } - return super.handle(el, context) - } - } - class H4Handler extends AbstractHandler { static isElementH4 = (el: CheerioElement): boolean => { return el.name === "h4" @@ -308,7 +264,6 @@ export const splitContentIntoSectionsAndColumns = ( // - A handler should never do both 1) and 2) – both apply a transformation and additionally let other handlers apply transformations. // see https://github.com/owid/owid-grapher/pull/1220#discussion_r816126831 fullWidthHandler - .setNext(new KeyInsightsHandler()) .setNext(new H4Handler()) .setNext(new SideBySideHandler()) .setNext(new StandaloneFigureHandler()) @@ -353,8 +308,6 @@ const addTocToSections = ( .map((el) => cheerioEl(el)) .filter(($el) => { return ( - $el.closest(`.${SUMMARY_CLASSNAME}`).length === 0 && - $el.closest(`.${KEY_INSIGHTS_H2_CLASSNAME}`).length === 0 && $el.closest(`.${RESEARCH_AND_WRITING_CLASSNAME}`).length === 0 ) }) diff --git a/site/blocks/KeyInsights.jsdom.test.tsx b/site/gdocs/components/KeyInsights.jsdom.test.tsx similarity index 56% rename from site/blocks/KeyInsights.jsdom.test.tsx rename to site/gdocs/components/KeyInsights.jsdom.test.tsx index 1abd478e8d7..cf6bbf6a1f6 100755 --- a/site/blocks/KeyInsights.jsdom.test.tsx +++ b/site/gdocs/components/KeyInsights.jsdom.test.tsx @@ -1,18 +1,19 @@ #! /usr/bin/env jest import React from "react" -import { - KeyInsightsSlides, - KeyInsightsThumbs, - KEY_INSIGHTS_CLASS_NAME, - KEY_INSIGHTS_INSIGHT_PARAM, -} from "./KeyInsights.js" +import { KEY_INSIGHTS_INSIGHT_PARAM } from "./KeyInsights.js" -import { KeyInsight, getWindowUrl } from "@ourworldindata/utils" +import { + EnrichedBlockKeyInsights, + EnrichedBlockKeyInsightsSlide, + getWindowUrl, + slugify, +} from "@ourworldindata/utils" import { fireEvent, render, screen } from "@testing-library/react" import "@testing-library/jest-dom" import { jest } from "@jest/globals" +import ArticleBlock from "./ArticleBlock.js" const KEY_INSIGHTS_SLUG = "key-insights" @@ -27,56 +28,41 @@ beforeEach(() => { window.IntersectionObserver = mockIntersectionObserver as any }) -const generateKeyInsights = ( - count: number, - { isTitleHidden = false }: { isTitleHidden?: boolean } = {} -): KeyInsight[] => { - return [...new Array(count)].map((_, idx) => ({ - title: `Key insight ${idx}`, - slug: `key-insight-${idx}`, - content: `content ${idx}`, - isTitleHidden, - })) +const generateKeyInsights = (count: number): EnrichedBlockKeyInsights => { + return { + type: "key-insights", + heading: "Key insights", + parseErrors: [], + insights: [...new Array(count)].map((_, idx) => ({ + title: `Key insight ${idx}`, + type: "key-insight-slide", + url: "https://ourworldindata.org/grapher/life-expectancy", + content: [], + })) as EnrichedBlockKeyInsightsSlide[], + } } -const renderKeyInsights = (keyInsights: KeyInsight[]) => { - const slug = KEY_INSIGHTS_SLUG - const title = "Key insights" - const titles = keyInsights.map(({ title }) => title) - +const renderKeyInsights = (keyInsightsBlock: EnrichedBlockKeyInsights) => { render( - <> - <h3 id={slug}>{title}</h3> - <div className={`${KEY_INSIGHTS_CLASS_NAME}`}> - <div className="block-wrapper"> - <KeyInsightsThumbs titles={titles} /> - </div> - <KeyInsightsSlides insights={keyInsights} /> - </div> - </> + <div> + <p>test??</p> + <ArticleBlock b={keyInsightsBlock} /> + </div> ) } -it("renders key insights with hidden titles", () => { - const keyInsights = generateKeyInsights(3, { isTitleHidden: true }) - renderKeyInsights(keyInsights) - - expect(screen.getAllByRole("tab")).toHaveLength(3) - expect(screen.queryAllByRole("heading", { level: 4 })).toHaveLength(0) -}) - it("renders key insights and selects the first one", () => { const keyInsights = generateKeyInsights(3) renderKeyInsights(keyInsights) expect(screen.getAllByRole("tab")).toHaveLength(3) expect(screen.getByRole("tab", { selected: true })).toHaveTextContent( - keyInsights[0].title + keyInsights.insights[0].title ) fireEvent.click(screen.getAllByRole("tab")[1]) expect(screen.getByRole("tab", { selected: true })).toHaveTextContent( - keyInsights[1].title + keyInsights.insights[1].title ) }) @@ -101,7 +87,8 @@ it("updates the URL", () => { fireEvent.click(screen.getAllByRole("tab")[1]) expect(getWindowUrl().hash).toEqual(`#${KEY_INSIGHTS_SLUG}`) + const slugifiedTitle = slugify(keyInsights.insights[1].title) expect(getWindowUrl().queryStr).toEqual( - `?${KEY_INSIGHTS_INSIGHT_PARAM}=${keyInsights[1].slug}` + `?${KEY_INSIGHTS_INSIGHT_PARAM}=${slugifiedTitle}` ) }) diff --git a/site/blocks/KeyInsights.scss b/site/gdocs/components/KeyInsights.scss similarity index 99% rename from site/blocks/KeyInsights.scss rename to site/gdocs/components/KeyInsights.scss index 5447cce3d18..5c8ea9e5e17 100644 --- a/site/blocks/KeyInsights.scss +++ b/site/gdocs/components/KeyInsights.scss @@ -2,7 +2,7 @@ $slide-content-height: $grapher-height; @import "react-horizontal-scrolling-menu/styles.css"; -.wp-block-owid-key-insights { +.key-insights { .react-horizontal-scrolling-menu--wrapper { position: relative; } diff --git a/site/gdocs/components/KeyInsights.tsx b/site/gdocs/components/KeyInsights.tsx index 3a494fab4db..118833f1b58 100644 --- a/site/gdocs/components/KeyInsights.tsx +++ b/site/gdocs/components/KeyInsights.tsx @@ -1,21 +1,283 @@ -import React from "react" +import React, { useCallback, useContext, useEffect, useState } from "react" +import { ScrollMenu, VisibilityContext } from "react-horizontal-scrolling-menu" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" +import { faAngleRight } from "@fortawesome/free-solid-svg-icons" import cx from "classnames" import { + getWindowUrl, + setWindowUrl, EnrichedBlockKeyInsightsSlide, slugify, KEY_INSIGHTS_ID, } from "@ourworldindata/utils" -import { - KEY_INSIGHTS_CLASS_NAME, - KEY_INSIGHTS_SLIDES_CLASS_NAME, - KEY_INSIGHTS_SLIDE_CLASS_NAME, - KEY_INSIGHTS_SLIDE_CONTENT_CLASS_NAME, - KeyInsightsThumbs, -} from "../../blocks/KeyInsights.js" + import { ArticleBlocks } from "./ArticleBlocks.js" import Image from "./Image.js" import Chart from "./Chart.js" +export const KEY_INSIGHTS_CLASS_NAME = "key-insights" +export const KEY_INSIGHTS_INSIGHT_PARAM = "insight" +const KEY_INSIGHTS_THUMBS_CLASS_NAME = "thumbs" +const KEY_INSIGHTS_THUMB_CLASS_NAME = "thumb" +const KEY_INSIGHTS_SLIDES_CLASS_NAME = "slides" +const KEY_INSIGHTS_SLIDE_CLASS_NAME = "slide" +const KEY_INSIGHTS_SLIDE_CONTENT_CLASS_NAME = "content" + +type scrollVisibilityApiType = React.ContextType<typeof VisibilityContext> + +export enum ArrowDirection { + prev = "prev", + next = "next", +} + +const Thumb = ({ + title, + onClick, + selected, +}: { + title: string + onClick: () => void + itemId: string // needed by react-horizontal-scrolling-menu, see lib's examples + selected: boolean +}) => { + return ( + <button + aria-label={`Go to slide: ${title}`} + onClick={onClick} + role="tab" + aria-selected={selected} + className={KEY_INSIGHTS_THUMB_CLASS_NAME} + > + {title} + </button> + ) +} + +/** + * Tab-based switcher for key insights + * + * NB: this component has only received limited efforts towards accessibility. + * + * A next possible step would be to managage focus via arrow keys (see + * https://w3c.github.io/aria/#managingfocus). A good implementation of + * accessibility practices for this kind of widget is available at + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role + */ +export const KeyInsightsThumbs = ({ titles }: { titles: string[] }) => { + const [selectedId, setSelectedId] = useState<string>("0") + const [slides, setSlides] = useState<HTMLElement | null>(null) + const [slug, setSlug] = useState<string>("") + const apiRef = React.useRef({} as scrollVisibilityApiType) + + // Not using useRef() here so that the "select slide based on hash" effect, + // running on page load only, runs after the ref has been attached (and not + // on first render, which would be before) + // https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node + const thumbsRef = useCallback((node) => { + if (node !== null) { + const keyInsightsNode = node.parentElement?.parentElement + + const tempSlides = keyInsightsNode?.querySelector( + `.${KEY_INSIGHTS_SLIDES_CLASS_NAME}` + ) + setSlides(tempSlides) + // get slug from previous <h3> + setSlug(keyInsightsNode?.previousElementSibling?.getAttribute("id")) + } + }, []) + + // Select active slide based on URL + useEffect(() => { + if (!slides) return + + const windowUrl = getWindowUrl() + if (!windowUrl.queryParams.insight) return + + // find the slide containing the h4 with the id matching the ?insight query param + const selectedSlideIdx = Array.from( + slides.querySelectorAll(`.${KEY_INSIGHTS_SLIDE_CLASS_NAME}`) + ).findIndex((slide) => + slide.querySelector( + `#${windowUrl.queryParams[KEY_INSIGHTS_INSIGHT_PARAM]}` + ) + ) + + if (selectedSlideIdx === -1) return + setSelectedId(selectedSlideIdx.toString()) + }, [slides]) + + // Scroll to selected item + useEffect(() => { + const item = apiRef.current.getItemById(selectedId) + if (!item) return + + apiRef.current.scrollToItem(item, "smooth", "center", "nearest") + }, [selectedId]) + + // Select active slide when corresponding thumb selected + useEffect(() => { + if (!slides) return + + // A less imperative, more React way to do this would be preferred. To + // switch between slides, I aimed to keep their content untouched + // (including event listeners hydrated by other components), while only + // updating their wrappers. Managing the switching logic through React + // would have required hydrating KeyInsightsSlides as well as all + // possible content components within them - even though they would have + // been already hydrated at the page level. From that perspective, the + // gain is not clear, and the approach not necessarily cleaner, so I + // stuck with the imperative approach. + + slides + .querySelectorAll(`.${KEY_INSIGHTS_SLIDE_CLASS_NAME}`) + .forEach((slide, idx) => { + if (idx === Number(selectedId)) { + slide.setAttribute("data-active", "true") + const windowUrl = getWindowUrl() + const anchor = slide.querySelector("h4")?.getAttribute("id") + if (!anchor) return + setWindowUrl( + windowUrl + .updateQueryParams({ + [KEY_INSIGHTS_INSIGHT_PARAM]: anchor, + }) + // When a key insight slug is changed, links + // pointing to that key insight soft-break and take + // readers to the top of the page. Adding an anchor + // pointing to the the block title (h3) serves as a + // stopgap, taking readers to the key insights block + // instead but without selecting a particular + // insight. + // + // This also improves the UX of readers coming + // through shared insights URL. e.g. + // /key-insights-demo?insight=insight-1#key-insights + // shows the whole insights block, including its + // titles (the target of the anchor). + .update({ hash: `#${slug}` }) + ) + } else { + slide.setAttribute("data-active", "false") + } + }) + // see https://github.com/owid/owid-grapher/pull/1435#discussion_r888058198 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedId]) + + return ( + <div + className={KEY_INSIGHTS_THUMBS_CLASS_NAME} + role="tablist" + ref={thumbsRef} + > + <ScrollMenu + LeftArrow={LeftArrow} + RightArrow={RightArrow} + transitionDuration={200} + apiRef={apiRef} + > + {titles.map((title, i) => { + const itemId = `${i}` + return ( + <Thumb + title={title} + key={itemId} + itemId={itemId} + onClick={() => { + setSelectedId(itemId) + }} + selected={itemId === selectedId} + /> + ) + })} + </ScrollMenu> + </div> + ) +} + +const Arrow = ({ + children, + disabled, + onClick, + className, + direction, +}: { + children: React.ReactNode + disabled: boolean + onClick: VoidFunction + className?: string + direction: ArrowDirection +}) => { + const classes = ["arrow", className] + return ( + <button + aria-label={`Scroll to ${ + direction === ArrowDirection.next ? "next" : "previous" + } slide`} + disabled={disabled} + onClick={onClick} + className={classes.join(" ")} + > + {children} + </button> + ) +} + +const LeftArrow = () => { + const { + isFirstItemVisible, + scrollPrev, + visibleItemsWithoutSeparators, + initComplete, + } = useContext(VisibilityContext) + + const [disabled, setDisabled] = useState( + !initComplete || (initComplete && isFirstItemVisible) + ) + useEffect(() => { + // NOTE: detect if whole component visible + if (visibleItemsWithoutSeparators.length) { + setDisabled(isFirstItemVisible) + } + }, [isFirstItemVisible, visibleItemsWithoutSeparators]) + + return !disabled ? ( + <Arrow + disabled={false} + onClick={() => scrollPrev()} + className="left" + direction={ArrowDirection.prev} + > + <FontAwesomeIcon icon={faAngleRight} flip="horizontal" /> + </Arrow> + ) : null +} + +const RightArrow = () => { + const { isLastItemVisible, scrollNext, visibleItemsWithoutSeparators } = + useContext(VisibilityContext) + + const [disabled, setDisabled] = useState( + !visibleItemsWithoutSeparators.length && isLastItemVisible + ) + useEffect(() => { + if (visibleItemsWithoutSeparators.length) { + setDisabled(isLastItemVisible) + } + }, [isLastItemVisible, visibleItemsWithoutSeparators]) + + return !disabled ? ( + <Arrow + disabled={false} + onClick={() => scrollNext()} + className="right" + direction={ArrowDirection.next} + > + <FontAwesomeIcon icon={faAngleRight} /> + </Arrow> + ) : null +} + type KeyInsightsProps = { className?: string insights: EnrichedBlockKeyInsightsSlide[] diff --git a/site/gdocs/components/topic-page.scss b/site/gdocs/components/topic-page.scss index 2be0522f1d9..69a8fedf1f6 100644 --- a/site/gdocs/components/topic-page.scss +++ b/site/gdocs/components/topic-page.scss @@ -150,7 +150,7 @@ } // small amount of extra padding to match figma designs which is 40px total for key-insights heading -.article-block__heading + .wp-block-owid-key-insights { +.article-block__heading + .key-insights { margin-top: 8px; } diff --git a/site/owid.entry.ts b/site/owid.entry.ts index 7b6c67b1d0a..06bbb7c8fea 100644 --- a/site/owid.entry.ts +++ b/site/owid.entry.ts @@ -12,7 +12,6 @@ import { runFeedbackPage } from "./Feedback.js" import { runDonateForm } from "./DonateForm.js" import { runCountryProfilePage } from "./runCountryProfilePage.js" import { runTableOfContents } from "./TableOfContents.js" -import { runRelatedCharts } from "./blocks/RelatedCharts.js" import { Explorer } from "../explorer/Explorer.js" import { ENV, @@ -43,7 +42,6 @@ window.runFeedbackPage = runFeedbackPage window.runDonateForm = runDonateForm window.runCountryProfilePage = runCountryProfilePage window.runTableOfContents = runTableOfContents -window.runRelatedCharts = runRelatedCharts window.MultiEmbedderSingleton = MultiEmbedderSingleton // Note: do a text search of the project for "runSiteFooterScripts" to find the usage. todo: clean that up. diff --git a/site/owid.scss b/site/owid.scss index c45970921d4..2c10ba4bd4a 100644 --- a/site/owid.scss +++ b/site/owid.scss @@ -51,7 +51,6 @@ @import "css/page.scss"; @import "css/sidebar.scss"; -@import "css/topic-page.scss"; @import "css/faq.scss"; @import "css/covid.scss"; @import "css/chart.scss"; @@ -70,14 +69,11 @@ @import "./blocks/additional-information.scss"; @import "./blocks/prominent-link.scss"; @import "./blocks/related-charts.scss"; -@import "./blocks/summary.scss"; @import "./blocks/help.scss"; @import "./blocks/CookiePreferences.scss"; @import "./blocks/Grid.scss"; @import "./blocks/Card.scss"; @import "./blocks/BiographyCard.scss"; -@import "./blocks/KeyInsights.scss"; -@import "./blocks/TechnicalText.scss"; @import "./blocks/research-and-writing.scss"; @import "./blocks/GalleryArrow.scss"; @import "./blocks/StickyNav.scss"; @@ -90,6 +86,7 @@ @import "./gdocs/components/TableOfContents.scss"; @import "./gdocs/components/MissingData.scss"; @import "./gdocs/components/HomepageIntro.scss"; +@import "./gdocs/components/KeyInsights.scss"; @import "./gdocs/components/AllCharts.scss"; @import "./gdocs/components/AdditionalCharts.scss"; @import "./gdocs/components/ResearchAndWriting.scss"; diff --git a/site/runSiteFooterScripts.ts b/site/runSiteFooterScripts.ts index 1bb18dbd639..71f10438dc4 100644 --- a/site/runSiteFooterScripts.ts +++ b/site/runSiteFooterScripts.ts @@ -13,10 +13,7 @@ import { runDetailsOnDemand } from "./detailsOnDemand.js" import { runDataTokens } from "./runDataTokens.js" import { runSearchCountry } from "./SearchCountry.js" import { hydrate as hydrateAdditionalInformation } from "./blocks/AdditionalInformation.js" -import { hydrateKeyInsights } from "./blocks/KeyInsights.js" -import { hydrateExpandableParagraphs } from "./blocks/ExpandableParagraph.js" import { hydrateCodeSnippets } from "@ourworldindata/components" -import { hydrateStickyNav } from "./blocks/StickyNav.js" import { hydrateDynamicCollectionPage } from "./collections/DynamicCollection.js" import { _OWID_DATA_INSIGHTS_INDEX_PAGE_DATA, @@ -85,10 +82,7 @@ export const runSiteFooterScripts = ( runDataTokens() runSearchCountry() hydrateAdditionalInformation() - hydrateKeyInsights() - hydrateExpandableParagraphs() hydrateCodeSnippets() - hydrateStickyNav() MultiEmbedderSingleton.setUpGlobalEntitySelectorForEmbeds() MultiEmbedderSingleton.embedAll() runAllGraphersLoadedListener()