Skip to content

Commit

Permalink
WIP: add football live page
Browse files Browse the repository at this point in the history
  • Loading branch information
marjisound committed Dec 17, 2024
1 parent 5a69638 commit 6adc8fb
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 1 deletion.
1 change: 1 addition & 0 deletions dotcom-rendering/configs/webpack/server.dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const devServer = {
},
},
setupMiddlewares: (middlewares, { app, compiler }) => {
console.log('marji 2: ');
if (!app) {
throw new Error('webpack-dev-server is not defined');
}
Expand Down
1 change: 1 addition & 0 deletions dotcom-rendering/scripts/json-schema/gen-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const tagPageSchema = getTagPageSchema();
const newsletterPageSchema = getNewsletterPageSchema();
const blockSchema = getBlockSchema();
const editionsCrosswordSchema = getEditionsCrosswordSchema();
const sportsSchema = getArticleSchema();

fs.writeFile(
`${root}/src/model/article-schema.json`,
Expand Down
8 changes: 8 additions & 0 deletions dotcom-rendering/scripts/json-schema/get-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ const getArticleSchema = () => {
);
};

const getSportsSchema = () => {
return JSON.stringify(
TJS.generateSchema(program, 'FELiveScoresType', settings),
null,
4,
);
};

const getFrontSchema = () => {
return JSON.stringify(
TJS.generateSchema(program, 'FEFrontType', settings),
Expand Down
48 changes: 48 additions & 0 deletions dotcom-rendering/src/components/Football/LiveScoresPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { FELiveScoresType } from 'src/types/sports';
import { css } from '@emotion/react';
import { MatchList } from './MatchList';

interface Props {
liveScores: FELiveScoresType;
}

const sportsPageStyles = css`
padding-top: 0;
padding-bottom: 2.25rem;
`;

const titleStyles = css`
font-size: 1.25rem;
line-height: 1.4375rem;
font-family: 'Guardian Egyptian Web', Georgia, serif;
font-weight: 900;
box-sizing: border-box;
padding: 0.375rem 0 0.75rem;
border-top: 0.0625rem dotted;
`;

const matchContainerStyles = css`
clear: both;
`;

export const LiveScoresPage = ({ liveScores }: Props) => {
return (
<article id="article" css={[sportsPageStyles]} role="main">
<div>
<h2 css={[titleStyles]}>{liveScores.pageTitle}</h2>
<div
css={[matchContainerStyles]}
data-show-more-contains="football-matches"
>
{liveScores.matchesGroupedByDateAndCompetition.map(
(item) => {
return (
<MatchList dateCompetition={item}></MatchList>
);
},
)}
</div>
</div>
</article>
);
};
48 changes: 48 additions & 0 deletions dotcom-rendering/src/components/Football/MatchList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DateCompetitionMatch } from 'src/types/sports';

interface Props {
dateCompetition: DateCompetitionMatch;
}

export const MatchList: React.FC<Props> = ({ dateCompetition }) => {
return (
<>
<div>{dateCompetition.date}</div>
{dateCompetition.competitions.map((comp) => (
<div key={comp.competition.id}>
<h3>{comp.competition.fullName}</h3>
<table>
<thead hidden>
<tr>
<th>Match status / kick off time</th>
<th>Match details</th>
</tr>
</thead>
<tbody>
{comp.matches.map((match) => (
<tr key={match.id} id={match.id}>
<td>{match.date.toString()}</td>
<td>
<strong>{match.homeTeam.name}</strong>{' '}
vs{' '}
<strong>{match.awayTeam.name}</strong>
{match.type === 'MatchDay' &&
match.liveMatch && (
<span> (Live!)</span>
)}
{match.type === 'Fixture' && (
<span> (Fixture)</span>
)}
{match.type === 'Result' && (
<span> (Result)</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</>
);
};
12 changes: 12 additions & 0 deletions dotcom-rendering/src/model/sports-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"pageTitle": {
"type": "string"
}
},
"required": [
"pageTitle"
],
"$schema": "http://json-schema.org/draft-07/schema#"
}
14 changes: 14 additions & 0 deletions dotcom-rendering/src/model/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import type { FETagPageType } from '../types/tagPage';
import articleSchema from './article-schema.json';
import blockSchema from './block-schema.json';
import editionsCrosswordSchema from './editions-crossword-schema.json';
import sportSchema from './sports-schema.json';
import frontSchema from './front-schema.json';
import newslettersPageSchema from './newsletter-page-schema.json';
import tagPageSchema from './tag-page-schema.json';
import { FELiveScoresType } from 'src/types/sports';

const options: Options = {
verbose: false,
Expand All @@ -36,6 +38,8 @@ const validateEditionsCrossword = ajv.compile<FEEditionsCrosswords>(
editionsCrosswordSchema,
);

const validateSports = ajv.compile<FELiveScoresType>(sportSchema);

export const validateAsArticleType = (data: unknown): FEArticleType => {
if (validateArticle(data)) return data;

Expand All @@ -60,6 +64,16 @@ export const validateAsEditionsCrosswordType = (
);
};

export const validateAsSports = (data: unknown): FELiveScoresType => {
if (validateSports(data)) {
return data;
}
throw new TypeError(
`Unable to validate request body for editions crosswords.\n
${JSON.stringify(validateSports.errors, null, 2)}`,
);
};

export const validateAsFrontType = (data: unknown): FEFrontType => {
if (validateFront(data)) return data;

Expand Down
16 changes: 16 additions & 0 deletions dotcom-rendering/src/server/handler.sports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { RequestHandler } from 'express';
import { validateAsSports } from '../model/validate';
import { renderSportsHtml } from './render.sports.web';
import { makePrefetchHeader } from './lib/header';

export const handleSports: RequestHandler = ({ body }, res) => {
console.log(`marji: `);
const matchList = validateAsSports(body);
console.log(matchList);

const { html, prefetchScripts } = renderSportsHtml({
sports: matchList,
});

res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html);
};
9 changes: 8 additions & 1 deletion dotcom-rendering/src/server/lib/get-content-from-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ async function getContentFromURL(url, _headers) {
.filter(isStringTuple),
);

console.log(`fetch url: ${jsonUrl}`);

// pick all the keys from the JSON except `html`
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we don't want `html` in the config
const { html, ...config } = await fetch(jsonUrl, { headers })
.then((response) => response.json())
.then((response) => {
console.log(response);

return response.json();
})
.catch((error) => {
console.log(error);
if (error?.type === 'invalid-json') {
throw new Error(
'Did not receive JSON response - are you sure this URL supports .json?dcr requests?',
Expand Down
33 changes: 33 additions & 0 deletions dotcom-rendering/src/server/render.sports.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FELiveScoresType } from '../types/sports';
import { LiveScoresPage } from '../components/Football/LiveScoresPage';
import { renderToStringWithEmotion } from '../lib/emotion';
import { getPathFromManifest } from '../lib/assets';
import { polyfillIO } from '../lib/polyfill.io';
import { isString } from '@guardian/libs';

interface Props {
sports: FELiveScoresType;
}

export const renderSportsHtml = ({
sports,
}: Props): { html: string; prefetchScripts: string[] } => {
const { html } = renderToStringWithEmotion(
<LiveScoresPage liveScores={sports} />,
);

/**
* The highest priority scripts.
* These scripts have a considerable impact on site performance.
* Only scripts critical to application execution may go in here.
* Please talk to the dotcom platform team before adding more.
* Scripts will be executed in the order they appear in this array
*/
const prefetchScripts = [
polyfillIO,
getPathFromManifest('client.web', 'frameworks.js'),
getPathFromManifest('client.web', 'index.js'),
].filter(isString);

return { html: html, prefetchScripts };
};
6 changes: 6 additions & 0 deletions dotcom-rendering/src/server/server.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
handleTagPage,
handleTagPageJson,
} from './handler.front.web';
import { handleSports } from './handler.sports';

/** article URLs contain a part that looks like “2022/nov/25” */
const ARTICLE_URL = /\/\d{4}\/[a-z]{3}\/\d{2}\//;
Expand Down Expand Up @@ -52,8 +53,11 @@ const editionalisefront = (url: string): string => {
// for more info
export const devServer = (): Handler => {
return (req, res, next) => {
console.log(`req.path: ${req.path}`);
const path = req.path.split('/')[1];

console.log(`path: ${path}`);

// handle urls with the ?url=… query param
const sourceUrl = req.url.split('?url=')[1];
if (path && sourceUrl) {
Expand Down Expand Up @@ -91,6 +95,8 @@ export const devServer = (): Handler => {
return handleAppsBlocks(req, res, next);
case 'EditionsCrossword':
return handleEditionsCrossword(req, res, next);
case 'Sports':
return handleSports(req, res, next);
default: {
// Do not redirect assets urls
if (req.url.match(ASSETS_URL)) return next();
Expand Down
118 changes: 118 additions & 0 deletions dotcom-rendering/src/types/sports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
type Round = {
roundNumber: string;
name?: string;
};
type Stage = {
stageNumber: String;
};

type Venue = {
id: string;
name: string;
};

type MatchDayTeam = {
id: string;
name: string;
score?: number;
htScore?: number;
aggregateScore?: number;
scorers?: string;
};

type FootballMatch = {
id: string;
date: Date;
stage: Stage;
round: Round;
leg: string;
homeTeam: MatchDayTeam;
awayTeam: MatchDayTeam;
venue?: Venue;
comments?: string;
};

type Fixture = FootballMatch & {
type: 'Fixture';
competition?: Competition;
};

type MatchDay = FootballMatch & {
type: 'MatchDay';
liveMatch: boolean;
result: boolean;
previewAvailable: boolean;
reportAvailable: boolean;
lineupsAvailable: boolean;
matchStatus: string;
attendance?: string;
referee?: string;
};

type Result = FootballMatch & {
type: 'Result';
reportAvailable: boolean;
attendance?: string;
referee?: string;
};

type LiveMatch = FootballMatch & {
type: 'LiveMatch';
status: string;
attendance?: string;
referee?: string;
};

type FootballMatchType = Fixture | MatchDay | Result | LiveMatch;

type LeagueStats = {
played: number;
won: number;
drawn: number;
lost: number;
goalsFor: number;
goalsAgainst: number;
};

type LeagueTeam = {
id: string;
name: string;
rank: number;
total: LeagueStats;
home: LeagueStats;
away: LeagueStats;
goalDifference: number;
points: number;
};

type LeagueTableEntry = { stageNumber: string; round: Round; team: LeagueTeam };

type Competition = {
id: string;
url: string;
fullName: string;
shortName: string;
nation: string;
startDate?: string;
matches: FootballMatch[];
leagueTable: LeagueTableEntry[];
showInTeamsList: boolean;
tableDividers: number[];
finalMatchSVG?: string;
};

type CompetitionMatch = {
competition: Competition;
matches: FootballMatchType[];
};

export type DateCompetitionMatch = {
date: string;
competitions: CompetitionMatch[];
};

export type FELiveScoresType = {
pageTitle: string;
type: string;
matchesGroupedByDateAndCompetition: DateCompetitionMatch[];
};

0 comments on commit 6adc8fb

Please sign in to comment.