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..4a501bf0198 --- /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 Fixture = { + args: { + edition: 'UK', + match: { + kind: 'Fixture', + series: 'Ashes 2025–2026', + competition: 'Second Test Match', + venue: 'Brisbane Cricket Ground', + matchDate: new Date('2026-01-26'), + homeTeam: 'Australia', + awayTeam: 'England', + innings: [], + }, + }, +} satisfies Story; + +export const Live = { + args: { + edition: 'UK', + match: { + ...Fixture.args.match, + kind: 'Live', + day: 2, + innings: [ + { + order: 1, + declared: false, + forfeited: false, + battingTeam: 'home', + runsScored: 169, + overs: '20.0', + fallOfWicket: [], + }, + { + order: 2, + declared: false, + forfeited: false, + battingTeam: 'away', + runsScored: 173, + overs: '19.3', + fallOfWicket: [], + }, + ], + }, + }, +} 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..e6e314cd7f1 --- /dev/null +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx @@ -0,0 +1,502 @@ +import { css } from '@emotion/react'; +import { + from, + headlineBold20Object, + headlineBold24Object, + space, + textSans12Object, + textSans14Object, + textSans15Object, + textSansBold14Object, + 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 { + 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 Inning = { + order: number; + battingTeam: string; + runsScored: number; + overs: string; + declared: boolean; + forfeited: boolean; + fallOfWicket: { order: number }[]; +}; + +type CricketMatch = { + kind: 'Fixture' | 'Live' | 'Result'; + series: string; + competition: string; + venue: string; + day?: number; + matchDate: Date; + homeTeam: string; + awayTeam: string; + innings: Inning[]; +}; + +type Props = { + edition: EditionId; + match: CricketMatch; + isApp?: boolean; +}; + +export const CricketMatchHeader = (props: Props) => { + const match = props.match; + + return ( +
+
+ +
+ +
+
+ {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 */} + + +
+
+ )} +
+ ); +}; + +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: 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.length > 0 ? ( + innings.map((inning) => ( + <> + + {!!inning.overs && + props.match.kind !== 'Result' && ( + <> + + {inning.fallOfWicket.length === 10 + ? 'All out' + : ''} + + + {inning.overs} overs + + + )} + + )) + ) : ( + + Yet to bat + + ))} + {teamIsWinner && ( +
+ Won by{' '} + + {marginOfVictory.number} {marginOfVictory.unit} + +
+ )} +
+ ); +}; + +const TeamName = (props: { name: string }) => ( +

+ {props.name} +

+); + +const Crest = (props: { name: string; paID: string }) => ( + + {/* TODO: Do we have cricket team crests? */} + + +); + +/** + * 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?: { + order: number; + }[]; + matchKind: CricketMatch['kind']; +}) => ( + * + *': { + marginLeft: -6, + }, + }} + style={{ + borderColor: palette(border(props.matchKind)), + '--svg-fill': palette(primaryText(props.matchKind)), + }} + > + + {props.fallOfWicket !== undefined ? ( + <> + {/* TODO: Convert dash to SVG? */} + + + + ) : null} + +); + +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 ( + <> + + + + ); + } +};