Skip to content

Commit

Permalink
Applied Styles To The Football Match List (#13244)
Browse files Browse the repository at this point in the history
* Applied styles to the football match list

To match the way it appears on frontend. Updated the stories to display more edge cases: narrow dates, uneven scores. Broke the component down into sub-components to which styles are applied. Added palette light and dark colours.

Also made use of `Intl.DateTimeFormat` to handle formatting the dates and times with appropriate locales and timezones for the different editions. Added functions to the `edition` module to get the timezone and locale for a given edition.

* Move the `grid` module into `src`

So it can be used outside storybook.

* Fix name shadowing in `grid` module

`from` was an import and a function argument.

* Fix name shadowing in `grid` module

`span` was the name of the function and one of its parameters.

---------

Co-authored-by: Marjan Kalanaki <[email protected]>
  • Loading branch information
JamieB-gu and marjisound authored Jan 30, 2025
1 parent 85ed8a8 commit 77940ac
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 45 deletions.
2 changes: 1 addition & 1 deletion dotcom-rendering/.storybook/decorators/gridDecorators.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css } from '@emotion/react';
import type { Decorator } from '@storybook/react';
import { grid } from './grid';
import { grid } from '../../src/grid';
import { from } from '@guardian/source/foundations';

/**
Expand Down
18 changes: 13 additions & 5 deletions dotcom-rendering/src/components/FootballLiveMatches.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import type { Meta, StoryObj } from '@storybook/react';
import { centreColumnDecorator } from '../../.storybook/decorators/gridDecorators';
import { FootballLiveMatches as FootballLiveMatchesComponent } from './FootballLiveMatches';

const meta = {
title: 'Components/Football Live Matches',
component: FootballLiveMatchesComponent,
decorators: [centreColumnDecorator],
decorators: [
// To make it easier to see the top border above the date
(Story) => (
<>
<div css={{ padding: 4 }}></div>
<Story />
</>
),
],
} satisfies Meta<typeof FootballLiveMatchesComponent>;

export default meta;
Expand All @@ -14,7 +21,8 @@ type Story = StoryObj<typeof meta>;

export const FootballLiveMatches = {
args: {
matches: [
edition: 'UK',
days: [
{
date: new Date('2025-01-24T00:00:00Z'),
competitions: [
Expand All @@ -24,11 +32,11 @@ export const FootballLiveMatches = {
nation: 'European',
matches: [
{
dateTime: new Date('2025-01-24T19:45:00Z'),
dateTime: new Date('2025-01-24T11:11:00Z'),
paId: '4482093',
homeTeam: {
name: 'Torino',
score: 1,
score: 10,
},
awayTeam: {
name: 'Cagliari',
Expand Down
271 changes: 241 additions & 30 deletions dotcom-rendering/src/components/FootballLiveMatches.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,247 @@
import { Fragment } from 'react';
import { css } from '@emotion/react';
import {
from,
headlineBold17,
space,
textSans14,
textSansBold14,
until,
} from '@guardian/source/foundations';
import { Fragment, type ReactNode } from 'react';
import type { FootballMatches } from '../footballMatches';
import { grid } from '../grid';
import {
type EditionId,
getLocaleFromEdition,
getTimeZoneFromEdition,
} from '../lib/edition';
import { palette } from '../palette';

type Props = {
matches: FootballMatches;
days: FootballMatches;
edition: EditionId;
};

export const FootballLiveMatches = ({ matches }: Props) => (
<>
{matches.map((day) => (
<Fragment key={day.date.toISOString()}>
<h2>{day.date.toString()}</h2>
{day.competitions.map((competition) => (
<Fragment key={competition.competitionId}>
<h3>{competition.name}</h3>
<ul>
{competition.matches.map((match) => (
<li key={match.paId}>
<time
dateTime={match.dateTime.toISOString()}
>
{match.dateTime.getUTCHours()}:
{match.dateTime.getUTCMinutes()}
</time>
{match.homeTeam.name} {match.homeTeam.score}
-{match.awayTeam.score}{' '}
{match.awayTeam.name}
</li>
))}
</ul>
</Fragment>
))}
</Fragment>
))}
</>
const getDateFormatter = (edition: EditionId): Intl.DateTimeFormat =>
new Intl.DateTimeFormat('en-GB', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: getTimeZoneFromEdition(edition),
});

const getTimeFormatter = (edition: EditionId): Intl.DateTimeFormat =>
new Intl.DateTimeFormat(getLocaleFromEdition(edition), {
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
hour12: false,
timeZone: getTimeZoneFromEdition(edition),
});

const Day = (props: { children: ReactNode }) => (
<h2
css={css`
${textSansBold14}
${grid.column.centre}
border-top: 1px solid ${palette('--football-match-list-border')};
padding-top: ${space[2]}px;
${from.leftCol} {
padding-bottom: ${space[6]}px;
${grid.between('left-column-start', 'centre-column-end')}
}
`}
>
{props.children}
</h2>
);

const CompetitionName = (props: { children: ReactNode }) => (
<h3
css={css`
${textSansBold14}
${grid.column.centre}
color: ${palette('--football-match-list-competition-text')};
border-top: 1px solid ${palette('--football-match-list-top-border')};
padding: ${space[2]}px;
background-color: ${palette('--football-match-list-background')};
margin-top: ${space[9]}px;
${from.leftCol} {
border-top-color: ${palette('--football-match-list-border')};
background-color: transparent;
margin-top: 0;
padding: ${space[1]}px 0 0;
${grid.column.left}
${headlineBold17}
}
`}
>
{props.children}
</h3>
);

const Matches = (props: { children: ReactNode }) => (
<ul
{...props}
css={css`
${grid.column.centre}
${from.leftCol} {
padding-bottom: ${space[9]}px;
}
`}
/>
);

const Match = (props: { children: ReactNode }) => (
<li
{...props}
css={css`
${textSans14}
background-color: ${palette('--football-match-list-background')};
padding: ${space[2]}px;
display: flex;
border: 1px solid ${palette('--football-match-list-border')};
${until.mobileMedium} {
flex-wrap: wrap;
}
${from.leftCol} {
&:first-of-type {
border-top-color: ${palette(
'--football-match-list-top-border',
)};
}
}
`}
/>
);

const MatchTime = (props: { children: ReactNode; dateTime: string }) => (
<time
{...props}
css={css`
width: 5rem;
${until.mobileMedium} {
flex-basis: 100%;
}
`}
/>
);

const HomeTeam = (props: { children: ReactNode }) => (
<span
{...props}
css={css`
text-align: right;
flex: 1 0 0;
padding-right: 1rem;
`}
/>
);

const AwayTeam = (props: { children: ReactNode }) => (
<span
{...props}
css={css`
flex: 1 0 0;
padding-left: 1rem;
`}
/>
);

const Battleline = () => (
<span
css={css`
display: block;
padding: 0 4px;
&:before {
content: '-';
}
`}
/>
);

const Scores = ({
homeScore,
awayScore,
}: {
homeScore: number;
awayScore: number;
}) => (
<span
css={css`
width: 3rem;
display: flex;
`}
>
<span
css={css`
text-align: right;
flex: 1 0 0;
`}
>
{homeScore}
</span>
<Battleline />
<span
css={css`
text-align: left;
flex: 1 0 0;
`}
>
{awayScore}
</span>
</span>
);

export const FootballLiveMatches = ({ edition, days }: Props) => {
const dateFormatter = getDateFormatter(edition);
const timeFormatter = getTimeFormatter(edition);

return (
<>
{days.map((day) => (
<section css={css(grid.container)} key={day.date.toISOString()}>
<Day>{dateFormatter.format(day.date)}</Day>
{day.competitions.map((competition) => (
<Fragment key={competition.competitionId}>
<CompetitionName>
{competition.name}
</CompetitionName>
<Matches>
{competition.matches.map((match) => (
<Match key={match.paId}>
<MatchTime
dateTime={match.dateTime.toISOString()}
>
{timeFormatter.format(
match.dateTime,
)}
</MatchTime>
<HomeTeam>
{match.homeTeam.name}
</HomeTeam>
<Scores
homeScore={match.homeTeam.score}
awayScore={match.awayTeam.score}
/>
<AwayTeam>
{match.awayTeam.name}
</AwayTeam>
</Match>
))}
</Matches>
</Fragment>
))}
</section>
))}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// ----- Imports ----- //

import { from } from '@guardian/source/foundations';
import { from as fromBreakpoint } from '@guardian/source/foundations';

// ----- Columns & Lines ----- //

Expand Down Expand Up @@ -38,23 +38,23 @@ const container = `
grid-template-columns: ${mobileColumns};
column-gap: ${mobileColumnGap};
${from.mobileLandscape} {
${fromBreakpoint.mobileLandscape} {
column-gap: ${columnGap};
}
${from.tablet} {
${fromBreakpoint.tablet} {
grid-template-columns: ${tabletColumns};
}
${from.desktop} {
${fromBreakpoint.desktop} {
grid-template-columns: ${desktopColumns};
}
${from.leftCol} {
${fromBreakpoint.leftCol} {
grid-template-columns: ${leftColColumns};
}
${from.wide} {
${fromBreakpoint.wide} {
grid-template-columns: ${wideColumns};
}
`;
Expand Down Expand Up @@ -86,16 +86,16 @@ const between = (from: Line | number, to: Line | number): string => `
* Ask the element to span a number of grid columns, starting at a specific
* grid line. The line can be specified either by `Line` name or by number.
* @param start The grid line to start from, either a `Line` name or a number.
* @param span The number of columns to span.
* @param columns The number of columns to span.
* @returns {string} CSS to place the element on the grid.
*
* @example <caption>The element will span 3 columns from the line.</caption>
* const styles = css`
* ${grid.span('centre-column-start', 3)}
* `;
*/
const span = (start: Line | number, span: number): string => `
grid-column: ${start} / span ${span};
const span = (start: Line | number, columns: number): string => `
grid-column: ${start} / span ${columns};
`;

/**
Expand Down
Loading

0 comments on commit 77940ac

Please sign in to comment.