Skip to content

Commit 223e947

Browse files
committed
Merge remote-tracking branch 'origin/main' into add-polling-for-match-header
2 parents 1a27c1e + e50f02b commit 223e947

12 files changed

Lines changed: 403 additions & 122 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
interface Props {
3+
testName: string;
4+
}
5+
6+
const { testName }: Props = $props();
7+
</script>
8+
9+
<a
10+
href={`https://metrics.gutools.co.uk/d/bfd4abner943ke/page-views?folderUid=efd48ip6ch3i8f&orgId=1&from=now-7d&to=now&var-test_name=${testName}`}
11+
target="_blank"
12+
>
13+
Grafana
14+
</a>

ab-testing/frontend/src/lib/components/OphanLink.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
href={`https://dashboard.ophan.co.uk/graph/breakdown?day=today&ab=${testName}`}
1111
target="_blank"
1212
>
13-
graph
13+
Ophan
1414
</a>

ab-testing/frontend/src/lib/components/TableFixed.svelte

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script lang="ts">
2-
import type { ABTest } from '../../../../types.js';
3-
import OphanLink from '$lib/components/OphanLink.svelte';
4-
import TestVariants from '$lib/components/TestVariants.svelte';
2+
import type { ABTest } from "../../../../types.js";
3+
import OphanLink from "$lib/components/OphanLink.svelte";
4+
import TestVariants from "$lib/components/TestVariants.svelte";
5+
import GrafanaLink from "./GrafanaLink.svelte";
56
67
interface Props {
78
tests: ABTest[];
@@ -39,7 +40,7 @@
3940
<th scope="col">Test Groups</th>
4041
<th scope="col">Expires In</th>
4142
<th scope="col">Audience</th>
42-
<th scope="col">Ophan</th>
43+
<th scope="col">Page Views</th>
4344
</tr>
4445
</thead>
4546
<tbody>
@@ -49,7 +50,7 @@
4950
>
5051
<td
5152
class="status"
52-
class:off={test.status === 'OFF'}
53+
class:off={test.status === "OFF"}
5354
class:expired
5455
>
5556
{#if expired}
@@ -69,7 +70,11 @@
6970
>{daysToExpiry(test.expirationDate)} days</td
7071
>
7172
<td>{test.audienceSize * 100}%</td>
72-
<td><OphanLink testName={test.name} /></td>
73+
<td>
74+
<GrafanaLink testName={test.name} /> | <OphanLink
75+
testName={test.name}
76+
/>
77+
</td>
7378
</tr>
7479
<tr>
7580
<th scope="row">Description</th>
@@ -106,7 +111,7 @@
106111
padding: 8px;
107112
}
108113
109-
th[scope='col'] {
114+
th[scope="col"] {
110115
background-color: var(--light-grey);
111116
}
112117
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { CallToActionAtom } from './CallToActionAtom';
2+
3+
export default {
4+
component: CallToActionAtom,
5+
title: 'Components/CallToActionAtom',
6+
};
7+
8+
export const Default = () => {
9+
return (
10+
<CallToActionAtom
11+
linkUrl="https://safety.epicgames.com/en-US?lang=en-US"
12+
backgroundImage="https://media.guim.co.uk/7fe58f11470360bc9f1e4b6bbcbf45d7cf06cfcf/0_0_1300_375/1300.jpg"
13+
text="This is a call to action text"
14+
buttonText="Learn more"
15+
/>
16+
);
17+
};
18+
19+
Default.storyName = 'default';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { css } from '@emotion/react';
2+
import {
3+
from,
4+
palette as sourcePalette,
5+
textSansBold20,
6+
} from '@guardian/source/foundations';
7+
import { Button, SvgExternal } from '@guardian/source/react-components';
8+
9+
type Props = {
10+
linkUrl: string;
11+
backgroundImage: string;
12+
text: string;
13+
buttonText: string;
14+
};
15+
16+
export const CallToActionAtom = ({
17+
linkUrl,
18+
backgroundImage,
19+
text,
20+
buttonText,
21+
}: Props) => {
22+
return (
23+
<a
24+
href={linkUrl}
25+
css={css`
26+
text-decoration: none;
27+
`}
28+
>
29+
<picture
30+
css={css`
31+
position: relative;
32+
display: flex;
33+
`}
34+
>
35+
<img
36+
src={backgroundImage}
37+
alt={''}
38+
css={css`
39+
height: 200px;
40+
object-fit: cover;
41+
42+
${from.tablet} {
43+
height: 250px;
44+
}
45+
${from.leftCol} {
46+
height: 375px;
47+
}
48+
`}
49+
/>
50+
<div
51+
css={css`
52+
position: absolute;
53+
bottom: 10%;
54+
left: 10%;
55+
transform: translate(-10%, -10%);
56+
`}
57+
>
58+
<h2
59+
css={css`
60+
${textSansBold20}
61+
margin-bottom: 8px;
62+
color: white;
63+
`}
64+
>
65+
{text}
66+
</h2>
67+
<Button
68+
iconSide="right"
69+
size="small"
70+
icon={<SvgExternal />}
71+
theme={{
72+
textPrimary: sourcePalette.neutral[7],
73+
backgroundPrimary: sourcePalette.neutral[97],
74+
backgroundPrimaryHover: sourcePalette.neutral[73],
75+
}}
76+
>
77+
{buttonText}
78+
</Button>
79+
</div>
80+
</picture>
81+
</a>
82+
);
83+
};

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,9 @@ export const OtherCompetition = {
3131
pageId: 'football/premierleague/table',
3232
},
3333
} satisfies Story;
34+
35+
export const WinterOlympics = {
36+
args: {
37+
pageId: 'sport/winter-olympics-2026',
38+
},
39+
} satisfies Story;

dotcom-rendering/src/components/DirectoryPageNav.tsx

Lines changed: 102 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { css } from '@emotion/react';
22
import {
3+
type Breakpoint,
4+
breakpoints,
35
from,
46
headlineBold15Object,
57
headlineBold17Object,
@@ -10,6 +12,7 @@ import {
1012
palette,
1113
} from '@guardian/source/foundations';
1214
import { grid } from '../grid';
15+
import { generateImageURL } from '../lib/image';
1316
import type { TagType } from '../types/tag';
1417

1518
type Props = {
@@ -43,7 +46,7 @@ const configs = [
4346
],
4447
tagIds: [],
4548
textColor: palette.neutral[7],
46-
backgroundColor: '#CCCCCC',
49+
backgroundColor: '#22B24B',
4750
title: {
4851
label: 'Winter Olympics 2026',
4952
id: 'sport/winter-olympics-2026',
@@ -79,31 +82,6 @@ const configs = [
7982
},
8083
] satisfies DirectoryPageNavConfig[];
8184

82-
const backgroundImageStyles = (
83-
images?: DirectoryPageNavConfig['backgroundImages'],
84-
) => {
85-
if (!images) return {};
86-
87-
return {
88-
backgroundImage: `url(${images.mobile})`,
89-
backgroundSize: 'cover',
90-
backgroundPosition: 'top center',
91-
92-
[from.mobileLandscape]: {
93-
backgroundImage: `url(${images.mobileLandscape})`,
94-
},
95-
[from.phablet]: {
96-
backgroundImage: `url(${images.phablet})`,
97-
},
98-
[from.tablet]: {
99-
backgroundImage: `url(${images.tablet})`,
100-
},
101-
[from.desktop]: {
102-
backgroundImage: `url(${images.desktop})`,
103-
},
104-
};
105-
};
106-
10785
export const DirectoryPageNav = ({ pageId, pageTags }: Props) => {
10886
const config = configs.find(
10987
(cfg) =>
@@ -123,21 +101,14 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => {
123101
backgroundColor,
124102
'&': css(grid.paddedContainer),
125103
alignContent: 'space-between',
126-
height: 116,
127-
[from.tablet]: {
128-
height: 140,
129-
},
130-
[from.desktop]: {
131-
height: 150,
132-
},
133-
...backgroundImageStyles(config.backgroundImages),
134104
});
135105

136106
const largeLinkStyles = css({
137107
...headlineBold24Object,
138108
color: textColor,
139109
textDecoration: 'none',
140110
'&': css(grid.column.centre),
111+
gridRow: 1,
141112
[from.tablet]: headlineBold42Object,
142113
[from.leftCol]: css(
143114
grid.between('left-column-start', 'right-column-end'),
@@ -148,6 +119,8 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => {
148119
display: 'flex',
149120
flexWrap: 'wrap',
150121
'&': css(grid.column.all),
122+
gridRow: 2,
123+
alignSelf: 'end',
151124
position: 'relative',
152125
'--top-border-gap': '1.55rem',
153126
[from.mobileLandscape]: {
@@ -225,7 +198,8 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => {
225198
});
226199

227200
return (
228-
<nav css={nav}>
201+
<nav css={[nav, heightStyles]}>
202+
<BackgroundImage images={config.backgroundImages} />
229203
<a href={`/${config.title.id}`} css={largeLinkStyles}>
230204
{config.title.label}
231205
</a>
@@ -252,3 +226,96 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => {
252226
</nav>
253227
);
254228
};
229+
230+
const heightStyles = css({
231+
height: 116,
232+
[from.tablet]: {
233+
height: 140,
234+
},
235+
[from.desktop]: {
236+
height: 150,
237+
},
238+
});
239+
240+
const BackgroundImage = (props: {
241+
images: DirectoryPageNavConfig['backgroundImages'];
242+
}) => {
243+
if (props.images === undefined) {
244+
return null;
245+
}
246+
247+
return (
248+
<picture
249+
css={[
250+
{
251+
'&': css(grid.column.all),
252+
gridRow: '1/3',
253+
},
254+
heightStyles,
255+
]}
256+
>
257+
<Source images={props.images} breakpoint="wide" />
258+
<Source images={props.images} breakpoint="leftCol" />
259+
<Source images={props.images} breakpoint="desktop" />
260+
<Source images={props.images} breakpoint="tablet" />
261+
<Source images={props.images} breakpoint="phablet" />
262+
<Source images={props.images} breakpoint="mobileLandscape" />
263+
<Source images={props.images} breakpoint="mobileMedium" />
264+
<Source images={props.images} breakpoint="mobile" />
265+
<img
266+
src={generateImageURL({
267+
mainImage: props.images.mobile,
268+
imageWidth: breakpoints.mobileMedium,
269+
resolution: 'low',
270+
})}
271+
alt="Winter Olympics background graphic"
272+
css={{
273+
width: '100%',
274+
height: '100%',
275+
objectFit: 'cover',
276+
objectPosition: 'top',
277+
}}
278+
/>
279+
</picture>
280+
);
281+
};
282+
283+
const Source = (props: { images: Images; breakpoint: Breakpoint }) => (
284+
<source
285+
media={`(min-width: ${breakpoints[props.breakpoint]}px)`}
286+
srcSet={`${generateImageURL({
287+
mainImage: props.images[breakpointToImageSize(props.breakpoint)],
288+
imageWidth: breakpoints[props.breakpoint],
289+
resolution: 'low',
290+
})}, ${generateImageURL({
291+
mainImage: props.images[breakpointToImageSize(props.breakpoint)],
292+
imageWidth: breakpoints[props.breakpoint],
293+
resolution: 'high',
294+
})} 2x`}
295+
/>
296+
);
297+
298+
/**
299+
* We don't have an image for every breakpoint, so this picks an appropriate
300+
* image size in each case.
301+
*/
302+
const breakpointToImageSize = (breakpoint: Breakpoint): ImageSize => {
303+
switch (breakpoint) {
304+
case 'mobile':
305+
case 'mobileMedium':
306+
return 'mobile';
307+
case 'mobileLandscape':
308+
return 'mobileLandscape';
309+
case 'phablet':
310+
return 'phablet';
311+
case 'tablet':
312+
return 'tablet';
313+
case 'desktop':
314+
case 'leftCol':
315+
case 'wide':
316+
return 'desktop';
317+
}
318+
};
319+
320+
type Images = Exclude<DirectoryPageNavConfig['backgroundImages'], undefined>;
321+
type ImageSize = keyof Images;

0 commit comments

Comments
 (0)