Skip to content

Commit 383908a

Browse files
committed
add valibot parser to validate more gallery response
1 parent ce90bda commit 383908a

File tree

14 files changed

+391
-45
lines changed

14 files changed

+391
-45
lines changed

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,6 @@ export const Carousel = ({
880880
return null;
881881
}
882882

883-
console.log('*************************** rendering related contents');
884-
885883
return (
886884
<CarouselColours props={props}>
887885
<div
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { css } from '@emotion/react';
2+
import { isNonNullable } from '@guardian/libs';
3+
import { useEffect, useState } from 'react';
4+
import { array, object, type Output, safeParse, string } from 'valibot';
5+
import { decideFormat } from '../lib/articleFormat';
6+
import { getDataLinkNameCard } from '../lib/getDataLinkName';
7+
import { addDiscussionIds } from '../lib/useCommentCount';
8+
import { palette } from '../palette';
9+
import { type DCRFrontImage } from '../types/front';
10+
import { type MainMedia } from '../types/mainMedia';
11+
import type { OnwardsSource } from '../types/onwards';
12+
import type { FETrailType, TrailType } from '../types/trails';
13+
import { FETrailTypeSchema } from '../types/valibotSchemas/trails';
14+
import { MoreGalleries } from './MoreGalleries';
15+
import { Placeholder } from './Placeholder';
16+
17+
type Props = {
18+
url: string;
19+
limit: number; // Limit the number of items shown (the api often returns more)
20+
onwardsSource: OnwardsSource;
21+
discussionApiUrl: string;
22+
absoluteServerTimes: boolean;
23+
isAdFreeUser: boolean;
24+
};
25+
26+
type MoreGalleriesResponse = Output<typeof MoreGalleriesResponseSchema>;
27+
28+
const MoreGalleriesResponseSchema = object({
29+
trails: array(FETrailTypeSchema),
30+
heading: string(),
31+
});
32+
33+
const minHeight = css`
34+
min-height: 300px;
35+
`;
36+
37+
const getMedia = (galleryCount: number | undefined): MainMedia | undefined => {
38+
if (typeof galleryCount === 'number') {
39+
return { type: 'Gallery', count: galleryCount.toString() };
40+
}
41+
return undefined;
42+
};
43+
44+
const buildTrails = (
45+
trails: FETrailType[],
46+
trailLimit: number,
47+
isAdFreeUser: boolean,
48+
): TrailType[] => {
49+
return trails
50+
.filter(
51+
(trailType) =>
52+
!(
53+
trailType.branding?.brandingType?.name === 'paid-content' &&
54+
isAdFreeUser
55+
),
56+
)
57+
.slice(0, trailLimit)
58+
.map((trail, index) => {
59+
const format = decideFormat(trail.format);
60+
const image: DCRFrontImage | undefined = trail.masterImage
61+
? {
62+
src: trail.masterImage,
63+
altText: '',
64+
}
65+
: undefined;
66+
67+
return {
68+
...trail,
69+
image,
70+
format,
71+
dataLinkName: getDataLinkNameCard(format, '0', index),
72+
mainMedia: getMedia(trail.galleryCount),
73+
};
74+
});
75+
};
76+
77+
const delay = (delayInms: number) => {
78+
return new Promise((resolve) => setTimeout(resolve, delayInms));
79+
};
80+
81+
const fetchJson = async (ajaxUrl: string): Promise<MoreGalleriesResponse> => {
82+
await delay(2000);
83+
const fetchResponse = await fetch(ajaxUrl);
84+
if (!fetchResponse.ok) {
85+
throw new Error(`HTTP error! status: ${fetchResponse.status}`);
86+
}
87+
const responseJson: unknown = await fetchResponse.json();
88+
const result = safeParse(MoreGalleriesResponseSchema, responseJson, {
89+
abortEarly: true, // Avoid parsing the rest of the object after facing the first error
90+
});
91+
if (result.success) {
92+
return result.output;
93+
} else {
94+
const errorMessages = result.issues
95+
.map(
96+
(issue) =>
97+
`${issue.path?.map((p) => p.key).join('.') ?? 'root'}: ${
98+
issue.message
99+
}`,
100+
)
101+
.join('; ');
102+
throw new Error(
103+
`Failed to parse MoreGalleriesResponse: ${errorMessages}`,
104+
);
105+
}
106+
};
107+
108+
export const FetchMoreGalleriesData = ({
109+
url,
110+
limit,
111+
onwardsSource,
112+
discussionApiUrl,
113+
absoluteServerTimes,
114+
isAdFreeUser,
115+
}: Props) => {
116+
const [data, setData] = useState<MoreGalleriesResponse | undefined>(
117+
undefined,
118+
);
119+
const [error, setError] = useState<Error | null>(null);
120+
121+
useEffect(() => {
122+
fetchJson(url)
123+
.then((fetchedData) => {
124+
setData(fetchedData);
125+
setError(null);
126+
})
127+
.catch((err) => {
128+
setError(
129+
err instanceof Error ? err : new Error('Unknown error'),
130+
);
131+
setData(undefined);
132+
});
133+
}, [url]);
134+
135+
if (error) {
136+
// Send the error to Sentry and then prevent the element from rendering
137+
window.guardian.modules.sentry.reportError(error, 'more-galleries');
138+
return null;
139+
}
140+
141+
if (!data?.trails) {
142+
return (
143+
<Placeholder
144+
height={720} // best guess at typical height // TODO: this is different value for different breakpoints!!!
145+
shouldShimmer={false}
146+
backgroundColor={palette('--onward-background')}
147+
/>
148+
);
149+
}
150+
151+
addDiscussionIds(
152+
data.trails
153+
.map((trail) => trail.discussion?.discussionId)
154+
.filter(isNonNullable),
155+
);
156+
157+
return (
158+
<div css={minHeight}>
159+
<MoreGalleries
160+
absoluteServerTimes={absoluteServerTimes}
161+
trails={buildTrails(data.trails, limit, isAdFreeUser)}
162+
discussionApiUrl={discussionApiUrl}
163+
heading="More galleries"
164+
onwardsSource={onwardsSource}
165+
/>
166+
</div>
167+
);
168+
};

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

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { css } from '@emotion/react';
22
import { isNonNullable } from '@guardian/libs';
33
import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat';
4-
import { decideTrail, decideTrailWithMasterImage } from '../lib/decideTrail';
4+
import { decideTrail } from '../lib/decideTrail';
55
import { useApi } from '../lib/useApi';
66
import { addDiscussionIds } from '../lib/useCommentCount';
77
import { palette } from '../palette';
88
import type { OnwardsSource } from '../types/onwards';
99
import type { RenderingTarget } from '../types/renderingTarget';
1010
import type { FETrailType, TrailType } from '../types/trails';
1111
import { Carousel } from './Carousel.importable';
12-
import { MoreGalleries } from './MoreGalleries';
1312
import { Placeholder } from './Placeholder';
1413

1514
type Props = {
@@ -41,12 +40,11 @@ const buildTrails = (
4140
trails: FETrailType[],
4241
trailLimit: number,
4342
isAdFreeUser: boolean,
44-
withMasterImage = false,
4543
): TrailType[] => {
4644
return trails
4745
.filter((trailType) => !(isTrailPaidContent(trailType) && isAdFreeUser))
4846
.slice(0, trailLimit)
49-
.map(withMasterImage ? decideTrailWithMasterImage : decideTrail);
47+
.map(decideTrail);
5048
};
5149

5250
export const FetchOnwardsData = ({
@@ -85,32 +83,22 @@ export const FetchOnwardsData = ({
8583

8684
return (
8785
<div css={minHeight}>
88-
{onwardsSource === 'more-galleries' ? (
89-
<MoreGalleries
90-
absoluteServerTimes={absoluteServerTimes}
91-
trails={buildTrails(data.trails, limit, isAdFreeUser, true)}
92-
discussionApiUrl={discussionApiUrl}
93-
heading="More galleries"
94-
onwardsSource={onwardsSource}
95-
/>
96-
) : (
97-
<Carousel
98-
heading={data.heading || data.displayname} // Sometimes the api returns heading as 'displayName'
99-
trails={buildTrails(data.trails, limit, isAdFreeUser)}
100-
description={data.description}
101-
onwardsSource={onwardsSource}
102-
format={format}
103-
leftColSize={
104-
format.design === ArticleDesign.LiveBlog ||
105-
format.design === ArticleDesign.DeadBlog
106-
? 'wide'
107-
: 'compact'
108-
}
109-
discussionApiUrl={discussionApiUrl}
110-
absoluteServerTimes={absoluteServerTimes}
111-
renderingTarget={renderingTarget}
112-
/>
113-
)}
86+
<Carousel
87+
heading={data.heading || data.displayname} // Sometimes the api returns heading as 'displayName'
88+
trails={buildTrails(data.trails, limit, isAdFreeUser)}
89+
description={data.description}
90+
onwardsSource={onwardsSource}
91+
format={format}
92+
leftColSize={
93+
format.design === ArticleDesign.LiveBlog ||
94+
format.design === ArticleDesign.DeadBlog
95+
? 'wide'
96+
: 'compact'
97+
}
98+
discussionApiUrl={discussionApiUrl}
99+
absoluteServerTimes={absoluteServerTimes}
100+
renderingTarget={renderingTarget}
101+
/>
114102
</div>
115103
);
116104
};

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,6 @@ export const OnwardsUpper = ({
303303
? getContainerDataUrl(pillar, editionId, ajaxUrl)
304304
: undefined;
305305

306-
console.log(`url: ${url}`);
307-
console.log(`curatedDataUrl: ${curatedDataUrl}`);
308-
309306
return (
310307
<div css={onwardsWrapper}>
311308
{!!url && (

dotcom-rendering/src/layouts/GalleryLayout.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { ArticleTitle } from '../components/ArticleTitle';
1818
import { Caption } from '../components/Caption';
1919
import { Carousel } from '../components/Carousel.importable';
2020
import { DiscussionLayout } from '../components/DiscussionLayout';
21-
import { FetchOnwardsData } from '../components/FetchOnwardsData.importable';
21+
import { FetchMoreGalleriesData } from '../components/FetchMoreGalleriesData.importable';
2222
import { Footer } from '../components/Footer';
2323
import { DesktopAdSlot, MobileAdSlot } from '../components/GalleryAdSlots';
2424
import { GalleryImage } from '../components/GalleryImage';
@@ -380,17 +380,16 @@ export const GalleryLayout = (props: WebProps | AppProps) => {
380380
frontendData.showBottomSocialButtons && isWeb
381381
}
382382
/>
383+
{/* TODO: I think to reduce the layout shift, we shouldn't defer until visible */}
383384
<Island priority="feature" defer={{ until: 'visible' }}>
384-
<FetchOnwardsData
385-
url={`${gallery.frontendData.config.ajaxUrl}/gallery/most-viewed.json?dcr=true`} // TODO: Fix the url for the app version too
385+
<FetchMoreGalleriesData
386+
url={`${gallery.frontendData.config.ajaxUrl}/gallery/most-viewed.json?dcr=true`}
386387
limit={5}
387388
onwardsSource={'more-galleries'}
388-
format={format}
389389
discussionApiUrl={discussionApiUrl}
390390
absoluteServerTimes={
391391
switches['absoluteServerTimes'] ?? false
392392
}
393-
renderingTarget={renderingTarget}
394393
isAdFreeUser={frontendData.isAdFreeUser}
395394
/>
396395
</Island>

dotcom-rendering/src/lib/image.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export const generateImageURL = ({
5959
aspectRatio?: string;
6060
cropOffset?: { x: number; y: number };
6161
}): string => {
62-
console.log(`mainImage: ${mainImage}`);
6362
const url = new URL(mainImage);
6463
const offset = cropOffset
6564
? `,offset-x${cropOffset.x},offset-y${cropOffset.y}`
@@ -75,12 +74,9 @@ export const generateImageURL = ({
7574

7675
const domain = isCodeGridUrl(url) ? 'i.guimcode.co.uk' : 'i.guim.co.uk';
7776

78-
const res = `https://${domain}/img/${getServiceFromUrl(url)}${
77+
return `https://${domain}/img/${getServiceFromUrl(url)}${
7978
url.pathname
8079
}?${params.toString()}`;
81-
82-
console.log(`res url: ${res}`);
83-
return res;
8480
};
8581

8682
export const isSupported = (imageUrl: string): boolean => {

dotcom-rendering/src/types/trails.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ interface BaseTrailType {
3434
galleryCount?: number;
3535
}
3636

37+
// export type BaseTrailType = Output<typeof BaseTrailTypeSchema> // TODO
38+
3739
export interface TrailType extends BaseTrailType {
3840
palette?: never;
3941
format: ArticleFormat;
@@ -65,6 +67,8 @@ export interface FETrailType extends BaseTrailType {
6567
image?: string;
6668
}
6769

70+
// export type FETrailType = Output<typeof FETrailTypeSchema>; // TODO
71+
6872
export interface TrailTabType {
6973
heading: string;
7074
trails: TrailType[];
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { literal, number, object, optional, string, union } from 'valibot';
2+
3+
export const BrandingLogoSchema = object({
4+
src: string(),
5+
link: string(),
6+
label: string(),
7+
dimensions: object({
8+
width: number(),
9+
height: number(),
10+
}),
11+
});
12+
13+
export const BrandingTypeSchema = union([
14+
object({ name: literal('paid-content') }),
15+
object({ name: literal('foundation') }),
16+
object({ name: literal('sponsored') }),
17+
]);
18+
19+
export const BrandingSchema = object({
20+
brandingType: optional(BrandingTypeSchema),
21+
sponsorName: string(),
22+
logo: BrandingLogoSchema,
23+
aboutThisLink: string(),
24+
logoForDarkBackground: optional(BrandingLogoSchema),
25+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { literal, union } from 'valibot';
2+
3+
// StarRating is 0 | 1 | 2 | 3 | 4 | 5
4+
export const StarRatingSchema = union([
5+
literal(0),
6+
literal(1),
7+
literal(2),
8+
literal(3),
9+
literal(4),
10+
literal(5),
11+
]);

0 commit comments

Comments
 (0)