diff --git a/dotcom-rendering/fixtures/generated/match-report.ts b/dotcom-rendering/fixtures/generated/match-report.ts index abf5e631e9a..40cd0a4d3c7 100644 --- a/dotcom-rendering/fixtures/generated/match-report.ts +++ b/dotcom-rendering/fixtures/generated/match-report.ts @@ -274,7 +274,7 @@ export const matchReport: MatchReportType = { shotsOff: 6, corners: 10, fouls: 4, - colours: '#ffffff', + colours: '#01009a', crest: 'https://sport.guim.co.uk/football/crests/120/7699.png', }, awayTeam: { diff --git a/dotcom-rendering/src/components/FootballMatchStat.tsx b/dotcom-rendering/src/components/FootballMatchStat.tsx index c434a8a5ee3..5f4d350b965 100644 --- a/dotcom-rendering/src/components/FootballMatchStat.tsx +++ b/dotcom-rendering/src/components/FootballMatchStat.tsx @@ -44,7 +44,7 @@ const labelCss = css` ${textSansBold14}; grid-area: label; justify-self: center; - color: ${palette('--football-match-stat-name')}; + color: ${palette('--football-match-stat-text')}; ${from.desktop} { ${textSansBold15}; } diff --git a/dotcom-rendering/src/components/Lineups.stories.tsx b/dotcom-rendering/src/components/Lineups.stories.tsx new file mode 100644 index 00000000000..27575cfd3b9 --- /dev/null +++ b/dotcom-rendering/src/components/Lineups.stories.tsx @@ -0,0 +1,55 @@ +import { css } from '@emotion/react'; +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { allModes } from '../../.storybook/modes'; +import { matchReport } from '../../fixtures/generated/match-report'; +import { grid } from '../grid'; +import { palette } from '../palette'; +import { type TeamType } from '../types/sport'; +import { Lineups as LineupsComponent } from './Lineups'; + +const homeTeam: TeamType = matchReport.homeTeam; + +const awayTeam: TeamType = matchReport.awayTeam; + +const meta = { + title: 'Components/Lineups', + component: LineupsComponent, + parameters: { + chromatic: { + modes: { + 'light leftCol': allModes['light leftCol'], + }, + }, + }, + decorators: [ + (Story) => ( +
+
+ +
+
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const LineupsStory = { + args: { + home: homeTeam, + away: awayTeam, + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/Lineups.tsx b/dotcom-rendering/src/components/Lineups.tsx new file mode 100644 index 00000000000..83a8be86ee2 --- /dev/null +++ b/dotcom-rendering/src/components/Lineups.tsx @@ -0,0 +1,224 @@ +import { css } from '@emotion/react'; +import { + from, + palette as sourcePalette, + space, + textSans14, + textSans15, + textSansBold12, + textSansBold14, +} from '@guardian/source/foundations'; +import type { FootballTeam } from '../footballMatch'; +import { palette } from '../palette'; +import Union from '../static/icons/Union.svg'; +import type { EventType } from '../types/sport'; + +type Props = { + home: FootballTeam; + away: FootballTeam; +}; + +const lineupSectionId = 'lineups'; +const substitutesSectionId = 'substitutes'; + +export const Lineups = ({ home, away }: Props) => { + return ( +
+
+ + <PlayerList team={home} isSubstitute={false} isHome={true} /> + <PlayerList team={away} isSubstitute={false} isHome={false} /> + </section> + <section + css={playerListSectionGridStyles} + aria-labelledby={substitutesSectionId} + > + <Title text="Substitutes" id={substitutesSectionId} /> + <PlayerList team={home} isSubstitute={true} isHome={true} /> + <PlayerList team={away} isSubstitute={true} isHome={false} /> + </section> + </section> + ); +}; + +const Event = ({ + type, + time, +}: { + type: 'substitution' | 'dismissal' | 'booking'; + time: string; +}) => { + switch (type) { + case 'dismissal': + return ( + <span + css={rectangle(BackgroundRed)} + aria-label="Red Card" + aria-hidden="false" + /> + ); + case 'booking': + return ( + <span + css={rectangle(BackgroundYellow)} + aria-label="Yellow Card" + aria-hidden="false" + /> + ); + case 'substitution': + return ( + <span + aria-label={`Substitution in ${time} minute`} + css={substitute} + > + <Union /> + {time} + </span> + ); + } +}; + +const Title = ({ text, id }: { text: string; id: string }) => ( + <h3 + id={id} + css={css` + border-bottom: 1px solid ${palette('--football-match-stat-border')}; + color: ${palette('--football-match-stat-text')}; + grid-column: home-start / away-end; + padding-bottom: ${space[1]}px; + ${textSansBold14} + `} + > + {text} + </h3> +); + +const PlayerList = ({ + team, + isSubstitute, + isHome, +}: { + team: FootballTeam; + isSubstitute: boolean; + isHome: boolean; +}) => { + return ( + <ul css={isHome ? homeStyles : awayStyles}> + {team.players + .filter((player) => player.substitute === isSubstitute) + .map((player) => ( + <li key={player.id} css={listItem}> + <strong css={shirtNumber(team.colours)}> + {player.shirtNumber} + </strong> + <span css={playerName}> + {player.name.charAt(0).toUpperCase()}.{' '} + {player.lastName} + </span> + {player.events.map((event: EventType) => ( + <Event + key={event.eventTime + event.eventType} + type={event.eventType} + time={event.eventTime} + /> + ))} + </li> + ))} + </ul> + ); +}; + +const sectionStyles = css` + border: 1px solid ${palette('--football-match-stat-border')}; + margin: ${space[2]}px; + border-radius: 6px; + + padding-top: 6px; + + background-color: ${palette('--football-match-info-background')}; +`; + +const playerListSectionGridStyles = css` + display: grid; + grid-template-columns: [home-start] 1fr [home-end away-start] 1fr [away-end]; + column-gap: 20px; + + padding: 0px 10px 10px 10px; +`; + +const homeStyles = css` + grid-column: home-start / home-end; +`; + +const awayStyles = css` + grid-column: away-start / away-end; + position: relative; + &::before { + content: ''; + position: absolute; + left: -10px; + top: 0; + bottom: 0; + width: 1px; + background-color: ${palette('--football-match-stat-border')}; + } +`; + +const shirtNumber = (color: string) => css` + display: inline-block; + width: ${space[5]}px; + ${textSansBold14} + color: ${color}; +`; + +const listItem = css` + padding-top: ${space[2]}px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: ${space[1]}px; + + &:last-child { + padding-bottom: ${space[0]}px; + } +`; + +const playerName = css` + ${textSans14} + ${from.tablet} { + ${textSans15} + } + color: ${palette('--football-match-stat-text')}; +`; + +const BackgroundRed = sourcePalette.news[400]; + +const BackgroundYellow = sourcePalette.brandAlt[300]; + +const rectangle = (color: string) => css` + display: inline-block; + width: ${space[3]}px; + height: ${space[4]}px; + border-radius: ${space[0]}px; + margin-left: ${space[1]}px; + background-color: ${color}; +`; + +const substitute = css` + ${textSansBold12} + border: 1px solid rgba(18, 18, 18, 0.20); + border-radius: ${space[8]}px; + padding: 0.5px ${space[1]}px 1.5px ${space[1]}px; + display: flex; + align-items: center; + color: ${palette('--football-match-stat-text')}; + opacity: 0.6; + gap: ${space[0]}px; + + svg { + fill: ${palette('--football-match-substitution-icon')}; + } +`; diff --git a/dotcom-rendering/src/paletteDeclarations.ts b/dotcom-rendering/src/paletteDeclarations.ts index bd4bdc42329..c4211185d75 100644 --- a/dotcom-rendering/src/paletteDeclarations.ts +++ b/dotcom-rendering/src/paletteDeclarations.ts @@ -7051,6 +7051,10 @@ const paletteColours = { light: () => sourcePalette.neutral[93], dark: () => sourcePalette.neutral[38], }, + '--football-match-info-background': { + light: () => sourcePalette.neutral[100], + dark: () => sourcePalette.neutral[10], + }, '--football-match-list-error': { light: () => sourcePalette.error[400], dark: () => sourcePalette.error[500], @@ -7071,10 +7075,14 @@ const paletteColours = { light: () => '#00679E', // replace with Source's `calculateHoverColour` when available dark: () => '#00A1E6', }, - '--football-match-stat-name': { + '--football-match-stat-text': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[86], }, + '--football-match-substitution-icon': { + light: () => sourcePalette.neutral[46], + dark: () => sourcePalette.neutral[60], + }, '--football-score-border': { light: () => sourcePalette.neutral[7], dark: () => sourcePalette.neutral[7], diff --git a/dotcom-rendering/src/static/icons/Union.svg b/dotcom-rendering/src/static/icons/Union.svg new file mode 100644 index 00000000000..39744f7af37 --- /dev/null +++ b/dotcom-rendering/src/static/icons/Union.svg @@ -0,0 +1,3 @@ +<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M2.80469 3.2998L5.17871 5.67773L4.71387 6.1416L3.14648 5.0166C3.02468 6.38068 3.46015 7.74532 4.50781 8.79492C6.33457 10.6248 9.3327 10.6368 11.1719 8.80078L11.9229 9.55371C9.67509 11.7976 6.02774 11.7851 3.77441 9.52832C2.52257 8.27434 1.96684 6.63966 2.08887 5.03418L0.454102 6.20215L0 5.74707L2.45215 3.29883L2.80469 3.2998ZM4.07715 1.67578C6.32503 -0.568258 9.97325 -0.556001 12.2266 1.70117C13.4782 2.9551 14.0331 4.58898 13.9111 6.19434L15.5459 5.02637L16 5.48145L13.5479 7.92969H13.1953L10.8213 5.55176L11.2861 5.08789L12.8535 6.21191C12.9753 4.84781 12.54 3.48317 11.4922 2.43359C9.66551 0.603931 6.66826 0.592039 4.8291 2.42773L4.07715 1.67578Z" fill="#707070"/> +</svg>