Skip to content

Commit fab0f99

Browse files
committed
Localised kick off time helper and tests
1 parent e814fc0 commit fab0f99

File tree

4 files changed

+121
-53
lines changed

4 files changed

+121
-53
lines changed
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import type { Meta, StoryObj } from '@storybook/react-webpack5';
2-
import { FootballPreMatchDetails } from './FootballPreMatchDetails';
2+
import { FootballPreMatchDetails as FootballPreMatchDetailsComponent } from './FootballPreMatchDetails';
33

44
const meta = {
55
title: 'Components/Football Pre-Match Details',
6-
component: FootballPreMatchDetails,
6+
component: FootballPreMatchDetailsComponent,
77
parameters: {
88
layout: 'padded',
99
},
10-
} satisfies Meta<typeof FootballPreMatchDetails>;
10+
} satisfies Meta<typeof FootballPreMatchDetailsComponent>;
1111

1212
export default meta;
1313
type Story = StoryObj<typeof meta>;
1414

15-
export const Default = {
15+
export const FootballPreMatchDetails = {
1616
args: {
1717
homeTeam: 'Man United',
1818
awayTeam: 'Arsenal',
1919
league: 'Premier League',
2020
venue: 'Old Trafford',
21-
kickOff: new Date('2026-02-15T17:00:00Z'),
21+
kickOff: new Date('2026-02-15T17:30:00Z'),
2222
edition: 'UK',
2323
},
2424
} satisfies Story;

dotcom-rendering/src/components/FootballPreMatchDetails.tsx

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@ import {
1313
LinkButton,
1414
SvgArrowRightStraight,
1515
} from '@guardian/source/react-components';
16-
import {
17-
type EditionId,
18-
getLocaleFromEdition,
19-
getTimeZoneFromEdition,
20-
} from '../lib/edition';
16+
import type { EditionId } from '../lib/edition';
17+
import { formatMatchKickOffTime } from '../lib/formatDate';
2118
import { palette } from '../palette';
2219

2320
const containerCss = css`
@@ -72,47 +69,33 @@ export const FootballPreMatchDetails = ({
7269
venue,
7370
kickOff,
7471
edition,
75-
}: PreMatchProps) => {
76-
const kickOffTime = new Intl.DateTimeFormat(getLocaleFromEdition(edition), {
77-
hour: 'numeric',
78-
minute: 'numeric',
79-
hour12: true,
80-
weekday: 'long',
81-
day: 'numeric',
82-
month: 'short',
83-
year: 'numeric',
84-
timeZoneName: 'short',
85-
timeZone: getTimeZoneFromEdition(edition),
86-
}).format(kickOff);
87-
88-
console.log(new Date());
89-
90-
return (
91-
<div css={containerCss}>
92-
<h3 css={headingCss}>
93-
{homeTeam} vs. {awayTeam}
94-
</h3>
95-
<div css={detailsCss}>
96-
<span>{league}</span>
97-
<span>{venue}</span>
98-
<time css={kickOffCss}>{kickOffTime}</time>
99-
</div>
100-
<LinkButton
101-
href="/football/fixtures"
102-
size="xsmall"
103-
priority="tertiary"
104-
icon={<SvgArrowRightStraight />}
105-
iconSide="right"
106-
theme={{
107-
textTertiary: palette('--football-pre-match-button'),
108-
borderTertiary: palette('--football-pre-match-button'),
109-
backgroundTertiaryHover: palette(
110-
'--football-pre-match-button-hover',
111-
),
112-
}}
113-
>
114-
Today's fixtures
115-
</LinkButton>
72+
}: PreMatchProps) => (
73+
<div css={containerCss}>
74+
<h3 css={headingCss}>
75+
{homeTeam} vs. {awayTeam}
76+
</h3>
77+
<div css={detailsCss}>
78+
<span>{league}</span>
79+
<span>{venue}</span>
80+
<time css={kickOffCss}>
81+
{formatMatchKickOffTime(kickOff, edition)}
82+
</time>
11683
</div>
117-
);
118-
};
84+
<LinkButton
85+
href="/football/fixtures"
86+
size="xsmall"
87+
priority="tertiary"
88+
icon={<SvgArrowRightStraight />}
89+
iconSide="right"
90+
theme={{
91+
textTertiary: palette('--football-pre-match-button'),
92+
borderTertiary: palette('--football-pre-match-button'),
93+
backgroundTertiaryHover: palette(
94+
'--football-pre-match-button-hover',
95+
),
96+
}}
97+
>
98+
Today's fixtures
99+
</LinkButton>
100+
</div>
101+
);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { formatMatchKickOffTime } from './formatDate';
2+
3+
describe('formatMatchKickOffTime', () => {
4+
it('should return a localised kick off date and time', () => {
5+
expect(
6+
formatMatchKickOffTime(new Date('2026-02-15T19:30:00Z'), 'UK'),
7+
).toBe('Sunday, 15 Feb 2026, 7:30 pm GMT');
8+
expect(
9+
formatMatchKickOffTime(new Date('2026-02-15T19:30:00Z'), 'EUR'),
10+
).toBe('Sunday, 15 Feb 2026, 8:30 pm CET');
11+
expect(
12+
formatMatchKickOffTime(new Date('2026-02-15T19:30:00Z'), 'AU'),
13+
).toBe('Monday 16 Feb 2026, 6:30 am AEDT');
14+
});
15+
16+
it('should display "Today" if the match is today in the current timezone', () => {
17+
jest.useFakeTimers().setSystemTime(new Date('2026-02-15T19:30:00Z'));
18+
19+
expect(
20+
formatMatchKickOffTime(new Date(`2026-02-15T19:30:00Z`), 'UK'),
21+
).toBe('Today, 7:30 pm GMT');
22+
expect(
23+
formatMatchKickOffTime(new Date(`2026-02-15T19:30:00Z`), 'US'),
24+
).toBe('Today, 2:30 PM EST');
25+
expect(
26+
formatMatchKickOffTime(new Date(`2026-02-15T20:00:00Z`), 'AU'),
27+
).toBe('Today, 7:00 am AEDT');
28+
expect(
29+
formatMatchKickOffTime(new Date(`2026-02-15T23:00:00Z`), 'EUR'),
30+
).toBe('Monday, 16 Feb 2026, 12:00 am CET');
31+
expect(
32+
formatMatchKickOffTime(new Date(`2026-02-15T01:00:00Z`), 'US'),
33+
).toBe('Saturday, Feb 14, 2026, 8:00 PM EST');
34+
});
35+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
type EditionId,
3+
getLocaleFromEdition,
4+
getTimeZoneFromEdition,
5+
} from './edition';
6+
7+
/**
8+
* Returns a localised date and time string for the given match kick off time.
9+
* If the match is today in the current timezone, 'Today' is shown instead of
10+
* the full date.
11+
*
12+
* Sunday, 15 Feb 2026, 7:30 pm GMT
13+
* Today, 7:30 pm GMT
14+
*/
15+
export const formatMatchKickOffTime = (kickOff: Date, edition: EditionId) => {
16+
const locale = getLocaleFromEdition(edition);
17+
const timeZone = getTimeZoneFromEdition(edition);
18+
19+
const getLocalisedDateString = (date: Date) =>
20+
date.toLocaleDateString(locale, {
21+
year: 'numeric',
22+
month: 'numeric',
23+
day: 'numeric',
24+
timeZone,
25+
});
26+
27+
const isToday =
28+
getLocalisedDateString(kickOff) === getLocalisedDateString(new Date());
29+
30+
const timeFormatter = new Intl.DateTimeFormat(locale, {
31+
hour: 'numeric',
32+
minute: 'numeric',
33+
hour12: true,
34+
timeZoneName: 'short',
35+
timeZone: timeZone,
36+
});
37+
38+
const dateFormatter = new Intl.DateTimeFormat(locale, {
39+
day: 'numeric',
40+
month: 'short',
41+
year: 'numeric',
42+
weekday: 'long',
43+
timeZone: timeZone,
44+
});
45+
46+
const formattedDate = isToday ? 'Today' : dateFormatter.format(kickOff);
47+
const formattedTime = timeFormatter.format(kickOff);
48+
49+
return `${formattedDate}, ${formattedTime}`;
50+
};

0 commit comments

Comments
 (0)