Skip to content

Commit

Permalink
Editions crossword player (#12935)
Browse files Browse the repository at this point in the history
Co-authored-by: Jamie B <[email protected]>
Co-authored-by: Marjan Kalanaki <[email protected]>
Co-authored-by: Frederick O'Brien <[email protected]>
Co-authored-by: Simon Adcock <[email protected]>
Co-authored-by: Simon Adcock <[email protected]>
  • Loading branch information
6 people authored Dec 9, 2024
1 parent 2a1bd37 commit 97c4202
Show file tree
Hide file tree
Showing 17 changed files with 307 additions and 64 deletions.
53 changes: 53 additions & 0 deletions dotcom-rendering/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,59 @@ declare module 'dynamic-import-polyfill' {
}) => void;
}

declare module '@guardian/react-crossword' {
import type { FC } from 'react';

export type Cell = {
number: number;
value: string;
};

export type Clue = {
id: string;
number: number;
humanNumber: string;
direction: 'across' | 'down';
position: { x: number; y: number };
separatorLocations: {
','?: number[];
'-'?: number[];
};
length: number;
clue: string;
group: string[];
solution?: string;
format?: string;
};

export type CrosswordProps = {
id: string;
data: {
id?: string;
number: number;
name: string;
date: string;
dimensions: { cols: number; rows: number };
entries: Clue[];
solutionAvailable: boolean;
hasNumbers: boolean;
randomCluesOrdering: boolean;
instructions?: string;
creator?: { name: string; webUrl: string };
pdf?: string;
annotatedSolution?: string;
dateSolutionAvailable: string;
};
onCorrect?: (cell: Cell) => void;
onLoaded?: () => void;
};

const Crossword: FC<CrosswordProps>;

// eslint-disable-next-line import/no-default-export -- react-crossword uses default exports
export default Crossword;
}

// SVG handling
declare module '*.svg' {
const content: any;
Expand Down
1 change: 1 addition & 0 deletions dotcom-rendering/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@guardian/identity-auth-frontend": "4.0.0",
"@guardian/libs": "19.2.1",
"@guardian/ophan-tracker-js": "2.2.5",
"@guardian/react-crossword": "2.0.2",
"@guardian/shimport": "1.0.2",
"@guardian/source": "8.0.0",
"@guardian/source-development-kitchen": "12.0.0",
Expand Down
24 changes: 24 additions & 0 deletions dotcom-rendering/src/client/main.editionsCrossword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* eslint-disable ssr-friendly/no-dom-globals-in-module-scope */
// @ts-expect-error: Cannot find module
import css from '@guardian/react-crossword/lib/index.css';
import { createRoot } from 'react-dom/client';
import { Crosswords } from '../components/Crosswords.editions';
import type { FEEditionsCrossword } from '../types/editionsCrossword';

const style = document.createElement('style');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We know this will be a string
style.innerHTML = css;
document.body.appendChild(style);

const element = document.getElementById('editions-crossword-player');
if (!element) {
throw new Error('No element found with id "editions-crossword-player"');
}
const crosswordsData = element.dataset.crosswords;
if (!crosswordsData) {
throw new Error('No data found for "editions-crossword-player"');
}

const crosswords = JSON.parse(crosswordsData) as FEEditionsCrossword[];
const root = createRoot(element);
root.render(<Crosswords crosswords={crosswords} timeZone={undefined} />);
36 changes: 24 additions & 12 deletions dotcom-rendering/src/components/CrosswordSelect.editions.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { from } from '@guardian/source/foundations';
import { Option, Select } from '@guardian/source/react-components';

type Props<Crossword> = {
/**
* Crosswords organised by date.
Expand Down Expand Up @@ -40,25 +43,34 @@ export function CrosswordSelect<Crossword extends { name: string }>({
}

return (
<>
<label htmlFor="date-select">Date</label>
<select
<div
css={{
display: 'flex',
flexDirection: 'column',
marginBottom: '20px',
[from.tablet]: {
maxWidth: '481px',
},
}}
>
<Select
id="date-select"
value={date}
onChange={(e) => onDateChange(e.target.value)}
label="Date"
>
{dates.map((crosswordDate) => (
<option value={crosswordDate} key={crosswordDate}>
<Option value={crosswordDate} key={crosswordDate}>
{crosswordDate}
</option>
</Option>
))}
</select>
</Select>
{crosswordsByDate[date] === undefined ||
crosswordsByDate[date].length === 0 ? null : (
<>
<label htmlFor="crossword-select">Crossword</label>
<select
<Select
id="crossword-select"
label="Crossword"
value={crosswordIndex}
onChange={(e) => {
const index = parseInt(e.target.value);
Expand All @@ -69,13 +81,13 @@ export function CrosswordSelect<Crossword extends { name: string }>({
}}
>
{crosswordsByDate[date].map((crossword, index) => (
<option value={index} key={crossword.name}>
<Option value={index} key={crossword.name}>
{crossword.name}
</option>
</Option>
))}
</select>
</Select>
</>
)}
</>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const UKTimezone = {
await waitFor(() => {
const clue = canvas.getByRole('listitem');
return expect(clue.textContent).toEqual(
args.crosswords[0]?.entries[0]?.clue,
`1${args.crosswords[0]?.entries[0]?.clue}`,
);
});
},
Expand Down
11 changes: 6 additions & 5 deletions dotcom-rendering/src/components/Crosswords.editions.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Crossword from '@guardian/react-crossword';
import { useEffect, useState } from 'react';
import {
type CrosswordsByDate,
Expand Down Expand Up @@ -83,11 +84,11 @@ const CrosswordsWithInitialDate = ({
onCrosswordIndexChange={setCrosswordIndex}
/>
{crossword === undefined ? null : (
<ul>
{crossword.entries.map((entry) => (
<li key={entry.id}>{entry.clue}</li>
))}
</ul>
<Crossword
data={crossword}
id={crossword.name}
key={crossword.name}
/>
)}
</>
);
Expand Down
17 changes: 17 additions & 0 deletions dotcom-rendering/src/components/EditionsCrosswordPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { StrictMode } from 'react';
import type { FEEditionsCrosswords } from '../types/editionsCrossword';

interface Props {
editionsCrosswords: FEEditionsCrosswords;
}

export const EditionsCrosswordPage = ({ editionsCrosswords }: Props) => {
return (
<StrictMode>
<main
id="editions-crossword-player"
data-crosswords={JSON.stringify(editionsCrosswords.crosswords)}
></main>
</StrictMode>
);
};
9 changes: 7 additions & 2 deletions dotcom-rendering/src/lib/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export type Build =
| 'client.apps'
| 'client.web'
| 'client.web.variant'
| 'client.web.legacy';
| 'client.web.legacy'
| 'client.editionsCrossword';

type ManifestPath = `./manifest.${Build}.json`;

Expand Down Expand Up @@ -108,6 +109,9 @@ export const WEB = getScriptRegex('client.web');
export const WEB_VARIANT_SCRIPT = getScriptRegex('client.web.variant');
export const WEB_LEGACY_SCRIPT = getScriptRegex('client.web.legacy');
export const APPS_SCRIPT = getScriptRegex('client.apps');
export const EDITIONS_CROSSWORD_SCRIPT = getScriptRegex(
'client.editionsCrossword',
);

export const generateScriptTags = (scripts: string[]): string[] =>
scripts.filter(isString).map((script) => {
Expand All @@ -117,7 +121,8 @@ export const generateScriptTags = (scripts: string[]): string[] =>
if (
script.match(WEB) ??
script.match(WEB_VARIANT_SCRIPT) ??
script.match(APPS_SCRIPT)
script.match(APPS_SCRIPT) ??
script.match(EDITIONS_CROSSWORD_SCRIPT)
) {
return `<script type="module" src="${script}"></script>`;
}
Expand Down
3 changes: 1 addition & 2 deletions dotcom-rendering/src/model/editions-crossword-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,7 @@
"length",
"number",
"position",
"separatorLocations",
"solution"
"separatorLocations"
]
}
},
Expand Down
9 changes: 7 additions & 2 deletions dotcom-rendering/src/server/handler.editionsCrossword.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { RequestHandler } from 'express';
import { validateAsEditionsCrosswordType } from '../model/validate';
import { makePrefetchHeader } from './lib/header';
import { renderCrosswordHtml } from './render.editionsCrossword';

export const handleEditionsCrossword: RequestHandler = ({ body }, res) => {
const editionsCrosswords = validateAsEditionsCrosswordType(body);
console.log(editionsCrosswords);
res.sendStatus(200);
const { html, prefetchScripts } = renderCrosswordHtml({
editionsCrosswords,
});

res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html);
};
21 changes: 21 additions & 0 deletions dotcom-rendering/src/server/htmlCrosswordPageTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
interface Props {
html: string;
scriptTags: string[];
}

export const htmlCrosswordPageTemplate = (props: Props): string => {
const { html, scriptTags } = props;

return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<meta name="robots" content="noindex">
${scriptTags.join('\n')}
</head>
<body>
${html}
</body>
</html>`;
};
44 changes: 30 additions & 14 deletions dotcom-rendering/src/server/lib/get-content-from-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,40 @@ exports.parseURL = parseURL;

/** @type {import('webpack-dev-server').ExpressRequestHandler} */
exports.getContentFromURLMiddleware = async (req, res, next) => {
const sourceURL = parseURL(req.originalUrl);

if (sourceURL) {
if (
req.path.startsWith('/AMP') &&
sourceURL.hostname === 'www.theguardian.com'
) {
res.redirect(
req.path.replace('www.theguardian.com', 'amp.theguardian.com'),
);
}

if (req.path === '/EditionsCrossword') {
try {
req.body = await getContentFromURL(sourceURL, req.headers);
const url = new URL(
'https://www.theguardian.com/crosswords/digital-edition',
);
const content = await getContentFromURL(url, req.headers);
req.body = content;
next();
} catch (error) {
console.error(error);
next(error);
}
} else {
const sourceURL = parseURL(req.originalUrl);
if (sourceURL) {
if (
req.path.startsWith('/AMP') &&
sourceURL.hostname === 'www.theguardian.com'
) {
res.redirect(
req.path.replace(
'www.theguardian.com',
'amp.theguardian.com',
),
);
}

try {
req.body = await getContentFromURL(sourceURL, req.headers);
} catch (error) {
console.error(error);
next(error);
}
}
next();
}
next();
};
31 changes: 31 additions & 0 deletions dotcom-rendering/src/server/render.editionsCrossword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { isString } from '@guardian/libs';
import { EditionsCrosswordPage } from '../components/EditionsCrosswordPage';
import { generateScriptTags, getPathFromManifest } from '../lib/assets';
import { renderToStringWithEmotion } from '../lib/emotion';
import type { FEEditionsCrosswords } from '../types/editionsCrossword';
import { htmlCrosswordPageTemplate } from './htmlCrosswordPageTemplate';

interface Props {
editionsCrosswords: FEEditionsCrosswords;
}

export const renderCrosswordHtml = ({
editionsCrosswords,
}: Props): { html: string; prefetchScripts: string[] } => {
const { html } = renderToStringWithEmotion(
<EditionsCrosswordPage editionsCrosswords={editionsCrosswords} />,
);

const prefetchScripts = [
getPathFromManifest('client.editionsCrossword', 'index.js'),
].filter(isString);

const scriptTags = generateScriptTags(prefetchScripts);

const pageHtml = htmlCrosswordPageTemplate({
html,
scriptTags,
});

return { html: pageHtml, prefetchScripts };
};
2 changes: 1 addition & 1 deletion dotcom-rendering/src/types/editionsCrossword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type FECrosswordEntry = {
length: number;
clue: string;
group: string[];
solution: string;
solution?: string;
format?: string;
};

Expand Down
Loading

0 comments on commit 97c4202

Please sign in to comment.