diff --git a/dotcom-rendering/fixtures/manual/footballData.ts b/dotcom-rendering/fixtures/manual/footballData.ts index d8103fdb6da..7f6f2e4bd3e 100644 --- a/dotcom-rendering/fixtures/manual/footballData.ts +++ b/dotcom-rendering/fixtures/manual/footballData.ts @@ -1,4 +1,5 @@ import type { FootballMatches } from '../../src/footballMatches'; +import type { FootballMatch as FootballMatchV2 } from '../../src/footballMatchV2'; import type { Region } from '../../src/sportDataPage'; export const regions: Region[] = [ @@ -20,6 +21,26 @@ export const regions: Region[] = [ }, ]; +export const footballMatchResultV2: FootballMatchV2 = { + kind: 'Result', + kickOff: new Date('2022-01-01T11:11:00Z'), + paId: '4479251', + homeTeam: { + name: 'Germany', + paID: '7699', + score: 2, + scorers: ['Sjoeke Nusken 56 Pen', 'Lea Schuller 66'], + }, + awayTeam: { + name: 'Denmark', + paID: '35854', + score: 1, + scorers: ['Amalie Vangsgaard 26'], + }, + venue: 'St Jakob Park', + comment: undefined, +}; + export const initialDays: FootballMatches = [ { dateISOString: new Date('2022-01-01T00:00:00Z').toISOString(), diff --git a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx b/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx index e6b937e1b1a..ffd6aab3133 100644 --- a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx +++ b/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx @@ -86,7 +86,7 @@ const StatusLine = (props: { }} > {props.leagueName} - {props.match.venue} •{' '} + {props.match.venue ? `${props.match.venue} • ` : null}

); diff --git a/dotcom-rendering/src/components/FootballMatchInfoPage.stories.tsx b/dotcom-rendering/src/components/FootballMatchInfoPage.stories.tsx index a1ef6ee8a62..0dfce5c6247 100644 --- a/dotcom-rendering/src/components/FootballMatchInfoPage.stories.tsx +++ b/dotcom-rendering/src/components/FootballMatchInfoPage.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-webpack5'; import { allModes } from '../../.storybook/modes'; +import { footballMatchResultV2 } from '../../fixtures/manual/footballData'; import { table } from '../../fixtures/manual/footballTable'; import { matchStats } from '../../fixtures/manual/matchStats'; import { FootballMatchInfoPage as FootballMatchInfoPageComponent } from './FootballMatchInfoPage'; @@ -22,6 +23,9 @@ type Story = StoryObj; export const FootballMatchInfoPage = { args: { matchStats, + matchInfo: footballMatchResultV2, table, + competitionName: "Women's Euro 2025", + edition: 'UK', }, } satisfies Story; diff --git a/dotcom-rendering/src/components/FootballMatchInfoPage.tsx b/dotcom-rendering/src/components/FootballMatchInfoPage.tsx index 7ebcebbc733..263e1b3f3b2 100644 --- a/dotcom-rendering/src/components/FootballMatchInfoPage.tsx +++ b/dotcom-rendering/src/components/FootballMatchInfoPage.tsx @@ -1,35 +1,62 @@ import { css } from '@emotion/react'; -import { from } from '@guardian/source/foundations'; +import { from, space } from '@guardian/source/foundations'; import { type FootballMatchStats } from '../footballMatchStats'; +import { type FootballMatch } from '../footballMatchV2'; import { type FootballTableSummary } from '../footballTables'; import { grid } from '../grid'; +import { type EditionId } from '../lib/edition'; import { palette } from '../palette'; +import { FootballMatchHeader } from './FootballMatchHeader/FootballMatchHeader'; import { FootballMatchInfo } from './FootballMatchInfo'; export const FootballMatchInfoPage = ({ matchStats, + matchInfo, + competitionName, + edition, table, }: { matchStats: FootballMatchStats; + matchInfo: FootballMatch; + competitionName: string; + edition: EditionId; table?: FootballTableSummary; }) => { return ( -
-
- +
+ +
+
+ +
); }; -const gridStyles = css` +const bodyGridStyles = css` ${grid.paddedContainer} position: relative; + padding-top: ${space[4]}px; + padding-bottom: ${space[8]}px; ${from.tablet} { + padding-top: ${space[2]}px; + padding-bottom: ${space[14]}px; &::before, &::after { content: ''; diff --git a/dotcom-rendering/src/footballMatch.ts b/dotcom-rendering/src/footballMatch.ts index c9c97292bab..9fa25fca96a 100644 --- a/dotcom-rendering/src/footballMatch.ts +++ b/dotcom-rendering/src/footballMatch.ts @@ -1,7 +1,7 @@ import { isOneOf } from '@guardian/libs'; import { listParse, replaceLiveMatchStatus } from './footballMatches'; import type { - FEFootballMatch, + FEFootballMatchStats, FEFootballPlayer, FEFootballPlayerEvent, FEFootballTeam, @@ -113,7 +113,7 @@ const parseTeam = ( })); export const parse = ( - feFootballMatch: FEFootballMatch, + feFootballMatch: FEFootballMatchStats, ): Result => parseTeam(feFootballMatch.homeTeam).flatMap((homeTeam) => parseTeam(feFootballMatch.awayTeam).map((awayTeam) => ({ diff --git a/dotcom-rendering/src/footballMatchStats.ts b/dotcom-rendering/src/footballMatchStats.ts index 060cfd6e910..2442f9c76eb 100644 --- a/dotcom-rendering/src/footballMatchStats.ts +++ b/dotcom-rendering/src/footballMatchStats.ts @@ -2,7 +2,7 @@ import { isOneOf } from '@guardian/libs'; import { listParse } from './footballMatches'; import type { FootballTeam } from './footballTeam'; import type { - FEFootballMatch, + FEFootballMatchStats, FEFootballPlayer, FEFootballPlayerEvent, FEFootballTeam, @@ -141,7 +141,7 @@ const parseTeamWithStats = ( })); export const parseMatchStats = ( - feFootballMatch: FEFootballMatch, + feFootballMatch: FEFootballMatchStats, ): Result => parseTeamWithStats(feFootballMatch.homeTeam).flatMap((homeTeam) => parseTeamWithStats(feFootballMatch.awayTeam).map((awayTeam) => ({ diff --git a/dotcom-rendering/src/footballMatchV2.ts b/dotcom-rendering/src/footballMatchV2.ts index b8865d47150..0e7849897bf 100644 --- a/dotcom-rendering/src/footballMatchV2.ts +++ b/dotcom-rendering/src/footballMatchV2.ts @@ -1,4 +1,15 @@ +import { isUndefined } from '@guardian/libs'; +import { replaceLiveMatchStatus } from './footballMatches'; import type { FootballTeam } from './footballTeam'; +import type { + FEFixture, + FEFootballMatch, + FEMatchDay, + FEResult, +} from './frontend/feFootballMatchListPage'; +import { oneOf, parseDate } from './lib/parse'; +import { error, type Result } from './lib/result'; +import { cleanTeamName } from './sportDataPage'; /** * There are three states a football match can be in. @@ -50,7 +61,7 @@ export type MatchResult = MatchData & { type MatchData = { paId: string; kickOff: Date; - venue: string; + venue?: string; }; /** @@ -61,3 +72,227 @@ type FootballMatchTeamWithScore = FootballTeam & { score: number; scorers: string[]; }; + +type FootballDayInvalidDate = { + kind: 'FootballDayInvalidDate'; + message: string; +}; + +type ExpectedMatchDayFixture = { + kind: 'ExpectedMatchDayFixture'; + message: string; +}; + +type FootballMatchInvalidDate = { + kind: 'FootballMatchInvalidDate'; + message: string; +}; + +type ExpectedMatchDayResult = { + kind: 'ExpectedMatchDayResult'; + message: string; +}; + +type ExpectedMatchDayLive = { + kind: 'ExpectedMatchDayLive'; + message: string; +}; + +type InvalidMatchDay = { + kind: 'InvalidMatchDay'; + errors: Array; +}; + +type UnexpectedLiveMatch = { + kind: 'UnexpectedLiveMatch'; + message: string; +}; + +type ParserError = + | FootballDayInvalidDate + | ExpectedMatchDayFixture + | FootballMatchInvalidDate + | ExpectedMatchDayResult + | ExpectedMatchDayLive + | InvalidMatchDay + | UnexpectedLiveMatch; + +const parseMatchDate = (date: string): Result => { + // Frontend appends a timezone in square brackets + const isoDate = date.split('[')[0]; + + if (isUndefined(isoDate)) { + return error( + `Expected a match date with a timezone appended in square brackets at the end, but instead got ${date}`, + ); + } + + return parseDate(isoDate); +}; + +const parseFixture = ( + feFixture: FEFixture | FEMatchDay, +): Result => { + if ( + feFixture.type === 'MatchDay' && + (feFixture.result || feFixture.liveMatch) + ) { + return error({ + kind: 'ExpectedMatchDayFixture', + message: `A fixture match day must not have 'liveMatch' or 'result' set to 'true', but this was not the case for ${feFixture.id}`, + }); + } + + return parseMatchDate(feFixture.date) + .mapError((message) => ({ + kind: 'FootballMatchInvalidDate', + message, + })) + .map((dateTimeISOString) => ({ + kind: 'Fixture', + paId: feFixture.id, + kickOff: dateTimeISOString, + venue: feFixture.venue?.name, + homeTeam: { + name: cleanTeamName(feFixture.homeTeam.name), + paID: feFixture.homeTeam.id, + }, + awayTeam: { + name: cleanTeamName(feFixture.awayTeam.name), + paID: feFixture.awayTeam.id, + }, + })); +}; + +const parseMatchResult = ( + feResult: FEResult | FEMatchDay, +): Result => { + if (feResult.type === 'MatchDay' && !feResult.result) { + return error({ + kind: 'ExpectedMatchDayResult', + message: `A result match day must have 'result' set to 'true', but this was not the case for ${feResult.id}`, + }); + } + + if ( + feResult.homeTeam.score === undefined || + feResult.awayTeam.score === undefined + ) { + return error({ + kind: 'ExpectedMatchDayResult', + message: `A result match must have scores for both teams, but this was not the case for ${feResult.id}`, + }); + } + + // Extract validated values so TypeScript can narrow the types + const homeScore = feResult.homeTeam.score; + const awayScore = feResult.awayTeam.score; + + return parseMatchDate(feResult.date) + .mapError((message) => ({ + kind: 'FootballMatchInvalidDate', + message, + })) + .map((dateTimeISOString) => ({ + kind: 'Result', + paId: feResult.id, + kickOff: dateTimeISOString, + venue: feResult.venue?.name, + homeTeam: { + name: cleanTeamName(feResult.homeTeam.name), + paID: feResult.homeTeam.id, + score: homeScore, + scorers: feResult.homeTeam.scorers?.split(',') ?? [], + }, + awayTeam: { + name: cleanTeamName(feResult.awayTeam.name), + paID: feResult.awayTeam.id, + score: awayScore, + scorers: feResult.awayTeam.scorers?.split(',') ?? [], + }, + comment: feResult.comments, + })); +}; + +const parseLiveMatch = ( + feMatchDay: FEMatchDay, +): Result => { + if (!feMatchDay.liveMatch) { + return error({ + kind: 'ExpectedMatchDayLive', + message: `A live match day must have 'liveMatch' set to 'true', but this was not the case for ${feMatchDay.id}`, + }); + } + + if ( + feMatchDay.homeTeam.score === undefined || + feMatchDay.awayTeam.score === undefined + ) { + return error({ + kind: 'ExpectedMatchDayLive', + message: `A live match must have scores for both teams, but this was not the case for ${feMatchDay.id}`, + }); + } + + // Extract validated values so TypeScript can narrow the types + const homeScore = feMatchDay.homeTeam.score; + const awayScore = feMatchDay.awayTeam.score; + + return parseMatchDate(feMatchDay.date) + .mapError((message) => ({ + kind: 'FootballMatchInvalidDate', + message, + })) + .map((dateTimeISOString) => ({ + kind: 'Live', + paId: feMatchDay.id, + kickOff: dateTimeISOString, + venue: feMatchDay.venue?.name, + homeTeam: { + name: cleanTeamName(feMatchDay.homeTeam.name), + paID: feMatchDay.homeTeam.id, + score: homeScore, + scorers: feMatchDay.homeTeam.scorers?.split(',') ?? [], + }, + awayTeam: { + name: cleanTeamName(feMatchDay.awayTeam.name), + paID: feMatchDay.awayTeam.id, + score: awayScore, + scorers: feMatchDay.awayTeam.scorers?.split(',') ?? [], + }, + dateTimeISOString, + comment: cleanTeamName(feMatchDay.comments ?? ''), + status: replaceLiveMatchStatus(feMatchDay.matchStatus), + })); +}; + +const parseMatchDay = ( + feMatchDay: FEMatchDay, +): Result => + oneOf([ + parseLiveMatch, + parseMatchResult, + parseFixture, + ])(feMatchDay).mapError((errors) => ({ + kind: 'InvalidMatchDay', + errors, + })); + +export const parseFootballMatchV2 = ( + feMatch: FEFootballMatch, +): Result => { + switch (feMatch.type) { + case 'Fixture': + return parseFixture(feMatch); + case 'Result': + return parseMatchResult(feMatch); + case 'MatchDay': + return parseMatchDay(feMatch); + case 'LiveMatch': + return error({ + kind: 'UnexpectedLiveMatch', + message: + "Did not expect to get a match with type 'LiveMatch', allowed options are 'MatchDay', 'Fixture' or 'Result'", + }); + } +}; diff --git a/dotcom-rendering/src/frontend/feFootballMatchPage.ts b/dotcom-rendering/src/frontend/feFootballMatchPage.ts index 975456e8791..009a3ac2920 100644 --- a/dotcom-rendering/src/frontend/feFootballMatchPage.ts +++ b/dotcom-rendering/src/frontend/feFootballMatchPage.ts @@ -1,4 +1,5 @@ import type { FEFootballDataPage } from './feFootballDataPage'; +import { type FEFootballMatch } from './feFootballMatchListPage'; import { type FEGroupSummary } from './feFootballTablesPage'; export type FEFootballPlayerEvent = { @@ -33,7 +34,7 @@ export type FEFootballTeam = { crest: string; }; -export type FEFootballMatch = { +export type FEFootballMatchStats = { id: string; homeTeam: FEFootballTeam; awayTeam: FEFootballTeam; @@ -42,6 +43,12 @@ export type FEFootballMatch = { }; export type FEFootballMatchPage = FEFootballDataPage & { - footballMatch: FEFootballMatch; + // This field name will need to get changed to matchStats in the future PRs. + // Since this change needs to happen in both frontend and DCAR, and it also + // needs to be backward compatible for a temprary duration, we will handle + // that in a separate PR. + footballMatch: FEFootballMatchStats; + matchInfo: FEFootballMatch; group?: FEGroupSummary; + competitionName: string; }; diff --git a/dotcom-rendering/src/frontend/schemas/feFootballMatchPage.json b/dotcom-rendering/src/frontend/schemas/feFootballMatchPage.json index fdc9d4a4d27..6180816bdb6 100644 --- a/dotcom-rendering/src/frontend/schemas/feFootballMatchPage.json +++ b/dotcom-rendering/src/frontend/schemas/feFootballMatchPage.json @@ -295,6 +295,9 @@ "status" ] }, + "matchInfo": { + "$ref": "#/definitions/FEFootballMatch" + }, "group": { "type": "object", "properties": { @@ -473,10 +476,15 @@ "entries", "round" ] + }, + "competitionName": { + "type": "string" } }, "required": [ - "footballMatch" + "competitionName", + "footballMatch", + "matchInfo" ] } ], @@ -1045,6 +1053,682 @@ "text", "url" ] + }, + "FEFootballMatch": { + "anyOf": [ + { + "$ref": "#/definitions/FELive" + }, + { + "$ref": "#/definitions/FEFixture" + }, + { + "$ref": "#/definitions/FEMatchDay" + }, + { + "$ref": "#/definitions/FEResult" + } + ] + }, + "FELive": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "stage": { + "type": "object", + "properties": { + "stageNumber": { + "type": "string" + } + }, + "required": [ + "stageNumber" + ] + }, + "round": { + "type": "object", + "properties": { + "roundNumber": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "roundNumber" + ] + }, + "leg": { + "type": "string" + }, + "homeTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "awayTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "venue": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "comments": { + "type": "string" + } + }, + "required": [ + "awayTeam", + "date", + "homeTeam", + "id", + "leg", + "round", + "stage" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "LiveMatch" + }, + "status": { + "type": "string" + }, + "attendance": { + "type": "string" + }, + "referee": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + }, + "required": [ + "status", + "type" + ] + } + ] + }, + "FEFixture": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "stage": { + "type": "object", + "properties": { + "stageNumber": { + "type": "string" + } + }, + "required": [ + "stageNumber" + ] + }, + "round": { + "type": "object", + "properties": { + "roundNumber": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "roundNumber" + ] + }, + "leg": { + "type": "string" + }, + "homeTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "awayTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "venue": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "comments": { + "type": "string" + } + }, + "required": [ + "awayTeam", + "date", + "homeTeam", + "id", + "leg", + "round", + "stage" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "Fixture" + }, + "competition": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "FEMatchDay": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "stage": { + "type": "object", + "properties": { + "stageNumber": { + "type": "string" + } + }, + "required": [ + "stageNumber" + ] + }, + "round": { + "type": "object", + "properties": { + "roundNumber": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "roundNumber" + ] + }, + "leg": { + "type": "string" + }, + "homeTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "awayTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "venue": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "comments": { + "type": "string" + } + }, + "required": [ + "awayTeam", + "date", + "homeTeam", + "id", + "leg", + "round", + "stage" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "MatchDay" + }, + "liveMatch": { + "type": "boolean" + }, + "result": { + "type": "boolean" + }, + "previewAvailable": { + "type": "boolean" + }, + "reportAvailable": { + "type": "boolean" + }, + "lineupsAvailable": { + "type": "boolean" + }, + "matchStatus": { + "type": "string" + }, + "attendance": { + "type": "string" + }, + "referee": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "competition": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + }, + "required": [ + "lineupsAvailable", + "liveMatch", + "matchStatus", + "previewAvailable", + "reportAvailable", + "result", + "type" + ] + } + ] + }, + "FEResult": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "stage": { + "type": "object", + "properties": { + "stageNumber": { + "type": "string" + } + }, + "required": [ + "stageNumber" + ] + }, + "round": { + "type": "object", + "properties": { + "roundNumber": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "roundNumber" + ] + }, + "leg": { + "type": "string" + }, + "homeTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "awayTeam": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "score": { + "type": "number" + }, + "htScore": { + "type": "number" + }, + "aggregateScore": { + "type": "number" + }, + "scorers": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "venue": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "comments": { + "type": "string" + } + }, + "required": [ + "awayTeam", + "date", + "homeTeam", + "id", + "leg", + "round", + "stage" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "Result" + }, + "reportAvailable": { + "type": "boolean" + }, + "attendance": { + "type": "string" + }, + "referee": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + }, + "required": [ + "reportAvailable", + "type" + ] + } + ] } }, "$schema": "http://json-schema.org/draft-07/schema#" diff --git a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx index 159fd61a2e7..8e71f8d9266 100644 --- a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx +++ b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx @@ -78,6 +78,9 @@ const SportsPage = ({ return ( ); diff --git a/dotcom-rendering/src/lib/parse.ts b/dotcom-rendering/src/lib/parse.ts index 11db6629dae..285423afd08 100644 --- a/dotcom-rendering/src/lib/parse.ts +++ b/dotcom-rendering/src/lib/parse.ts @@ -9,3 +9,40 @@ export const parseIntResult = (int: string): Result => { return ok(parsed); }; + +export const parseDate = (a: string): Result => { + const d = new Date(a); + + if (d.toString() === 'Invalid Date') { + return error(`${a} isn't a valid Date`); + } + + return ok(d); +}; + +export type Parser = (a: A) => Result; + +export const oneOf = + (parsers: [Parser, ...Array>]) => + (input: A): Result => { + const f = ( + remainingParsers: Array>, + errs: E[], + ): Result => { + const [head, ...tail] = remainingParsers; + + if (head === undefined) { + return error(errs); + } + + const result = head(input); + + if (!result.ok) { + return f(tail, [...errs, result.error]); + } + + return ok(result.value); + }; + + return f(parsers, []); + }; diff --git a/dotcom-rendering/src/server/handler.sportDataPage.web.ts b/dotcom-rendering/src/server/handler.sportDataPage.web.ts index 7be586a511f..8faba0d1aa2 100644 --- a/dotcom-rendering/src/server/handler.sportDataPage.web.ts +++ b/dotcom-rendering/src/server/handler.sportDataPage.web.ts @@ -6,6 +6,7 @@ import { parse as parseFootballMatches, } from '../footballMatches'; import { parseMatchStats } from '../footballMatchStats'; +import { parseFootballMatchV2 } from '../footballMatchV2'; import { parse as parseFootballTables, parseTableSummary, @@ -223,6 +224,7 @@ const parseFEFootballMatch = ( } const parsedFootballMatchStats = parseMatchStats(data.footballMatch); + const matchInfo = parseFootballMatchV2(data.matchInfo); if (!parsedFootballMatchStats.ok) { throw new Error( @@ -230,6 +232,13 @@ const parseFEFootballMatch = ( ); } + if (!matchInfo.ok) { + const aggeregatedErrors = getParserErrorMessage(matchInfo.error); + throw new Error( + `Failed to parse football match info: ${matchInfo.error.kind} ${aggeregatedErrors}`, + ); + } + const group = data.group && parseTableSummary(data.group); if (group && !group.ok) { @@ -241,6 +250,8 @@ const parseFEFootballMatch = ( return { match: parsedFootballMatch.value, matchStats: parsedFootballMatchStats.value, + matchInfo: matchInfo.value, + competitionName: data.competitionName, group: group?.value, kind: 'FootballMatchSummary', nav: { diff --git a/dotcom-rendering/src/sportDataPage.ts b/dotcom-rendering/src/sportDataPage.ts index c96d8001012..b840db973e7 100644 --- a/dotcom-rendering/src/sportDataPage.ts +++ b/dotcom-rendering/src/sportDataPage.ts @@ -2,6 +2,7 @@ import type { CricketMatch } from './cricketMatch'; import type { FootballMatch } from './footballMatch'; import type { FootballMatches } from './footballMatches'; import { type FootballMatchStats } from './footballMatchStats'; +import { type FootballMatch as FootballMatchV2 } from './footballMatchV2'; import type { FootballTableCompetitions, FootballTableSummary, @@ -53,7 +54,9 @@ export type CricketMatchPage = SportPageConfig & { export type FootballMatchSummaryPage = SportPageConfig & { match: FootballMatch; matchStats: FootballMatchStats; + matchInfo: FootballMatchV2; group?: FootballTableSummary; + competitionName: string; kind: 'FootballMatchSummary'; };