From ad85f37df748ee5ec08a6734bd7ea35e502a62de Mon Sep 17 00:00:00 2001 From: James Mockett <1166188+jamesmockett@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:52:21 +0000 Subject: [PATCH 1/5] Work in progress cricket match header component --- .../CricketMatchHeader.stories.tsx | 57 +++ .../CricketMatchHeader/CricketMatchHeader.tsx | 400 ++++++++++++++++++ 2 files changed, 457 insertions(+) create mode 100644 dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx create mode 100644 dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx new file mode 100644 index 00000000000..25b2264fa02 --- /dev/null +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { CricketMatchHeader } from './CricketMatchHeader'; + +const meta = { + component: CricketMatchHeader, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Live = { + args: { + edition: 'UK', + match: { + kind: 'Live', + series: 'Ashes 2025–2026', + competition: 'Second Test Match', + venue: 'Brisbane Cricket Ground', + day: 2, + matchDate: new Date('2026-01-26'), + homeTeam: { + name: 'Australia', + paID: '44', + }, + awayTeam: { + name: 'England', + paID: '997', + }, + innings: [ + { + order: 1, + battingTeam: 'Australia', + runs: 169, + overs: '20.0', + fallOfWicket: [ + { order: '1' }, + { order: '2' }, + { order: '3' }, + { order: '4' }, + { order: '5' }, + { order: '6' }, + { order: '7' }, + { order: '8' }, + ], + }, + { + order: 2, + battingTeam: 'England', + runs: 173, + overs: '12.5', + fallOfWicket: [{ order: '1' }], + }, + ], + }, + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx new file mode 100644 index 00000000000..88c62fea7f0 --- /dev/null +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx @@ -0,0 +1,400 @@ +import { css } from '@emotion/react'; +import { + from, + headlineBold20Object, + headlineBold24Object, + space, + textSans14Object, + textSansBold14Object, + textSansBold17Object, + until, +} from '@guardian/source/foundations'; +import { type ReactNode, useMemo } from 'react'; +import { grid } from '../../grid'; +import { + type EditionId, + getLocaleFromEdition, + getTimeZoneFromEdition, +} from '../../lib/edition'; +import { palette } from '../../palette'; +import type { ColourName } from '../../paletteDeclarations'; +import { BigNumber } from '../BigNumber'; +import { FootballCrest } from '../FootballCrest'; +import { + background, + border, + primaryText, + secondaryText, +} from '../FootballMatchHeader/colours'; + +type CricketTeam = { + name: string; + paID: string; +}; + +type Inning = { + order: number; + battingTeam: string; + runs: number; + overs: string; + fallOfWicket: { order: string }[]; +}; + +type CricketMatch = { + kind: 'Fixture' | 'Live' | 'Result'; + series: string; + competition: string; + venue: string; + day: number; + matchDate: Date; + homeTeam: CricketTeam; + awayTeam: CricketTeam; + innings: Inning[]; +}; + +type Props = { + edition: EditionId; + match: CricketMatch; +}; + +export const CricketMatchHeader = (props: Props) => { + const match = props.match; + + return ( +
+
+ +
+ +
+
+
+ ); +}; + +const StatusLine = (props: { match: CricketMatch; edition: EditionId }) => ( +

+ + {props.match.series} + + + {props.match.competition}, {props.match.venue} •{' '} + + +

+); + +const SeriesName = (props: { + matchKind: CricketMatch['kind']; + children: ReactNode; +}) => ( + <> + + {props.children} + + + {' '} + •{' '} + + +); + +const MatchStatus = (props: { match: CricketMatch; edition: EditionId }) => { + const matchDateFormatter = useMemo( + () => MatchDateFormatterForEdition(props.edition), + [props.edition], + ); + + switch (props.match.kind) { + case 'Fixture': + return matchDateFormatter.format(props.match.matchDate); + default: + return ( + Day {props.match.day} + ); + } +}; + +const MatchDateFormatterForEdition = ( + edition: EditionId, +): Intl.DateTimeFormat => + new Intl.DateTimeFormat(getLocaleFromEdition(edition), { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric', + timeZone: getTimeZoneFromEdition(edition), + }); + +const Hr = (props: { + borderStyle: 'dotted' | 'solid'; + borderColour: ColourName; +}) => ( +
+); + +const Teams = (props: { match: CricketMatch }) => ( +
+ + +
+); + +const Team = (props: { + team: 'homeTeam' | 'awayTeam'; + match: CricketMatch; +}) => ( +
+ + + + + {props.match.kind !== 'Fixture' ? ( + + ) : null} + {/* {props.match.kind !== 'Fixture' ? ( + + ) : null} */} +
+); + +const TeamName = (props: { name: string }) => ( +

+ {props.name} +

+); + +const Crest = (props: { name: string; paID: string }) => ( + + + +); + +/** + * This uses `role="img"` because the score can be made up of multiple inline + * SVGs used in combination. For example, a 1 and a 2 are combined to make 12. + * We can't use the `img` tag because the SVGs are inline, and we can't use an + * SVG `title` tag because that would only work for a single number, not a + * combined one. + * + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/img_role + */ +const Score = (props: { + runs: number; + fallOfWicket: number; + matchKind: CricketMatch['kind']; +}) => ( + * + *': { + marginLeft: -6, + }, + }} + style={{ + borderColor: palette(border(props.matchKind)), + '--svg-fill': palette(primaryText(props.matchKind)), + }} + > + + + + +); + +const circleStyles = { + borderRadius: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 60, + height: 60, +}; + +const ScoreNumber = (props: { score: number }) => { + if (!Number.isInteger(props.score) || props.score < 0) { + return null; + } else if (props.score < 10) { + return ; + } else { + return ( + <> + + + + ); + } +}; + +// const Scorers = (props: { scorers: string[] }) => +// props.scorers.length === 0 ? null : ( +//
    +// {props.scorers.map((scorer) => ( +//
  • {scorer}
  • +// ))} +//
+// ); From 092025cb2bfb78a3e279c216b435277de9eaa757 Mon Sep 17 00:00:00 2001 From: James Mockett <1166188+jamesmockett@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:00:43 +0000 Subject: [PATCH 2/5] Update data model and logic for innings --- .../CricketMatchHeader.stories.tsx | 25 +--- .../CricketMatchHeader/CricketMatchHeader.tsx | 128 +++++++++++------- 2 files changed, 81 insertions(+), 72 deletions(-) diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx index 25b2264fa02..983e4584977 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx @@ -27,31 +27,16 @@ export const Live = { name: 'England', paID: '997', }, - innings: [ - { - order: 1, - battingTeam: 'Australia', + innings: { + homeTeam: { runs: 169, overs: '20.0', - fallOfWicket: [ - { order: '1' }, - { order: '2' }, - { order: '3' }, - { order: '4' }, - { order: '5' }, - { order: '6' }, - { order: '7' }, - { order: '8' }, - ], + fallOfWicket: 8, }, - { - order: 2, - battingTeam: 'England', + awayTeam: { runs: 173, - overs: '12.5', - fallOfWicket: [{ order: '1' }], }, - ], + }, }, }, } satisfies Story; diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx index 88c62fea7f0..7e3d0e09ae6 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx @@ -5,6 +5,7 @@ import { headlineBold24Object, space, textSans14Object, + textSans15Object, textSansBold14Object, textSansBold17Object, until, @@ -33,11 +34,9 @@ type CricketTeam = { }; type Inning = { - order: number; - battingTeam: string; runs: number; - overs: string; - fallOfWicket: { order: string }[]; + overs?: string; + fallOfWicket?: number; }; type CricketMatch = { @@ -49,7 +48,10 @@ type CricketMatch = { matchDate: Date; homeTeam: CricketTeam; awayTeam: CricketTeam; - innings: Inning[]; + innings: { + homeTeam?: Inning; + awayTeam?: Inning; + }; }; type Props = { @@ -236,47 +238,65 @@ const Teams = (props: { match: CricketMatch }) => ( const Team = (props: { team: 'homeTeam' | 'awayTeam'; match: CricketMatch; -}) => ( -
- - { + const team = props.match[props.team]; + const innings = props.match.innings[props.team]; + + return ( +
- - - {props.match.kind !== 'Fixture' ? ( - - ) : null} - {/* {props.match.kind !== 'Fixture' ? ( + + + + + {props.match.kind !== 'Fixture' ? ( + innings ? ( + + ) : ( + + Yet to bat + + ) + ) : null} + {/* {props.match.kind !== 'Fixture' ? ( ) : null} */} -
-); +
+ ); +}; const TeamName = (props: { name: string }) => (

( */ const Score = (props: { runs: number; - fallOfWicket: number; + fallOfWicket?: number; matchKind: CricketMatch['kind']; }) => ( - - + {props.fallOfWicket !== undefined ? ( + <> + + + + ) : null} ); From 6b4ca8a7475de44dc78b93c1bd67bd3f82119ad2 Mon Sep 17 00:00:00 2001 From: James Mockett <1166188+jamesmockett@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:40:38 +0000 Subject: [PATCH 3/5] Display overs --- .../CricketMatchHeader/CricketMatchHeader.tsx | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx index 7e3d0e09ae6..7d933ba1389 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx @@ -4,6 +4,7 @@ import { headlineBold20Object, headlineBold24Object, space, + textSans12Object, textSans14Object, textSans15Object, textSansBold14Object, @@ -44,7 +45,7 @@ type CricketMatch = { series: string; competition: string; venue: string; - day: number; + day?: number; matchDate: Date; homeTeam: CricketTeam; awayTeam: CricketTeam; @@ -272,13 +273,29 @@ const Team = (props: { > - {props.match.kind !== 'Fixture' ? ( - innings ? ( - + {props.match.kind !== 'Fixture' && + (innings ? ( + <> + + {!!innings.overs && ( + + {innings.overs} overs + + )} + ) : ( Yet to bat - ) - ) : null} - {/* {props.match.kind !== 'Fixture' ? ( - - ) : null} */} + ))} ); }; @@ -319,6 +332,7 @@ const Crest = (props: { name: string; paID: string }) => ( zIndex: 1, }} > + {/* TODO: Do we have cricket team crests? */} * + *': { marginLeft: -6, }, @@ -369,6 +383,7 @@ const Score = (props: { {props.fallOfWicket !== undefined ? ( <> + {/* TODO: Convert dash to SVG? */} { ); } }; - -// const Scorers = (props: { scorers: string[] }) => -// props.scorers.length === 0 ? null : ( -//

    -// {props.scorers.map((scorer) => ( -//
  • {scorer}
  • -// ))} -//
-// ); From 110b24079b0f0f45b1ec9a760bf759f991baa1e2 Mon Sep 17 00:00:00 2001 From: James Mockett <1166188+jamesmockett@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:41:10 +0000 Subject: [PATCH 4/5] Update stories to show fixture and live states --- .../CricketMatchHeader.stories.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx index 983e4584977..995fa4bf12c 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx @@ -9,15 +9,14 @@ export default meta; type Story = StoryObj; -export const Live = { +export const Fixture = { args: { edition: 'UK', match: { - kind: 'Live', + kind: 'Fixture', series: 'Ashes 2025–2026', competition: 'Second Test Match', venue: 'Brisbane Cricket Ground', - day: 2, matchDate: new Date('2026-01-26'), homeTeam: { name: 'Australia', @@ -27,6 +26,18 @@ export const Live = { name: 'England', paID: '997', }, + innings: {}, + }, + }, +} satisfies Story; + +export const Live = { + args: { + edition: 'UK', + match: { + ...Fixture.args.match, + kind: 'Live', + day: 2, innings: { homeTeam: { runs: 169, @@ -35,6 +46,8 @@ export const Live = { }, awayTeam: { runs: 173, + overs: '19.3', + fallOfWicket: 5, }, }, }, From 875f3c26019e36ef97f7d99bb7da4130d95dc3f5 Mon Sep 17 00:00:00 2001 From: Frederick O'Brien Date: Thu, 26 Mar 2026 14:06:45 +0000 Subject: [PATCH 5/5] App notification prompts and adjust rendering across match stages --- .../CricketMatchHeader.stories.tsx | 36 ++-- .../CricketMatchHeader/CricketMatchHeader.tsx | 174 +++++++++++++----- 2 files changed, 145 insertions(+), 65 deletions(-) diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx index 995fa4bf12c..4a501bf0198 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx @@ -18,15 +18,9 @@ export const Fixture = { competition: 'Second Test Match', venue: 'Brisbane Cricket Ground', matchDate: new Date('2026-01-26'), - homeTeam: { - name: 'Australia', - paID: '44', - }, - awayTeam: { - name: 'England', - paID: '997', - }, - innings: {}, + homeTeam: 'Australia', + awayTeam: 'England', + innings: [], }, }, } satisfies Story; @@ -38,18 +32,26 @@ export const Live = { ...Fixture.args.match, kind: 'Live', day: 2, - innings: { - homeTeam: { - runs: 169, + innings: [ + { + order: 1, + declared: false, + forfeited: false, + battingTeam: 'home', + runsScored: 169, overs: '20.0', - fallOfWicket: 8, + fallOfWicket: [], }, - awayTeam: { - runs: 173, + { + order: 2, + declared: false, + forfeited: false, + battingTeam: 'away', + runsScored: 173, overs: '19.3', - fallOfWicket: 5, + fallOfWicket: [], }, - }, + ], }, }, } satisfies Story; diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx index 7d933ba1389..e6e314cd7f1 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx @@ -11,6 +11,8 @@ import { textSansBold17Object, until, } from '@guardian/source/foundations'; +import { SvgNotificationsOn } from '@guardian/source/react-components'; +import { ToggleSwitch } from '@guardian/source-development-kitchen/react-components'; import { type ReactNode, useMemo } from 'react'; import { grid } from '../../grid'; import { @@ -29,15 +31,14 @@ import { secondaryText, } from '../FootballMatchHeader/colours'; -type CricketTeam = { - name: string; - paID: string; -}; - type Inning = { - runs: number; - overs?: string; - fallOfWicket?: number; + order: number; + battingTeam: string; + runsScored: number; + overs: string; + declared: boolean; + forfeited: boolean; + fallOfWicket: { order: number }[]; }; type CricketMatch = { @@ -47,17 +48,15 @@ type CricketMatch = { venue: string; day?: number; matchDate: Date; - homeTeam: CricketTeam; - awayTeam: CricketTeam; - innings: { - homeTeam?: Inning; - awayTeam?: Inning; - }; + homeTeam: string; + awayTeam: string; + innings: Inning[]; }; type Props = { edition: EditionId; match: CricketMatch; + isApp?: boolean; }; export const CricketMatchHeader = (props: Props) => { @@ -88,6 +87,46 @@ export const CricketMatchHeader = (props: Props) => {
+ {props.isApp && match.kind !== 'Result' && ( +
+
+ Be notified about start times, wickets, run outs, alien + invasions and final scores +
+
+ {' '} + Get match notifications + + {/* TODO: Wire toggle up for app notifications */} + + +
+
+ )} ); }; @@ -231,17 +270,26 @@ const Teams = (props: { match: CricketMatch }) => ( }, }} > - - + + ); -const Team = (props: { - team: 'homeTeam' | 'awayTeam'; - match: CricketMatch; -}) => { - const team = props.match[props.team]; - const innings = props.match.innings[props.team]; +const Team = (props: { team: string; match: CricketMatch }) => { + const innings = props.match.innings.filter( + (inning) => inning.battingTeam === props.team, + ); + { + /* TODO: Calculate if team won and margin/nature of victory */ + } + const teamIsWinner = Math.random() < 0.5 && props.match.kind === 'Result'; + const marginOfVictory: { + number: number; + unit: 'runs' | 'wickets'; + } = { + number: 4, + unit: 'runs', + }; return (
- + - + {props.match.kind !== 'Fixture' && - (innings ? ( - <> - - {!!innings.overs && ( - - {innings.overs} overs - - )} - + (innings.length > 0 ? ( + innings.map((inning) => ( + <> + + {!!inning.overs && + props.match.kind !== 'Result' && ( + <> + + {inning.fallOfWicket.length === 10 + ? 'All out' + : ''} + + + {inning.overs} overs + + + )} + + )) ) : ( ))} + {teamIsWinner && ( +
+ Won by{' '} + + {marginOfVictory.number} {marginOfVictory.unit} + +
+ )}
); }; @@ -356,7 +432,9 @@ const Crest = (props: { name: string; paID: string }) => ( */ const Score = (props: { runs: number; - fallOfWicket?: number; + fallOfWicket?: { + order: number; + }[]; matchKind: CricketMatch['kind']; }) => ( - + ) : null}