diff --git a/dotcom-rendering/src/devServer/docs/home.tsx b/dotcom-rendering/src/devServer/docs/home.tsx new file mode 100644 index 00000000000..d81f6aa4303 --- /dev/null +++ b/dotcom-rendering/src/devServer/docs/home.tsx @@ -0,0 +1,194 @@ +import { + articleBold17Object, + headlineBold20Object, + palette, + space, + until, +} from '@guardian/source/foundations'; +import { + LinkButton, + SvgArrowRightStraight, + TextInput, +} from '@guardian/source/react-components'; +import { formatToString } from '../../lib/articleFormat'; +import type { Article as ArticleModel } from '../../types/article'; +import type { Block } from '../../types/blocks'; +import type { FEElement } from '../../types/content'; +import type { RenderingTarget } from '../../types/renderingTarget'; + +export const Home = ({ articles }: { articles: ArticleModel[] }) => ( + <> +
+ {articles.map((article) => ( + + ))} + +); + +const Form = () => ( + + + +); + +const ArticleInfo = ({ article }: { article: ArticleModel }) => ( +
+ + {article.frontendData.webTitle} + + +
+
Format
+
{formatToString(article)}
+
Main Media
+
{mainMediaElement(article.frontendData.mainMediaElements)}
+
Body
+
{bodyElements(article.frontendData.blocks)}
+
+
+); + +const ArticleLinks = ({ url }: { url: URL }) => ( +
+ + + +
+); + +const mainMediaElement = (elements: FEElement[]): string => { + const element = elements[0]; + + return element !== undefined ? elementToString(element) : 'none'; +}; + +const bodyElements = (blocks: Block[]): string => + blocks + .flatMap((block) => block.elements) + .map(elementToString) + .reduce(mergeText, []) + .join(', '); + +const elementToString = (element: FEElement): string => { + const role = 'role' in element ? ` (${element.role})` : ''; + const elem = element._type.split('.').at(-1)?.replace('BlockElement', ''); + + return `${elem}${role}`; +}; + +const mergeText = (elems: string[], elem: string) => { + if (elem === 'Text' && elems.at(-1) === 'Text') { + return elems; + } + + return [...elems, elem]; +}; + +const StageLinks = ({ stage, url }: { stage: Stage; url: URL }) => ( +
+ } + iconSide="right" + theme={{ backgroundPrimary: stageColour(stage) }} + size="small" + > + 📱 Apps {stage} + + } + iconSide="right" + theme={{ backgroundPrimary: stageColour(stage) }} + size="small" + > + 🌍 Dotcom {stage} + +
+); + +type Stage = 'DEV' | 'CODE' | 'PROD'; + +const buildUrl = (target: RenderingTarget, stage: Stage, url: URL) => { + switch (stage) { + case 'PROD': { + const builtUrl = new URL(url.pathname, prodOrigin); + builtUrl.search = params(target).toString(); + + return builtUrl; + } + case 'CODE': { + const builtUrl = new URL(url.pathname, codeOrigin); + builtUrl.search = params(target).toString(); + + return builtUrl; + } + case 'DEV': + return new URL(`${devPath(target)}/${url.toString()}`, devOrigin); + } +}; + +const stageTextColour = (stage: Stage) => { + switch (stage) { + case 'DEV': + return palette.neutral[7]; + case 'CODE': + case 'PROD': + return palette.neutral[100]; + } +}; + +const stageColour = (stage: Stage) => { + switch (stage) { + case 'DEV': + return palette.brandAlt[400]; + case 'CODE': + return palette.success[400]; + case 'PROD': + return palette.brand[400]; + } +}; + +const devOrigin = 'http://localhost:3030'; +const codeOrigin = 'https://m.code.dev-theguardian.com'; +const prodOrigin = 'https://www.theguardian.com'; + +const params = (target: RenderingTarget): URLSearchParams => + new URLSearchParams(target === 'Apps' ? { dcr: 'apps' } : undefined); + +const devPath = (target: RenderingTarget): string => { + switch (target) { + case 'Web': + return '/Article'; + case 'Apps': + return '/AppsArticle'; + } +}; diff --git a/dotcom-rendering/src/devServer/routers/home.tsx b/dotcom-rendering/src/devServer/routers/home.tsx new file mode 100644 index 00000000000..72ae2273cc1 --- /dev/null +++ b/dotcom-rendering/src/devServer/routers/home.tsx @@ -0,0 +1,78 @@ +import { type Request, Router } from 'express'; +import { error, ok, type Result } from '../../lib/result'; +import { validateAsFEArticle } from '../../model/validate'; +import { type Article, enhanceArticleType } from '../../types/article'; +import { Home } from '../docs/home'; +import { sendReact } from '../send'; + +export const home = Router(); + +home.get('/', async (req, res, next) => { + const dotcomURL = parseUrl(req); + + if (dotcomURL.kind === 'ok') { + const article = await extractArticle(dotcomURL.value); + + return sendReact('Home', )(req, res, next); + } else if (dotcomURL.error.kind === 'noParameter') { + return sendReact('Home', )(req, res, next); + } else { + console.error(dotcomURL.error.message); + res.sendStatus(400); + } +}); + +const parseUrl = (req: Request): Result => { + const param = req.query.dotcomURL; + + if (param === undefined) { + return error({ kind: 'noParameter' }); + } + + if (typeof param !== 'string') { + return error({ + kind: 'parseError', + message: + "'dotcomURL' query parameter must contain a single string.", + }); + } + + try { + const url = new URL(param); + + if (url.host !== 'www.theguardian.com' || url.protocol !== 'https:') { + return error({ + kind: 'parseError', + message: + "'dotcomURL' must start with 'https://www.theguardian.com'", + }); + } + + return ok(url); + } catch (_e) { + return error({ + kind: 'parseError', + message: `Could not parse the 'dotcomURL' query parameter. Received: ${param}, expected: a valid URL string.`, + }); + } +}; + +type UrlError = + | { + kind: 'noParameter'; + } + | { + kind: 'parseError'; + message: string; + }; + +const extractArticle = async (dotcomURL: URL): Promise
=> { + const url = new URL(dotcomURL); + url.pathname = `${url.pathname}.json`; + url.searchParams.append('dcr', 'true'); + + const json = await fetch(url).then((res) => res.json()); + const frontendData = validateAsFEArticle(json); + + return enhanceArticleType(frontendData, 'Web'); +}; diff --git a/dotcom-rendering/src/server/server.dev.ts b/dotcom-rendering/src/server/server.dev.ts index 134f284341e..d28f5d12569 100644 --- a/dotcom-rendering/src/server/server.dev.ts +++ b/dotcom-rendering/src/server/server.dev.ts @@ -1,4 +1,5 @@ import { type Handler, Router } from 'express'; +import { home } from '../devServer/routers/home'; import { pages } from '../devServer/routers/pages'; import { targets } from '../devServer/routers/targets'; import { handleAllEditorialNewslettersPage } from './handler.allEditorialNewslettersPage.web'; @@ -125,6 +126,7 @@ renderer.post('/CricketMatchPage', handleCricketMatchPage); renderer.post('/FootballMatchSummaryPage', handleFootballMatchPage); const router = Router(); +router.use('/home', home); router.use('/pages', pages); router.use('/targets', targets); router.use(renderer);