diff --git a/dotcom-rendering/src/components/FootballMatchStat.stories.tsx b/dotcom-rendering/src/components/FootballMatchStat.stories.tsx new file mode 100644 index 00000000000..339ecc24a57 --- /dev/null +++ b/dotcom-rendering/src/components/FootballMatchStat.stories.tsx @@ -0,0 +1,79 @@ +import { css } from '@emotion/react'; +import { space } from '@guardian/source/foundations'; +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { palette } from '../palette'; +import { FootballMatchStat } from './FootballMatchStat'; + +const meta = { + title: 'Components/Football Match Stat', + component: FootballMatchStat, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + viewport: { + defaultViewport: 'mobileMedium', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default = { + args: { + label: 'Goal Attempts', + home: { + teamName: 'Manchester United', + teamColour: '#da020e', + value: 7, + }, + away: { + teamName: 'Arsenal', + teamColour: '#023474', + value: 4, + }, + }, +} satisfies Story; + +export const ShownAsPercentage = { + args: { + label: 'Possession', + home: { + teamName: 'West Ham', + teamColour: '#722642', + value: 39, + }, + away: { + teamName: 'Newcastle', + teamColour: '#383838', + value: 61, + }, + showPercentage: true, + }, +} satisfies Story; + +export const RaisedLabelOnDesktop = { + args: { + ...Default.args, + raiseLabelOnDesktop: true, + }, +} satisfies Story; + +export const LargeNumbersOnDesktop = { + args: { + ...Default.args, + largeNumbersOnDesktop: true, + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/FootballMatchStat.tsx b/dotcom-rendering/src/components/FootballMatchStat.tsx new file mode 100644 index 00000000000..c434a8a5ee3 --- /dev/null +++ b/dotcom-rendering/src/components/FootballMatchStat.tsx @@ -0,0 +1,165 @@ +import { css } from '@emotion/react'; +import { + from, + space, + textSansBold14, + textSansBold15, + textSansBold20, + textSansBold28, + visuallyHidden, +} from '@guardian/source/foundations'; +import { palette } from '../palette'; + +const containerCss = css` + position: relative; + padding: 5px 10px 10px; + border: 1px solid ${palette('--football-match-stat-border')}; + border-radius: 6px; + &::before { + position: absolute; + content: ''; + left: 50%; + bottom: 0; + width: 1px; + height: ${space[6]}px; + background-color: ${palette('--football-match-stat-border')}; + } +`; + +const headerCss = css` + display: grid; + grid-template-columns: auto 1fr auto; + grid-template-areas: 'home-stat label away-stat'; +`; + +const raiseLabelCss = css` + ${from.desktop} { + grid-template-areas: + 'label label label' + 'home-stat . away-stat'; + } +`; + +const labelCss = css` + ${textSansBold14}; + grid-area: label; + justify-self: center; + color: ${palette('--football-match-stat-name')}; + ${from.desktop} { + ${textSansBold15}; + } +`; + +const numberCss = css` + ${textSansBold20}; + grid-area: home-stat; + color: var(--match-stat-team-colour); +`; + +const largeNumberCss = css` + ${from.desktop} { + ${textSansBold28} + } +`; + +const awayStatCss = css` + grid-area: away-stat; + justify-self: end; +`; + +const chartCss = css` + position: relative; + display: flex; + gap: 10px; +`; + +const barCss = css` + height: ${space[2]}px; + width: var(--match-stat-percentage); + background-color: var(--match-stat-team-colour); + border-radius: 8px; +`; + +type MatchStatistic = { + teamName: string; + teamColour: string; + value: number; +}; + +type Props = { + label: string; + home: MatchStatistic; + away: MatchStatistic; + showPercentage?: boolean; + raiseLabelOnDesktop?: boolean; + largeNumbersOnDesktop?: boolean; +}; + +const formatValue = (value: number, showPercentage: boolean) => + `${value}${showPercentage ? '%' : ''}`; + +export const FootballMatchStat = ({ + label, + home, + away, + showPercentage = false, + raiseLabelOnDesktop = false, + largeNumbersOnDesktop = false, +}: Props) => { + const homePercentage = (home.value / (home.value + away.value)) * 100; + const awayPercentage = (away.value / (home.value + away.value)) * 100; + + return ( +
+
+ {label} + + + {home.teamName} + + {formatValue(home.value, showPercentage)} + + + + {away.teamName} + + {formatValue(away.value, showPercentage)} + +
+ +
+ ); +}; diff --git a/dotcom-rendering/src/components/FootballMiniMatchStats.stories.tsx b/dotcom-rendering/src/components/FootballMiniMatchStats.stories.tsx new file mode 100644 index 00000000000..7bfc0cbcbff --- /dev/null +++ b/dotcom-rendering/src/components/FootballMiniMatchStats.stories.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/react'; +import { breakpoints, from } from '@guardian/source/foundations'; +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { palette } from '../palette'; +import { FootballMiniMatchStats as FootballMiniMatchStatsComponent } from './FootballMiniMatchStats'; + +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 containerCss = css` + padding: 10px; + ${from.desktop} { + padding-left: 20px; + padding-right: 0; + } +`; + +const meta = { + title: 'Components/Football Mini Match Stats', + component: FootballMiniMatchStatsComponent, + decorators: [ + (Story) => ( +
+
+ +
+
+ ), + ], + parameters: { + chromatic: { + viewports: [ + breakpoints.mobileMedium, + breakpoints.tablet, + breakpoints.wide, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const FootballMiniMatchStats = { + args: { + homeTeam: { + name: 'Manchester United', + colour: '#da020e', + }, + awayTeam: { + name: 'Arsenal', + colour: '#023474', + }, + stats: [ + { + label: 'Possession', + homeValue: 39, + awayValue: 61, + showPercentage: true, + }, + { label: 'Goal Attempts', homeValue: 7, awayValue: 4 }, + ], + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/FootballMiniMatchStats.tsx b/dotcom-rendering/src/components/FootballMiniMatchStats.tsx new file mode 100644 index 00000000000..0c335fcb1cd --- /dev/null +++ b/dotcom-rendering/src/components/FootballMiniMatchStats.tsx @@ -0,0 +1,91 @@ +import { css } from '@emotion/react'; +import { from } from '@guardian/source/foundations'; +import { + LinkButton, + SvgArrowRightStraight, +} from '@guardian/source/react-components'; +import { palette } from '../palette'; +import { FootballMatchStat } from './FootballMatchStat'; + +const containerCss = css` + display: flex; + flex-direction: column; + gap: 10px; +`; + +const buttonTextCss = css` + ${from.desktop} { + display: none; + } +`; + +const buttonTextShortCss = css` + display: none; + ${from.desktop} { + display: inline; + } +`; + +type FootballTeam = { + name: string; + colour: string; +}; + +type MatchStatistic = { + label: string; + homeValue: number; + awayValue: number; + showPercentage?: boolean; +}; + +type Props = { + homeTeam: FootballTeam; + awayTeam: FootballTeam; + stats: MatchStatistic[]; +}; + +export const FootballMiniMatchStats = ({ + homeTeam, + awayTeam, + stats, +}: Props) => { + return ( +
+ {stats.map((stat) => ( + + ))} + } + iconSide="right" + theme={{ + backgroundPrimary: palette( + '--football-match-stat-button-background', + ), + backgroundPrimaryHover: palette( + '--football-match-stat-button-background-hover', + ), + }} + > + More stats, line-ups and tables + Stats and line ups + +
+ ); +}; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index 061f0926be6..bd4bdc42329 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -7043,6 +7043,10 @@ const paletteColours = { light: () => '#3DB540', dark: () => '#3DB540', }, + '--football-live-blog-background': { + light: () => sourcePalette.neutral[97], + dark: () => sourcePalette.neutral[10], + }, '--football-match-hover': { light: () => sourcePalette.neutral[93], dark: () => sourcePalette.neutral[38], @@ -7055,6 +7059,22 @@ const paletteColours = { light: () => sourcePalette.sport[500], dark: () => sourcePalette.sport[500], }, + '--football-match-stat-border': { + light: () => sourcePalette.neutral[86], + dark: () => sourcePalette.neutral[38], + }, + '--football-match-stat-button-background': { + light: () => sourcePalette.sport[400], + dark: () => sourcePalette.sport[500], + }, + '--football-match-stat-button-background-hover': { + light: () => '#00679E', // replace with Source's `calculateHoverColour` when available + dark: () => '#00A1E6', + }, + '--football-match-stat-name': { + light: () => sourcePalette.neutral[7], + dark: () => sourcePalette.neutral[86], + }, '--football-score-border': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[7],