Skip to content

Commit ed02afd

Browse files
authored
Image resizer for football crests (#14939)
Currently for football crests we are limited to two prebuilt sizes of image, 60px wide and 120px wide, hosted on `sport.guim.co.uk`. We would instead like to use the flexibility of the image resizer, on `i.guim.co.uk`, to request a greater range of image variants. This will allow us to choose more appropriate image dimensions for different designs, give us access to more image formats, and allow us to provide responsive images for different screen pixel densities. This PR also refactors several different implementations of the crest image element into a pair of components, `FootballCrest` and `FootballTableCrest`, and consolidates the building of the crest URL within one of these.
1 parent fc4c396 commit ed02afd

File tree

10 files changed

+189
-108
lines changed

10 files changed

+189
-108
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { generateImageURL } from '../lib/image';
2+
3+
type Props = {
4+
teamId: string;
5+
altText: string;
6+
width: number;
7+
className?: string;
8+
};
9+
10+
export const FootballCrest = (props: Props) => {
11+
const mainImage = crestUrl(props.teamId)?.toString();
12+
13+
if (mainImage === undefined) {
14+
return null;
15+
}
16+
17+
const lowRes = generateImageURL({
18+
mainImage,
19+
imageWidth: props.width,
20+
resolution: 'low',
21+
});
22+
const highRes = generateImageURL({
23+
mainImage,
24+
imageWidth: props.width,
25+
resolution: 'high',
26+
});
27+
28+
return (
29+
<img
30+
srcSet={`${lowRes}, ${highRes} 2x`}
31+
src={lowRes}
32+
alt={props.altText}
33+
className={props.className}
34+
/>
35+
);
36+
};
37+
38+
/**
39+
* @param teamId The PA ID of the team.
40+
*/
41+
const crestUrl = (teamId: string): URL | undefined => {
42+
try {
43+
return new URL(
44+
`${teamId}.png`,
45+
'https://sport.guim.co.uk/football/crests/',
46+
);
47+
} catch (e) {
48+
return undefined;
49+
}
50+
};

dotcom-rendering/src/components/FootballMatchList.tsx

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import type { Result } from '../lib/result';
3030
import { useHydrated } from '../lib/useHydrated';
3131
import { palette } from '../palette';
32+
import { FootballTableCrest } from './FootballTableCrest';
3233

3334
type Props = {
3435
initialDays: FootballMatches;
@@ -76,10 +77,6 @@ const footballMatchesGridStyles = css`
7677
}
7778
`;
7879

79-
function getFootballCrestImageUrl(teamId: string) {
80-
return `https://sport.guim.co.uk/football/crests/60/${teamId}.png`;
81-
}
82-
8380
const getTimeFormatter = (edition: EditionId): Intl.DateTimeFormat =>
8481
new Intl.DateTimeFormat(getLocaleFromEdition(edition), {
8582
hour: '2-digit',
@@ -301,28 +298,6 @@ const Match = ({
301298
</MatchWrapper>
302299
);
303300

304-
const FootballCrest = ({ teamId }: { teamId: string }) => (
305-
<div
306-
css={css`
307-
width: 1.25rem;
308-
height: 1.25rem;
309-
flex-shrink: 0;
310-
display: flex;
311-
justify-content: center;
312-
`}
313-
>
314-
<img
315-
css={css`
316-
max-width: 100%;
317-
max-height: 100%;
318-
object-fit: contain;
319-
`}
320-
src={getFootballCrestImageUrl(teamId)}
321-
alt=""
322-
/>
323-
</div>
324-
);
325-
326301
const HomeTeam = ({ team }: { team: Team }) => (
327302
<div
328303
css={css`
@@ -341,7 +316,7 @@ const HomeTeam = ({ team }: { team: Team }) => (
341316
>
342317
{team.name}
343318
</span>
344-
<FootballCrest teamId={team.id} />
319+
<FootballTableCrest teamId={team.id} />
345320
</div>
346321
);
347322

@@ -355,7 +330,7 @@ const AwayTeam = ({ team }: { team: Team }) => (
355330
gap: 0.325rem;
356331
`}
357332
>
358-
<FootballCrest teamId={team.id} />
333+
<FootballTableCrest teamId={team.id} />
359334
{team.name}
360335
</div>
361336
);

dotcom-rendering/src/components/FootballTable.tsx

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { css } from '@emotion/react';
22
import { from, textSans14, until } from '@guardian/source/foundations';
33
import type { FootballTable as FootballTableData } from '../footballTables';
44
import { palette } from '../palette';
5+
import { FootballTableCrest } from './FootballTableCrest';
56
import { FootballTableForm } from './FootballTableForm';
67

78
const tableStyles = css`
@@ -68,11 +69,6 @@ type Props = {
6869
guardianBaseUrl: string;
6970
};
7071

71-
function getFootballCrestImageUrl(teamId: string) {
72-
// todo: use fastly resizer
73-
return `https://sport.guim.co.uk/football/crests/60/${teamId}.png`;
74-
}
75-
7672
const FullTableLink = ({
7773
competitionName,
7874
competitionUrl,
@@ -87,28 +83,6 @@ const FullTableLink = ({
8783
</a>
8884
);
8985

90-
const FootballCrest = ({ teamId }: { teamId: string }) => (
91-
<div
92-
css={css`
93-
width: 1.25rem;
94-
height: 1.25rem;
95-
flex-shrink: 0;
96-
display: flex;
97-
justify-content: center;
98-
`}
99-
>
100-
<img
101-
css={css`
102-
max-width: 100%;
103-
max-height: 100%;
104-
object-fit: contain;
105-
`}
106-
src={getFootballCrestImageUrl(teamId)}
107-
alt=""
108-
/>
109-
</div>
110-
);
111-
11286
const TeamWithCrest = ({
11387
team,
11488
id,
@@ -125,7 +99,7 @@ const TeamWithCrest = ({
12599
gap: 0.325rem;
126100
`}
127101
>
128-
<FootballCrest teamId={id} />
102+
<FootballTableCrest teamId={id} />
129103
{url ? (
130104
<a
131105
css={css`
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FootballCrest } from './FootballCrest';
2+
3+
type Props = {
4+
teamId: string;
5+
};
6+
7+
/**
8+
* A specific, small version of the crest, used for football tables and lists
9+
* of fixtures and results.
10+
*/
11+
export const FootballTableCrest = (props: Props) => (
12+
<picture
13+
css={{
14+
width: '1.25rem',
15+
height: '1.25rem',
16+
flexShrink: 0,
17+
display: 'flex',
18+
justifyContent: 'center',
19+
}}
20+
>
21+
<FootballCrest
22+
teamId={props.teamId}
23+
altText=""
24+
width={20}
25+
css={{
26+
maxWidth: '100%',
27+
maxHeight: '100%',
28+
objectFit: 'contain',
29+
}}
30+
/>
31+
</picture>
32+
);

dotcom-rendering/src/components/TagPageHeader.stories.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,20 @@ export const WithLinkAndImage = {
2828
args: {
2929
title: 'Example title',
3030
description: `<p>And a much longer description with lots of text, other thoughts and musings <a href="#">and a link</a></p>`,
31-
image: 'https://uploads.guim.co.uk/2023/02/17/Josh_Halliday.jpg',
31+
image: {
32+
kind: 'byline',
33+
url: 'https://uploads.guim.co.uk/2023/02/17/Josh_Halliday.jpg',
34+
},
3235
},
3336
} satisfies Story;
3437

3538
export const WithFootballCrest = {
3639
args: {
3740
title: 'Aston Villa',
3841
description: `<p>And a much longer description with lots of text, other thoughts and musings <a href="#">and a link</a></p>`,
39-
image: 'https://sport.guim.co.uk/football/crests/120/2.png',
42+
image: {
43+
kind: 'footballCrest',
44+
teamId: '2',
45+
},
4046
},
4147
} satisfies Story;

dotcom-rendering/src/components/TagPageHeader.tsx

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { css, jsx } from '@emotion/react';
2-
import { isUndefined } from '@guardian/libs';
32
import {
43
breakpoints,
54
from,
@@ -13,12 +12,14 @@ import { Fragment, type ReactNode } from 'react';
1312
import { isElement, parseHtml } from '../lib/domUtils';
1413
import { palette as schemedPalette } from '../palette';
1514
import { logger } from '../server/lib/logging';
15+
import type { HeaderImage as HeaderImageModel } from '../types/tagPage';
16+
import { FootballCrest } from './FootballCrest';
1617
import { generateSources, Sources } from './Picture';
1718

1819
type Props = {
1920
title: string;
2021
description?: string;
21-
image?: string;
22+
image?: HeaderImageModel;
2223
};
2324

2425
const width = (columns: number, columnWidth: number, columnGap: number) =>
@@ -267,11 +268,7 @@ const imageStyle = css`
267268
}
268269
`;
269270

270-
const crestStyle = css`
271-
height: 5rem;
272-
`;
273-
274-
const Picture = ({ image }: { image: string }) => {
271+
const BylineImage = ({ image }: { image: string }) => {
275272
const sources = generateSources(image, [
276273
{ breakpoint: breakpoints.mobile, width: 80 },
277274
{ breakpoint: breakpoints.desktop, width: 100 },
@@ -282,16 +279,13 @@ const Picture = ({ image }: { image: string }) => {
282279
if (!fallback) throw new Error('Missing source');
283280

284281
return (
285-
<picture>
282+
<>
286283
<Sources sources={sources} />
287284
<img alt="" src={fallback} css={imageStyle} />
288-
</picture>
285+
</>
289286
);
290287
};
291288

292-
const isFootballCrest = (image: string) =>
293-
image.startsWith('https://sport.guim.co.uk/football/crests/');
294-
295289
export const TagPageHeader = ({ title, description, image }: Props) => {
296290
const descriptionFragment = description
297291
? parseHtml(description)
@@ -304,19 +298,7 @@ export const TagPageHeader = ({ title, description, image }: Props) => {
304298
<h2 css={titleStyle}>{title}</h2>
305299
</div>
306300

307-
{!isUndefined(image) && (
308-
<div css={[sectionImage, paddings]}>
309-
{isFootballCrest(image) ? (
310-
<img
311-
css={crestStyle}
312-
src={image}
313-
alt={`${title} football crest`}
314-
/>
315-
) : (
316-
<Picture image={image} />
317-
)}
318-
</div>
319-
)}
301+
<HeaderImage image={image} title={title} />
320302

321303
{descriptionFragment ? (
322304
<div css={[sectionContent, paddings, paragraphStyle]}>
@@ -330,3 +312,35 @@ export const TagPageHeader = ({ title, description, image }: Props) => {
330312
</section>
331313
);
332314
};
315+
316+
const HeaderImage = (props: { image: Props['image']; title: string }) => {
317+
if (props.image === undefined) {
318+
return null;
319+
}
320+
321+
switch (props.image.kind) {
322+
case 'byline':
323+
return (
324+
<HeaderPicture>
325+
<BylineImage image={props.image.url} />
326+
</HeaderPicture>
327+
);
328+
case 'footballCrest':
329+
return (
330+
<HeaderPicture>
331+
<FootballCrest
332+
teamId={props.image.teamId}
333+
altText={`${props.title} football crest`}
334+
width={140}
335+
css={{
336+
height: '5rem',
337+
}}
338+
/>
339+
</HeaderPicture>
340+
);
341+
}
342+
};
343+
344+
const HeaderPicture = (props: { children: ReactNode }) => (
345+
<picture css={[sectionImage, paddings]}>{props.children}</picture>
346+
);

dotcom-rendering/src/lib/image.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const getServiceFromUrl = (url: URL): string => {
3131
return 'static';
3232
case 'uploads':
3333
return 'uploads';
34+
case 'sport':
35+
return 'sport';
3436
case 'media':
3537
default:
3638
return 'media';

0 commit comments

Comments
 (0)