Skip to content

Commit fc806f3

Browse files
authored
Merge pull request #14977 from guardian/football-match-info
Football match info lineups
2 parents db889b4 + bc1aa7d commit fc806f3

File tree

6 files changed

+293
-3
lines changed

6 files changed

+293
-3
lines changed

dotcom-rendering/fixtures/generated/match-report.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export const matchReport: MatchReportType = {
274274
shotsOff: 6,
275275
corners: 10,
276276
fouls: 4,
277-
colours: '#ffffff',
277+
colours: '#01009a',
278278
crest: 'https://sport.guim.co.uk/football/crests/120/7699.png',
279279
},
280280
awayTeam: {

dotcom-rendering/src/components/FootballMatchStat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const labelCss = css`
4444
${textSansBold14};
4545
grid-area: label;
4646
justify-self: center;
47-
color: ${palette('--football-match-stat-name')};
47+
color: ${palette('--football-match-stat-text')};
4848
${from.desktop} {
4949
${textSansBold15};
5050
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { css } from '@emotion/react';
2+
import type { Meta, StoryObj } from '@storybook/react-webpack5';
3+
import { allModes } from '../../.storybook/modes';
4+
import { matchReport } from '../../fixtures/generated/match-report';
5+
import { grid } from '../grid';
6+
import { palette } from '../palette';
7+
import { type TeamType } from '../types/sport';
8+
import { Lineups as LineupsComponent } from './Lineups';
9+
10+
const homeTeam: TeamType = matchReport.homeTeam;
11+
12+
const awayTeam: TeamType = matchReport.awayTeam;
13+
14+
const meta = {
15+
title: 'Components/Lineups',
16+
component: LineupsComponent,
17+
parameters: {
18+
chromatic: {
19+
modes: {
20+
'light leftCol': allModes['light leftCol'],
21+
},
22+
},
23+
},
24+
decorators: [
25+
(Story) => (
26+
<div
27+
css={css`
28+
background-color: ${palette(
29+
'--football-match-info-background',
30+
)};
31+
${grid.paddedContainer}
32+
`}
33+
>
34+
<div
35+
css={css`
36+
${grid.column.centre}
37+
`}
38+
>
39+
<Story />
40+
</div>
41+
</div>
42+
),
43+
],
44+
} satisfies Meta<typeof LineupsComponent>;
45+
46+
export default meta;
47+
48+
type Story = StoryObj<typeof meta>;
49+
50+
export const LineupsStory = {
51+
args: {
52+
home: homeTeam,
53+
away: awayTeam,
54+
},
55+
} satisfies Story;
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { css } from '@emotion/react';
2+
import {
3+
from,
4+
palette as sourcePalette,
5+
space,
6+
textSans14,
7+
textSans15,
8+
textSansBold12,
9+
textSansBold14,
10+
} from '@guardian/source/foundations';
11+
import type { FootballTeam } from '../footballMatch';
12+
import { palette } from '../palette';
13+
import Union from '../static/icons/Union.svg';
14+
import type { EventType } from '../types/sport';
15+
16+
type Props = {
17+
home: FootballTeam;
18+
away: FootballTeam;
19+
};
20+
21+
const lineupSectionId = 'lineups';
22+
const substitutesSectionId = 'substitutes';
23+
24+
export const Lineups = ({ home, away }: Props) => {
25+
return (
26+
<section css={sectionStyles} aria-label="Team Lineups and Substitutes">
27+
<section
28+
css={playerListSectionGridStyles}
29+
aria-labelledby={lineupSectionId}
30+
>
31+
<Title text="Lineups" id={lineupSectionId} />
32+
<PlayerList team={home} isSubstitute={false} isHome={true} />
33+
<PlayerList team={away} isSubstitute={false} isHome={false} />
34+
</section>
35+
<section
36+
css={playerListSectionGridStyles}
37+
aria-labelledby={substitutesSectionId}
38+
>
39+
<Title text="Substitutes" id={substitutesSectionId} />
40+
<PlayerList team={home} isSubstitute={true} isHome={true} />
41+
<PlayerList team={away} isSubstitute={true} isHome={false} />
42+
</section>
43+
</section>
44+
);
45+
};
46+
47+
const Event = ({
48+
type,
49+
time,
50+
}: {
51+
type: 'substitution' | 'dismissal' | 'booking';
52+
time: string;
53+
}) => {
54+
switch (type) {
55+
case 'dismissal':
56+
return (
57+
<span
58+
css={rectangle(BackgroundRed)}
59+
aria-label="Red Card"
60+
aria-hidden="false"
61+
/>
62+
);
63+
case 'booking':
64+
return (
65+
<span
66+
css={rectangle(BackgroundYellow)}
67+
aria-label="Yellow Card"
68+
aria-hidden="false"
69+
/>
70+
);
71+
case 'substitution':
72+
return (
73+
<span
74+
aria-label={`Substitution in ${time} minute`}
75+
css={substitute}
76+
>
77+
<Union />
78+
{time}
79+
</span>
80+
);
81+
}
82+
};
83+
84+
const Title = ({ text, id }: { text: string; id: string }) => (
85+
<h3
86+
id={id}
87+
css={css`
88+
border-bottom: 1px solid ${palette('--football-match-stat-border')};
89+
color: ${palette('--football-match-stat-text')};
90+
grid-column: home-start / away-end;
91+
padding-bottom: ${space[1]}px;
92+
${textSansBold14}
93+
`}
94+
>
95+
{text}
96+
</h3>
97+
);
98+
99+
const PlayerList = ({
100+
team,
101+
isSubstitute,
102+
isHome,
103+
}: {
104+
team: FootballTeam;
105+
isSubstitute: boolean;
106+
isHome: boolean;
107+
}) => {
108+
return (
109+
<ul css={isHome ? homeStyles : awayStyles}>
110+
{team.players
111+
.filter((player) => player.substitute === isSubstitute)
112+
.map((player) => (
113+
<li key={player.id} css={listItem}>
114+
<strong css={shirtNumber(team.colours)}>
115+
{player.shirtNumber}
116+
</strong>
117+
<span css={playerName}>
118+
{player.name.charAt(0).toUpperCase()}.{' '}
119+
{player.lastName}
120+
</span>
121+
{player.events.map((event: EventType) => (
122+
<Event
123+
key={event.eventTime + event.eventType}
124+
type={event.eventType}
125+
time={event.eventTime}
126+
/>
127+
))}
128+
</li>
129+
))}
130+
</ul>
131+
);
132+
};
133+
134+
const sectionStyles = css`
135+
border: 1px solid ${palette('--football-match-stat-border')};
136+
margin: ${space[2]}px;
137+
border-radius: 6px;
138+
139+
padding-top: 6px;
140+
141+
background-color: ${palette('--football-match-info-background')};
142+
`;
143+
144+
const playerListSectionGridStyles = css`
145+
display: grid;
146+
grid-template-columns: [home-start] 1fr [home-end away-start] 1fr [away-end];
147+
column-gap: 20px;
148+
149+
padding: 0px 10px 10px 10px;
150+
`;
151+
152+
const homeStyles = css`
153+
grid-column: home-start / home-end;
154+
`;
155+
156+
const awayStyles = css`
157+
grid-column: away-start / away-end;
158+
position: relative;
159+
&::before {
160+
content: '';
161+
position: absolute;
162+
left: -10px;
163+
top: 0;
164+
bottom: 0;
165+
width: 1px;
166+
background-color: ${palette('--football-match-stat-border')};
167+
}
168+
`;
169+
170+
const shirtNumber = (color: string) => css`
171+
display: inline-block;
172+
width: ${space[5]}px;
173+
${textSansBold14}
174+
color: ${color};
175+
`;
176+
177+
const listItem = css`
178+
padding-top: ${space[2]}px;
179+
display: flex;
180+
flex-wrap: wrap;
181+
align-items: center;
182+
gap: ${space[1]}px;
183+
184+
&:last-child {
185+
padding-bottom: ${space[0]}px;
186+
}
187+
`;
188+
189+
const playerName = css`
190+
${textSans14}
191+
${from.tablet} {
192+
${textSans15}
193+
}
194+
color: ${palette('--football-match-stat-text')};
195+
`;
196+
197+
const BackgroundRed = sourcePalette.news[400];
198+
199+
const BackgroundYellow = sourcePalette.brandAlt[300];
200+
201+
const rectangle = (color: string) => css`
202+
display: inline-block;
203+
width: ${space[3]}px;
204+
height: ${space[4]}px;
205+
border-radius: ${space[0]}px;
206+
margin-left: ${space[1]}px;
207+
background-color: ${color};
208+
`;
209+
210+
const substitute = css`
211+
${textSansBold12}
212+
border: 1px solid rgba(18, 18, 18, 0.20);
213+
border-radius: ${space[8]}px;
214+
padding: 0.5px ${space[1]}px 1.5px ${space[1]}px;
215+
display: flex;
216+
align-items: center;
217+
color: ${palette('--football-match-stat-text')};
218+
opacity: 0.6;
219+
gap: ${space[0]}px;
220+
221+
svg {
222+
fill: ${palette('--football-match-substitution-icon')};
223+
}
224+
`;

dotcom-rendering/src/paletteDeclarations.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7051,6 +7051,10 @@ const paletteColours = {
70517051
light: () => sourcePalette.neutral[93],
70527052
dark: () => sourcePalette.neutral[38],
70537053
},
7054+
'--football-match-info-background': {
7055+
light: () => sourcePalette.neutral[100],
7056+
dark: () => sourcePalette.neutral[10],
7057+
},
70547058
'--football-match-list-error': {
70557059
light: () => sourcePalette.error[400],
70567060
dark: () => sourcePalette.error[500],
@@ -7071,10 +7075,14 @@ const paletteColours = {
70717075
light: () => '#00679E', // replace with Source's `calculateHoverColour` when available
70727076
dark: () => '#00A1E6',
70737077
},
7074-
'--football-match-stat-name': {
7078+
'--football-match-stat-text': {
70757079
light: () => sourcePalette.neutral[7],
70767080
dark: () => sourcePalette.neutral[86],
70777081
},
7082+
'--football-match-substitution-icon': {
7083+
light: () => sourcePalette.neutral[46],
7084+
dark: () => sourcePalette.neutral[60],
7085+
},
70787086
'--football-score-border': {
70797087
light: () => sourcePalette.neutral[7],
70807088
dark: () => sourcePalette.neutral[7],
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)