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';
};