From 72f50f13f27bf46e4bcbf27b71a23966e3d43304 Mon Sep 17 00:00:00 2001 From: William Mead <285652121+williammead@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:49:08 +0100 Subject: [PATCH 01/15] rough idea of the cricket mini match stats component --- .../CricketMiniMatchStats.stories.tsx | 79 ++++++++ .../src/components/CricketMiniMatchStats.tsx | 180 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx create mode 100644 dotcom-rendering/src/components/CricketMiniMatchStats.tsx diff --git a/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx b/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx new file mode 100644 index 00000000000..7d350f5e895 --- /dev/null +++ b/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx @@ -0,0 +1,79 @@ +import { css } from '@emotion/react'; +import { breakpoints, from } from '@guardian/source/foundations'; +import preview from '../../.storybook/preview'; +import { palette } from '../palette'; +import { CricketMiniMatchStats as CricketMiniMatchStatsComponent } from './CricketMiniMatchStats'; + +// For Development Purposes +import { object, string, Output } from 'valibot'; + +const feCricketMatchStatsSummarySchema = object({ + id: string(), + currentBattingTeam: string(), + matchStatus: string(), + infoURL: string(), +}); + +type FECricketMatchStatsSummary = Output< + typeof feCricketMatchStatsSummarySchema +>; +// End of Development Helpers + +const gridCss = css` + background-color: ${palette('--football-live-blog-background')}; + /** + * Extremely simplified live blog grid layout as we're only interested in + * the 240px wide left column added at the desktop breakpoint. + * dotcom-rendering/src/layouts/LiveLayout.tsx + */ + ${from.desktop} { + display: grid; + grid-column-gap: 20px; + grid-template-columns: 240px 1fr; + } +`; + +const meta = preview.meta({ + title: 'Components/Cricket Mini Match Stats', + component: CricketMiniMatchStatsComponent, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + chromatic: { + viewports: [ + breakpoints.mobileMedium, + breakpoints.tablet, + breakpoints.wide, + ], + }, + }, +}); + +const feMatchStatsSummaryData: FECricketMatchStatsSummary = { + id: '4540747', + matchStatus: 'test', + currentBattingTeam: 'England', + infoURL: + 'https://www.theguardian.com/football/match/2026/feb/17/bristolcity-v-wrexham', +}; + +const getMockData = (data: FECricketMatchStatsSummary) => + new Promise((resolve) => { + setTimeout(() => { + resolve(data); + }, 1000); + }); + +export const CricketMiniMatchStats = meta.story({ + args: { + matchStatsUrl: + 'https://api.nextgen.guardianapps.co.uk/sport/cricket/match-header/2026-06-25/england-cricket-team.json', + getMatchStatsData: () => getMockData(feMatchStatsSummaryData), + refreshInterval: 16_000, + }, +}); diff --git a/dotcom-rendering/src/components/CricketMiniMatchStats.tsx b/dotcom-rendering/src/components/CricketMiniMatchStats.tsx new file mode 100644 index 00000000000..bb7a64cbade --- /dev/null +++ b/dotcom-rendering/src/components/CricketMiniMatchStats.tsx @@ -0,0 +1,180 @@ +import { css } from '@emotion/react'; +import { log } from '@guardian/libs'; +import { from } from '@guardian/source/foundations'; +import { + LinkButton, + SvgArrowRightStraight, +} from '@guardian/source/react-components'; +import type { SWRConfiguration } from 'swr'; +import useSWR from 'swr'; +import { safeParse } from 'valibot'; +import type { Result } from '../lib/result'; +import { error, fromValibot, ok } from '../lib/result'; +import { palette } from '../palette'; +import { Placeholder } from './Placeholder'; + +//For Development Purposes +import { object, string, Output } from 'valibot'; + +const feCricketMatchStatsSummarySchema = object({ + id: string(), + currentBattingTeam: string(), + matchStatus: string(), + infoURL: string(), +}); + +type FECricketMatchStatsSummary = Output< + typeof feCricketMatchStatsSummarySchema +>; + +type CricketMatchStatsSummary = { + matchStatus: string; + currentBattingTeam: string; + infoURL: string; +}; + +type UnknownEventType = { + kind: 'UnknownEventType'; + message: string; +}; + +type ParserError = UnknownEventType; + +const parseMatchStatsSummary = ( + feCricketMatchStatsSummary: FECricketMatchStatsSummary, +): Result => + ok({ + matchStatus: feCricketMatchStatsSummary.matchStatus, + currentBattingTeam: 'England', + infoURL: 'www.theguardian.com', + }); + +// End of Development helpers + +const containerCss = css` + isolation: isolate; /* [1] */ + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + background-color: ${palette('--football-live-blog-background')}; + ${from.mobileLandscape} { + padding: 20px; + } + ${from.desktop} { + padding-top: 24px; + padding-right: 0; + } +`; + +const buttonTextCss = css` + ${from.desktop} { + display: none; + } +`; + +const buttonTextShortCss = css` + display: none; + ${from.desktop} { + display: inline; + } +`; + +type Props = { + matchStatsUrl: string; + getMatchStatsData: (url: string) => Promise; + refreshInterval: number; +}; + +export const CricketMiniMatchStats = (props: Props) => { + const { data, error: swrError } = useSWR( + props.matchStatsUrl, + fetcher(props.getMatchStatsData), + swrOptions(props.refreshInterval), + ); + + if (swrError != null) { + return null; + } + + if (data === undefined) { + return ( + + ); + } + + return ( +
+

Stat 1

+

Stat 2

+ } + iconSide="right" + theme={{ + backgroundPrimary: palette( + '--football-match-stat-button-background', + ), + backgroundPrimaryHover: palette( + '--football-match-stat-button-background-hover', + ), + }} + > + More match info + More match info + +
+ ); +}; + +const swrOptions = ( + refreshInterval: number, +): SWRConfiguration => ({ + errorRetryCount: 1, + refreshInterval: (latestData: CricketMatchStatsSummary | undefined) => + latestData?.matchStatus === 'FT' ? 0 : refreshInterval, +}); + +const fetcher = + (getMatchStatsData: Props['getMatchStatsData']) => + (url: string): Promise => + getMatchStatsData(url) + .then(parseData) + .then((result) => { + if (!result.ok) { + log('dotcom', result.error); + throw new Error(); + } else { + return result.value; + } + }) + .catch(() => { + log('dotcom', 'Failed to fetch math stats summary data'); + throw new Error(); + }); + +const parseData = (json: unknown): Result => { + const feData = fromValibot( + safeParse(feCricketMatchStatsSummarySchema, json), + ); + + if (!feData.ok) { + return error('Failed to validate match stats summary data'); + } + + const parsedMatchStats = parseMatchStatsSummary(feData.value); + + if (!parsedMatchStats.ok) { + return error('Failed to parse match stats summary'); + } + + return ok(parsedMatchStats.value); +}; From 0484b540ad3da3bd1b1752ba74f9338987d67c13 Mon Sep 17 00:00:00 2001 From: William Mead <285652121+williammead@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:19:33 +0100 Subject: [PATCH 02/15] moved temporary types into appropriate files --- .../src/components/CricketMatchStat.tsx | 31 ++++++++++++ .../CricketMiniMatchStats.stories.tsx | 16 +----- .../src/components/CricketMiniMatchStats.tsx | 49 ++++--------------- dotcom-rendering/src/cricketMatchV2.ts | 17 +++++++ .../src/frontend/feCricketMatchPage.ts | 11 +++++ 5 files changed, 69 insertions(+), 55 deletions(-) create mode 100644 dotcom-rendering/src/components/CricketMatchStat.tsx diff --git a/dotcom-rendering/src/components/CricketMatchStat.tsx b/dotcom-rendering/src/components/CricketMatchStat.tsx new file mode 100644 index 00000000000..5dd4281034d --- /dev/null +++ b/dotcom-rendering/src/components/CricketMatchStat.tsx @@ -0,0 +1,31 @@ +import { css } from '@emotion/react'; +import { visuallyHidden } from '@guardian/source/foundations'; + +type MatchStatProps = { + heading: string; + value: string; + layout?: 'regular' | 'compact'; +}; + +export const CricketMatchStat = ({ + heading, + value, + layout, +}: MatchStatProps) => { + const compactLayout = layout === 'compact'; + return ( +
+

{heading}

+ + + {heading} + + {value} + +
+ ); +}; diff --git a/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx b/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx index 7d350f5e895..a549ef7e2c1 100644 --- a/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx +++ b/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx @@ -1,24 +1,10 @@ import { css } from '@emotion/react'; import { breakpoints, from } from '@guardian/source/foundations'; import preview from '../../.storybook/preview'; +import type { FECricketMatchStatsSummary } from '../frontend/feCricketMatchPage'; import { palette } from '../palette'; import { CricketMiniMatchStats as CricketMiniMatchStatsComponent } from './CricketMiniMatchStats'; -// For Development Purposes -import { object, string, Output } from 'valibot'; - -const feCricketMatchStatsSummarySchema = object({ - id: string(), - currentBattingTeam: string(), - matchStatus: string(), - infoURL: string(), -}); - -type FECricketMatchStatsSummary = Output< - typeof feCricketMatchStatsSummarySchema ->; -// End of Development Helpers - const gridCss = css` background-color: ${palette('--football-live-blog-background')}; /** diff --git a/dotcom-rendering/src/components/CricketMiniMatchStats.tsx b/dotcom-rendering/src/components/CricketMiniMatchStats.tsx index bb7a64cbade..5c4ac6d5861 100644 --- a/dotcom-rendering/src/components/CricketMiniMatchStats.tsx +++ b/dotcom-rendering/src/components/CricketMiniMatchStats.tsx @@ -8,49 +8,15 @@ import { import type { SWRConfiguration } from 'swr'; import useSWR from 'swr'; import { safeParse } from 'valibot'; +import type { CricketMatchStatsSummary } from '../cricketMatchV2'; +import { parseMatchStatsSummary } from '../cricketMatchV2'; +import { feCricketMatchStatsSummarySchema } from '../frontend/feCricketMatchPage'; import type { Result } from '../lib/result'; import { error, fromValibot, ok } from '../lib/result'; import { palette } from '../palette'; +import { CricketMatchStat } from './CricketMatchStat'; import { Placeholder } from './Placeholder'; -//For Development Purposes -import { object, string, Output } from 'valibot'; - -const feCricketMatchStatsSummarySchema = object({ - id: string(), - currentBattingTeam: string(), - matchStatus: string(), - infoURL: string(), -}); - -type FECricketMatchStatsSummary = Output< - typeof feCricketMatchStatsSummarySchema ->; - -type CricketMatchStatsSummary = { - matchStatus: string; - currentBattingTeam: string; - infoURL: string; -}; - -type UnknownEventType = { - kind: 'UnknownEventType'; - message: string; -}; - -type ParserError = UnknownEventType; - -const parseMatchStatsSummary = ( - feCricketMatchStatsSummary: FECricketMatchStatsSummary, -): Result => - ok({ - matchStatus: feCricketMatchStatsSummary.matchStatus, - currentBattingTeam: 'England', - infoURL: 'www.theguardian.com', - }); - -// End of Development helpers - const containerCss = css` isolation: isolate; /* [1] */ display: flex; @@ -112,8 +78,11 @@ export const CricketMiniMatchStats = (props: Props) => { return (
-

Stat 1

-

Stat 2

+ + = { // Fixtures 'pre-match': 'Fixture', @@ -221,6 +228,16 @@ const parseTeams = ( }); }; +// TODO: Need to actually parse data +export const parseMatchStatsSummary = ( + feCricketMatchStatsSummary: FECricketMatchStatsSummary, +): Result => + ok({ + matchStatus: feCricketMatchStatsSummary.matchStatus, + currentBattingTeam: 'England', + infoURL: 'www.theguardian.com', + }); + const parseWinnerResult = ( winner: FECricketMatchResultWinnerStatus, resultType: WinnerResult['type'], diff --git a/dotcom-rendering/src/frontend/feCricketMatchPage.ts b/dotcom-rendering/src/frontend/feCricketMatchPage.ts index 740a0bc719e..265dc8ec9c7 100644 --- a/dotcom-rendering/src/frontend/feCricketMatchPage.ts +++ b/dotcom-rendering/src/frontend/feCricketMatchPage.ts @@ -113,3 +113,14 @@ export type FECricketMatchPage = { contributionsServiceUrl: string; pageId: string; }; + +export const feCricketMatchStatsSummarySchema = object({ + id: string(), + currentBattingTeam: string(), + matchStatus: string(), + infoURL: string(), +}); + +export type FECricketMatchStatsSummary = Output< + typeof feCricketMatchStatsSummarySchema +>; From b00c4b9dd3ef37ae5740b67739567902718fdf3c Mon Sep 17 00:00:00 2001 From: William Mead <285652121+williammead@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:03:50 +0100 Subject: [PATCH 03/15] removed unused props --- dotcom-rendering/src/components/CricketMatchStat.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/dotcom-rendering/src/components/CricketMatchStat.tsx b/dotcom-rendering/src/components/CricketMatchStat.tsx index 5dd4281034d..ad72a70a13d 100644 --- a/dotcom-rendering/src/components/CricketMatchStat.tsx +++ b/dotcom-rendering/src/components/CricketMatchStat.tsx @@ -4,15 +4,9 @@ import { visuallyHidden } from '@guardian/source/foundations'; type MatchStatProps = { heading: string; value: string; - layout?: 'regular' | 'compact'; }; -export const CricketMatchStat = ({ - heading, - value, - layout, -}: MatchStatProps) => { - const compactLayout = layout === 'compact'; +export const CricketMatchStat = ({ heading, value }: MatchStatProps) => { return (

{heading}

From 8f786cf6a976f23d39d83367dc65c4d66b754e68 Mon Sep 17 00:00:00 2001 From: William Mead <285652121+williammead@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:37:27 +0100 Subject: [PATCH 04/15] add isCricketMatchReport to StandardLayout --- dotcom-rendering/src/layouts/StandardLayout.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/layouts/StandardLayout.tsx b/dotcom-rendering/src/layouts/StandardLayout.tsx index 78d86de2e1c..1810d9011e5 100644 --- a/dotcom-rendering/src/layouts/StandardLayout.tsx +++ b/dotcom-rendering/src/layouts/StandardLayout.tsx @@ -153,6 +153,12 @@ export const StandardLayout = (props: WebProps | AppProps) => { const isFootballMatchReport = format.design === ArticleDesign.MatchReport && !!footballMatchUrl; + const cricketMatchUrl = + article.matchType == 'CricketMatchType' ? article.matchUrl : undefined; + + const isCricketMatchReport = + format.design === ArticleDesign.MatchReport && !!cricketMatchUrl; + const isMedia = format.design === ArticleDesign.Video || format.design === ArticleDesign.Audio; @@ -236,6 +242,7 @@ export const StandardLayout = (props: WebProps | AppProps) => { { const MatchHeaderContainer = ({ isFootballMatchReport, + isCricketMatchReport, renderingTarget, article, format, }: { isFootballMatchReport: boolean; + isCricketMatchReport: boolean; renderingTarget: RenderingTarget; article: ArticleDeprecated; format: ArticleFormat; @@ -897,7 +906,12 @@ const MatchHeaderContainer = ({ ); } - if (!isApps && cricketMatchHeaderUrl && isCricketRedesignEnabled) { + if ( + isCricketMatchReport && + !isApps && + cricketMatchHeaderUrl && + isCricketRedesignEnabled + ) { return ( <>