Skip to content

Commit 22e226c

Browse files
committed
Implement onwards content gallery-style AB test
1 parent 134b52d commit 22e226c

File tree

6 files changed

+356
-7
lines changed

6 files changed

+356
-7
lines changed

dotcom-rendering/src/components/Card/Card.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export type Props = {
163163
/** Determines if the headline should be positioned within the content or outside the content */
164164
headlinePosition?: 'inner' | 'outer';
165165
enableHls?: boolean;
166+
isInOnwardsAbTestVariantStandardCard?: boolean;
166167
};
167168

168169
const starWrapper = (cardHasImage: boolean) => css`
@@ -421,6 +422,7 @@ export const Card = ({
421422
headlinePosition = 'inner',
422423
subtitleSize = 'small',
423424
enableHls = false,
425+
isInOnwardsAbTestVariantStandardCard,
424426
}: Props) => {
425427
const hasSublinks = supportingContent && supportingContent.length > 0;
426428
const sublinkPosition = decideSublinkPosition(
@@ -668,7 +670,12 @@ export const Card = ({
668670
*/
669671
const getGapSizes = (): GapSizes => {
670672
if (isOnwardContent && !isGallerySecondaryOnward) {
671-
if (isMoreGalleriesOnwardContent) {
673+
if (
674+
isMoreGalleriesOnwardContent ||
675+
// This is untidy. If we implement the gallery-style redesign for onwards content
676+
// in all articles, we can refactor how we determine gap sizes for onwards cards.
677+
isInOnwardsAbTestVariantStandardCard
678+
) {
672679
return {
673680
row: 'small',
674681
column: 'small',

dotcom-rendering/src/components/MoreGalleries.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ const cardsContainerStyles = css`
5353
${from.desktop} {
5454
${grid.between('centre-column-start', 'right-column-end')}
5555
}
56-
57-
${from.leftCol} {
58-
${grid.between('centre-column-start', 'right-column-end')}
59-
}
6056
`;
6157

6258
const standardCardStyles = css`
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import { css } from '@emotion/react';
2+
import {
3+
from,
4+
headlineBold24,
5+
space,
6+
until,
7+
} from '@guardian/source/foundations';
8+
import { StraightLines } from '@guardian/source-development-kitchen/react-components';
9+
import { grid } from '../grid';
10+
import { decideFormat } from '../lib/articleFormat';
11+
import { useApi } from '../lib/useApi';
12+
import { palette } from '../palette';
13+
import type { DCRFrontCard } from '../types/front';
14+
import type { FETrailType } from '../types/trails';
15+
import { Card } from './Card/Card';
16+
import type { Props as CardProps } from './Card/Card';
17+
import { LeftColumn } from './LeftColumn';
18+
import { Placeholder } from './Placeholder';
19+
20+
type OnwardsResponse = {
21+
trails: FETrailType[];
22+
heading: string;
23+
displayname: string;
24+
description: string;
25+
};
26+
27+
const standardCardListStyles = css`
28+
width: 100%;
29+
display: grid;
30+
gap: 20px;
31+
32+
${from.tablet} {
33+
grid-template-columns: repeat(4, 1fr);
34+
padding-top: ${space[2]}px;
35+
}
36+
37+
${from.leftCol} {
38+
&::before {
39+
content: '';
40+
position: absolute;
41+
left: -11px;
42+
bottom: 0;
43+
top: 8px;
44+
width: 1px;
45+
background-color: ${palette('--onward-content-border')};
46+
}
47+
}
48+
`;
49+
50+
const cardsContainerStyles = css`
51+
${grid.column.centre}
52+
position: relative;
53+
54+
${from.desktop} {
55+
${grid.between('centre-column-start', 'right-column-end')}
56+
}
57+
`;
58+
59+
const splashCardStyles = css`
60+
margin-bottom: ${space[4]}px;
61+
${from.leftCol} {
62+
margin-top: ${space[2]}px;
63+
64+
&::before {
65+
content: '';
66+
position: absolute;
67+
left: -11px;
68+
top: 8px;
69+
bottom: 0;
70+
width: 1px;
71+
background-color: ${palette('--onward-content-border')};
72+
}
73+
}
74+
`;
75+
76+
const standardCardStyles = css`
77+
position: relative;
78+
display: flex;
79+
80+
${from.tablet} {
81+
:not(:first-child)::before {
82+
content: '';
83+
position: absolute;
84+
top: 0;
85+
bottom: 0;
86+
left: -11px;
87+
width: 1px;
88+
background: ${palette('--onward-content-border')};
89+
}
90+
}
91+
`;
92+
93+
const containerStyles = css`
94+
display: flex;
95+
flex-direction: column;
96+
padding-bottom: ${space[6]}px;
97+
98+
${from.leftCol} {
99+
flex-direction: row;
100+
padding-right: 80px;
101+
}
102+
`;
103+
104+
const headerStyles = css`
105+
${headlineBold24};
106+
color: ${palette('--carousel-text')};
107+
padding-bottom: ${space[2]}px;
108+
padding-top: ${space[1]}px;
109+
margin-left: 0;
110+
`;
111+
112+
const mobileHeaderStyles = css`
113+
${headerStyles};
114+
${from.tablet} {
115+
padding-left: 4px;
116+
}
117+
${from.leftCol} {
118+
display: none;
119+
}
120+
`;
121+
122+
const convertFETrailToDcrTrail = (
123+
trails: FETrailType[],
124+
discussionApiUrl: string,
125+
): DCRFrontCard[] =>
126+
trails.map((trail) => ({
127+
dataLinkName: 'onwards-content-card',
128+
discussionId: trail.discussion?.discussionId,
129+
discussionApiUrl,
130+
format: decideFormat(trail.format),
131+
headline: trail.headline,
132+
image: {
133+
src: trail.masterImage ?? '',
134+
altText: trail.linkText ?? '',
135+
},
136+
isExternalLink: false,
137+
onwardsSource: 'related-content',
138+
showLivePlayable: false,
139+
showQuotedHeadline: false,
140+
url: trail.url,
141+
webPublicationDate: trail.webPublicationDate,
142+
isImmersive: false,
143+
}));
144+
145+
const getDefaultCardProps = (
146+
trail: DCRFrontCard,
147+
isInOnwardsAbTestVariantStandardCard: boolean,
148+
) => {
149+
const defaultProps: CardProps = {
150+
aspectRatio: '5:4',
151+
avatarUrl: trail.avatarUrl,
152+
branding: trail.branding,
153+
byline: trail.byline,
154+
dataLinkName: `onwards-content-gallery-style ${trail.dataLinkName}`,
155+
discussionApiUrl: trail.discussionApiUrl,
156+
discussionId: trail.discussionId,
157+
format: trail.format,
158+
headlineText: trail.headline,
159+
image: trail.image,
160+
imageLoading: 'lazy',
161+
isCrossword: trail.isCrossword,
162+
isExternalLink: false,
163+
isInOnwardsAbTestVariantStandardCard,
164+
isOnwardContent: true,
165+
kickerText: trail.kickerText,
166+
linkTo: trail.url,
167+
mainMedia: trail.mainMedia,
168+
onwardsSource: 'related-content',
169+
showAge: false,
170+
showByline: trail.showByline,
171+
showClock: false,
172+
showPulsingDot: false,
173+
showQuotedHeadline: trail.showQuotedHeadline,
174+
showTopBarDesktop: false,
175+
showTopBarMobile: false,
176+
snapData: trail.snapData,
177+
starRating: trail.starRating,
178+
trailText: trail.trailText,
179+
webPublicationDate: trail.webPublicationDate,
180+
};
181+
182+
return defaultProps;
183+
};
184+
185+
type Props = {
186+
url: string;
187+
discussionApiUrl: string;
188+
isInOnwardsAbTestVariant?: boolean;
189+
};
190+
191+
/**
192+
* We are running an AB test to use the More Galleries style of onwards content on all articles.
193+
* If the test is successful, this component will likely be removed the MoreGalleries.tsx will
194+
* be generalised so that is can be used for onwards content across all articles.
195+
*/
196+
export const MoreGalleriesStyleOnwardsContent = ({
197+
url,
198+
discussionApiUrl,
199+
isInOnwardsAbTestVariant,
200+
}: Props) => {
201+
const { data, error } = useApi<OnwardsResponse>(url);
202+
203+
if (error) {
204+
// Send the error to Sentry and then prevent the element from rendering
205+
window.guardian.modules.sentry.reportError(error, 'onwards-lower');
206+
return null;
207+
}
208+
209+
if (!data?.trails) {
210+
return (
211+
<Placeholder
212+
heights={
213+
new Map([
214+
['mobile', 900],
215+
['tablet', 600],
216+
['desktop', 900],
217+
])
218+
}
219+
shouldShimmer={false}
220+
backgroundColor={palette('--article-background')}
221+
/>
222+
);
223+
}
224+
225+
const trails: DCRFrontCard[] = convertFETrailToDcrTrail(
226+
data.trails,
227+
discussionApiUrl,
228+
);
229+
230+
const [firstTrail, ...standardTrails] = trails;
231+
if (!firstTrail) return null;
232+
233+
const heading = data.heading || data.displayname;
234+
235+
return (
236+
<div
237+
data-component="onwards-content-gallery-style"
238+
css={containerStyles}
239+
>
240+
<LeftColumn>
241+
<h2 css={headerStyles}>
242+
<span>{heading}</span>
243+
</h2>
244+
</LeftColumn>
245+
<h2 css={mobileHeaderStyles}>
246+
<span>{heading}</span>
247+
</h2>
248+
<div>
249+
<div css={[cardsContainerStyles, splashCardStyles]}>
250+
<Card
251+
{...getDefaultCardProps(firstTrail, false)}
252+
mediaPositionOnDesktop="right"
253+
mediaPositionOnMobile="top"
254+
mediaSize="medium"
255+
headlineSizes={{
256+
desktop: 'small',
257+
tablet: 'small',
258+
mobile: 'small',
259+
}}
260+
/>
261+
</div>
262+
<StraightLines
263+
cssOverrides={[
264+
cardsContainerStyles,
265+
css`
266+
${until.tablet} {
267+
display: none;
268+
}
269+
`,
270+
]}
271+
count={1}
272+
color={palette('--onward-content-border')}
273+
/>
274+
<ul css={[cardsContainerStyles, standardCardListStyles]}>
275+
{standardTrails.slice(0, 4).map((trail) => (
276+
<li key={trail.url} css={standardCardStyles}>
277+
<Card
278+
{...getDefaultCardProps(
279+
trail,
280+
!!isInOnwardsAbTestVariant,
281+
)}
282+
mediaPositionOnDesktop="bottom"
283+
mediaPositionOnMobile="left"
284+
mediaSize="small"
285+
/>
286+
</li>
287+
))}
288+
</ul>
289+
</div>
290+
</div>
291+
);
292+
};

dotcom-rendering/src/components/OnwardsUpper.importable.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import {
77
Pillar,
88
} from '../lib/articleFormat';
99
import type { EditionId } from '../lib/edition';
10+
import { useAB } from '../lib/useAB';
1011
import { useIsHorizontalScrollingSupported } from '../lib/useIsHorizontalScrollingSupported';
1112
import { palette } from '../palette';
1213
import type { OnwardsSource } from '../types/onwards';
1314
import type { RenderingTarget } from '../types/renderingTarget';
1415
import type { TagType } from '../types/tag';
1516
import { FetchOnwardsData } from './FetchOnwardsData.importable';
17+
import { MoreGalleriesStyleOnwardsContent } from './MoreGalleriesStyleOnwardsContent.importable';
1618
import { Section } from './Section';
1719

1820
type PillarForContainer =
@@ -224,6 +226,11 @@ export const OnwardsUpper = ({
224226
renderingTarget,
225227
webURL,
226228
}: Props) => {
229+
const abTestAPI = useAB()?.api;
230+
const isInOnwardsAbTestVariant =
231+
renderingTarget === 'Web' &&
232+
!abTestAPI?.isUserInVariant('OnwardJourneys', 'variant');
233+
227234
const isHorizontalScrollingSupported = useIsHorizontalScrollingSupported();
228235
if (!isHorizontalScrollingSupported) return null;
229236

@@ -320,7 +327,19 @@ export const OnwardsUpper = ({
320327

321328
return (
322329
<div css={onwardsWrapper}>
323-
{!!url && (
330+
{!!url && isInOnwardsAbTestVariant && (
331+
<Section
332+
fullWidth={true}
333+
borderColour={palette('--article-section-border')}
334+
>
335+
<MoreGalleriesStyleOnwardsContent
336+
url={url}
337+
discussionApiUrl={discussionApiUrl}
338+
isInOnwardsAbTestVariant={isInOnwardsAbTestVariant}
339+
/>
340+
</Section>
341+
)}
342+
{!!url && !isInOnwardsAbTestVariant && (
324343
<Section
325344
fullWidth={true}
326345
borderColour={palette('--article-section-border')}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import type { ABTest } from '@guardian/ab-core';
22
import { abTestTest } from './tests/ab-test-test';
33
import { noAuxiaSignInGate } from './tests/no-auxia-sign-in-gate';
4+
import { onwardJourneys } from './tests/onward-journeys';
45
import { userBenefitsApi } from './tests/user-benefits-api';
56

67
// keep in sync with ab-tests in frontend
78
// https://github.com/guardian/frontend/tree/main/static/src/javascripts/projects/common/modules/experiments/ab-tests.ts
8-
export const tests: ABTest[] = [abTestTest, userBenefitsApi, noAuxiaSignInGate];
9+
export const tests: ABTest[] = [
10+
abTestTest,
11+
userBenefitsApi,
12+
noAuxiaSignInGate,
13+
onwardJourneys,
14+
];

0 commit comments

Comments
 (0)