diff --git a/dotcom-rendering/.storybook/decorators/gridDecorators.tsx b/dotcom-rendering/.storybook/decorators/gridDecorators.tsx
index ea457589605..5c118f2e2a2 100644
--- a/dotcom-rendering/.storybook/decorators/gridDecorators.tsx
+++ b/dotcom-rendering/.storybook/decorators/gridDecorators.tsx
@@ -1,6 +1,6 @@
import { css } from '@emotion/react';
import type { Decorator } from '@storybook/react';
-import { grid } from './grid';
+import { grid } from '../../src/grid';
import { from } from '@guardian/source/foundations';
/**
diff --git a/dotcom-rendering/src/components/FootballLiveMatches.stories.tsx b/dotcom-rendering/src/components/FootballLiveMatches.stories.tsx
index d17078c0bed..7039182666d 100644
--- a/dotcom-rendering/src/components/FootballLiveMatches.stories.tsx
+++ b/dotcom-rendering/src/components/FootballLiveMatches.stories.tsx
@@ -1,11 +1,18 @@
import type { Meta, StoryObj } from '@storybook/react';
-import { centreColumnDecorator } from '../../.storybook/decorators/gridDecorators';
import { FootballLiveMatches as FootballLiveMatchesComponent } from './FootballLiveMatches';
const meta = {
title: 'Components/Football Live Matches',
component: FootballLiveMatchesComponent,
- decorators: [centreColumnDecorator],
+ decorators: [
+ // To make it easier to see the top border above the date
+ (Story) => (
+ <>
+
+
+ >
+ ),
+ ],
} satisfies Meta;
export default meta;
@@ -14,7 +21,8 @@ type Story = StoryObj;
export const FootballLiveMatches = {
args: {
- matches: [
+ edition: 'UK',
+ days: [
{
date: new Date('2025-01-24T00:00:00Z'),
competitions: [
@@ -24,11 +32,11 @@ export const FootballLiveMatches = {
nation: 'European',
matches: [
{
- dateTime: new Date('2025-01-24T19:45:00Z'),
+ dateTime: new Date('2025-01-24T11:11:00Z'),
paId: '4482093',
homeTeam: {
name: 'Torino',
- score: 1,
+ score: 10,
},
awayTeam: {
name: 'Cagliari',
diff --git a/dotcom-rendering/src/components/FootballLiveMatches.tsx b/dotcom-rendering/src/components/FootballLiveMatches.tsx
index e04f1003201..9afe14f3924 100644
--- a/dotcom-rendering/src/components/FootballLiveMatches.tsx
+++ b/dotcom-rendering/src/components/FootballLiveMatches.tsx
@@ -1,36 +1,247 @@
-import { Fragment } from 'react';
+import { css } from '@emotion/react';
+import {
+ from,
+ headlineBold17,
+ space,
+ textSans14,
+ textSansBold14,
+ until,
+} from '@guardian/source/foundations';
+import { Fragment, type ReactNode } from 'react';
import type { FootballMatches } from '../footballMatches';
+import { grid } from '../grid';
+import {
+ type EditionId,
+ getLocaleFromEdition,
+ getTimeZoneFromEdition,
+} from '../lib/edition';
+import { palette } from '../palette';
type Props = {
- matches: FootballMatches;
+ days: FootballMatches;
+ edition: EditionId;
};
-export const FootballLiveMatches = ({ matches }: Props) => (
- <>
- {matches.map((day) => (
-
- {day.date.toString()}
- {day.competitions.map((competition) => (
-
- {competition.name}
-
- {competition.matches.map((match) => (
-
-
- {match.dateTime.getUTCHours()}:
- {match.dateTime.getUTCMinutes()}
-
- {match.homeTeam.name} {match.homeTeam.score}
- -{match.awayTeam.score}{' '}
- {match.awayTeam.name}
-
- ))}
-
-
- ))}
-
- ))}
- >
+const getDateFormatter = (edition: EditionId): Intl.DateTimeFormat =>
+ new Intl.DateTimeFormat('en-GB', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ timeZone: getTimeZoneFromEdition(edition),
+ });
+
+const getTimeFormatter = (edition: EditionId): Intl.DateTimeFormat =>
+ new Intl.DateTimeFormat(getLocaleFromEdition(edition), {
+ hour: '2-digit',
+ minute: '2-digit',
+ timeZoneName: 'short',
+ hour12: false,
+ timeZone: getTimeZoneFromEdition(edition),
+ });
+
+const Day = (props: { children: ReactNode }) => (
+
+ {props.children}
+
+);
+
+const CompetitionName = (props: { children: ReactNode }) => (
+
+ {props.children}
+
+);
+
+const Matches = (props: { children: ReactNode }) => (
+
+);
+
+const Match = (props: { children: ReactNode }) => (
+
+);
+
+const MatchTime = (props: { children: ReactNode; dateTime: string }) => (
+
+);
+
+const HomeTeam = (props: { children: ReactNode }) => (
+
+);
+
+const AwayTeam = (props: { children: ReactNode }) => (
+
+);
+
+const Battleline = () => (
+
+);
+
+const Scores = ({
+ homeScore,
+ awayScore,
+}: {
+ homeScore: number;
+ awayScore: number;
+}) => (
+
+
+ {homeScore}
+
+
+
+ {awayScore}
+
+
);
+
+export const FootballLiveMatches = ({ edition, days }: Props) => {
+ const dateFormatter = getDateFormatter(edition);
+ const timeFormatter = getTimeFormatter(edition);
+
+ return (
+ <>
+ {days.map((day) => (
+
+ {dateFormatter.format(day.date)}
+ {day.competitions.map((competition) => (
+
+
+ {competition.name}
+
+
+ {competition.matches.map((match) => (
+
+
+ {timeFormatter.format(
+ match.dateTime,
+ )}
+
+
+ {match.homeTeam.name}
+
+
+
+ {match.awayTeam.name}
+
+
+ ))}
+
+
+ ))}
+
+ ))}
+ >
+ );
+};
diff --git a/dotcom-rendering/.storybook/decorators/grid.ts b/dotcom-rendering/src/grid.ts
similarity index 92%
rename from dotcom-rendering/.storybook/decorators/grid.ts
rename to dotcom-rendering/src/grid.ts
index 6f62df84b53..e835a587e04 100644
--- a/dotcom-rendering/.storybook/decorators/grid.ts
+++ b/dotcom-rendering/src/grid.ts
@@ -1,6 +1,6 @@
// ----- Imports ----- //
-import { from } from '@guardian/source/foundations';
+import { from as fromBreakpoint } from '@guardian/source/foundations';
// ----- Columns & Lines ----- //
@@ -38,23 +38,23 @@ const container = `
grid-template-columns: ${mobileColumns};
column-gap: ${mobileColumnGap};
- ${from.mobileLandscape} {
+ ${fromBreakpoint.mobileLandscape} {
column-gap: ${columnGap};
}
- ${from.tablet} {
+ ${fromBreakpoint.tablet} {
grid-template-columns: ${tabletColumns};
}
- ${from.desktop} {
+ ${fromBreakpoint.desktop} {
grid-template-columns: ${desktopColumns};
}
- ${from.leftCol} {
+ ${fromBreakpoint.leftCol} {
grid-template-columns: ${leftColColumns};
}
- ${from.wide} {
+ ${fromBreakpoint.wide} {
grid-template-columns: ${wideColumns};
}
`;
@@ -86,7 +86,7 @@ const between = (from: Line | number, to: Line | number): string => `
* Ask the element to span a number of grid columns, starting at a specific
* grid line. The line can be specified either by `Line` name or by number.
* @param start The grid line to start from, either a `Line` name or a number.
- * @param span The number of columns to span.
+ * @param columns The number of columns to span.
* @returns {string} CSS to place the element on the grid.
*
* @example The element will span 3 columns from the line.
@@ -94,8 +94,8 @@ const between = (from: Line | number, to: Line | number): string => `
* ${grid.span('centre-column-start', 3)}
* `;
*/
-const span = (start: Line | number, span: number): string => `
- grid-column: ${start} / span ${span};
+const span = (start: Line | number, columns: number): string => `
+ grid-column: ${start} / span ${columns};
`;
/**
diff --git a/dotcom-rendering/src/lib/edition.ts b/dotcom-rendering/src/lib/edition.ts
index 2273b766380..8e34e942d77 100644
--- a/dotcom-rendering/src/lib/edition.ts
+++ b/dotcom-rendering/src/lib/edition.ts
@@ -79,6 +79,9 @@ const editionList = [
shortTitle: string;
}>;
+type TimeZone = (typeof editionList)[number]['timeZone'];
+type Locale = (typeof editionList)[number]['dateLocale'];
+
const [ukEdition] = editionList;
/**
@@ -151,9 +154,16 @@ const splitEditionalisedPage = (
const isEditionalisedPage = (pageId: string): boolean =>
!!splitEditionalisedPage(pageId);
+const getTimeZoneFromEdition = (edition: EditionId): TimeZone =>
+ getEditionFromId(edition).timeZone;
+
+const getLocaleFromEdition = (edition: EditionId): Locale =>
+ getEditionFromId(edition).dateLocale;
+
export {
EditionId,
Edition,
+ TimeZone,
editionList,
editionalisedPages,
getEditionFromId,
@@ -163,4 +173,6 @@ export {
isEditionalisedPage,
isNetworkFront,
splitEditionalisedPage,
+ getTimeZoneFromEdition,
+ getLocaleFromEdition,
};
diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts
index 213a89dd699..cd0ba51df75 100644
--- a/dotcom-rendering/src/paletteDeclarations.ts
+++ b/dotcom-rendering/src/paletteDeclarations.ts
@@ -6482,6 +6482,22 @@ const paletteColours = {
light: followTextLight,
dark: followTextDark,
},
+ '--football-match-list-background': {
+ light: () => sourcePalette.neutral[97],
+ dark: () => sourcePalette.neutral[20],
+ },
+ '--football-match-list-border': {
+ light: () => sourcePalette.neutral[93],
+ dark: () => sourcePalette.neutral[38],
+ },
+ '--football-match-list-competition-text': {
+ light: () => sourcePalette.sport[300],
+ dark: () => sourcePalette.neutral[86],
+ },
+ '--football-match-list-top-border': {
+ light: () => sourcePalette.sport[500],
+ dark: () => sourcePalette.neutral[60],
+ },
'--front-container-background': {
light: () => sourcePalette.neutral[100],
dark: () => sourcePalette.neutral[10],