@reuters-graphics/graphics-components 3.4.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ export type LegendMode = 'threshold' | 'continuous' | 'diverging' | 'categorical' | 'proportional-symbols';
2
+ /** A formatter that turns a numeric value into a display string. */
3
+ export type LegendFormatter = (value: number) => string;
4
+ /**
5
+ * A threshold/diverging bin. Define at least one of `from`/`to`. Colors are
6
+ * passed through directly, so use any CSS color or design token.
7
+ */
8
+ export interface LegendItem {
9
+ /** CSS color for the bin or category swatch. */
10
+ color: string;
11
+ /** Lower bound of the bin (inclusive). Omit for an open-ended first bin. */
12
+ from?: number | null;
13
+ /** Upper bound of the bin (exclusive). Omit for an open-ended last bin. */
14
+ to?: number | null;
15
+ /** Optional explicit label. Generated from the bounds when omitted. */
16
+ label?: string;
17
+ }
18
+ /** A continuous gradient stop. */
19
+ export interface LegendStop {
20
+ /** Numeric position of the stop along the domain. */
21
+ value: number;
22
+ /** CSS color at this stop. */
23
+ color: string;
24
+ /** Optional explicit label. */
25
+ label?: string;
26
+ }
27
+ /** A continuous-mode axis tick. */
28
+ export interface LegendTick {
29
+ /** Numeric position of the tick along the domain. */
30
+ value: number;
31
+ /** Optional explicit label. Formatted from `value` when omitted. */
32
+ label?: string;
33
+ }
34
+ /** The highlighted center of a diverging legend. */
35
+ export interface LegendMidpoint {
36
+ /** Numeric value of the midpoint within the legend domain. */
37
+ value: number;
38
+ /** Optional explicit label. Formatted from `value` when omitted. */
39
+ label?: string;
40
+ }
41
+ /** A proportional-symbols entry. */
42
+ export interface LegendSymbolItem {
43
+ /** Non-negative numeric value mapped to circle area. */
44
+ value: number;
45
+ /** Optional explicit label. Formatted from `value` when omitted. */
46
+ label?: string;
47
+ }
48
+ /** Fallback swatch for missing/unknown values. */
49
+ export interface LegendNoData {
50
+ /** Label shown next to the fallback swatch. */
51
+ label: string;
52
+ /** Optional CSS color for the fallback swatch. */
53
+ color?: string;
54
+ }
55
+ import type { ContainerWidth } from '../@types/global';
56
+ interface Props {
57
+ /** Optional legend heading displayed above the scale. */
58
+ title?: string;
59
+ /** Optional subtitle displayed beneath the title. */
60
+ subtitle?: string;
61
+ /** Legend rendering mode. */
62
+ mode?: LegendMode;
63
+ /**
64
+ * Legend entries. Threshold/diverging modes use numeric bounds and colors;
65
+ * categorical mode uses label and color pairs; proportional-symbols mode
66
+ * uses numeric values and optional labels.
67
+ */
68
+ items?: LegendItem[] | LegendSymbolItem[];
69
+ /** Ordered continuous color stops with numeric values. */
70
+ stops?: LegendStop[];
71
+ /** Optional tick values and labels for continuous mode. */
72
+ ticks?: LegendTick[];
73
+ /** Diverging midpoint with a numeric value and optional label. */
74
+ midpoint?: LegendMidpoint | null;
75
+ /** Optional formatter applied to numeric values. */
76
+ formatter?: LegendFormatter | null;
77
+ /** Optional fallback swatch for missing values. */
78
+ noData?: LegendNoData | null;
79
+ /** Width of the legend within the text well. */
80
+ width?: ContainerWidth;
81
+ }
82
+ /**
83
+ * `Legend` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-legend--docs)
84
+ *
85
+ * Quantitative and categorical legend for maps and data displays.
86
+ *
87
+ * Renders one of five legend types:
88
+ * - `threshold`: discrete bins with color swatches
89
+ * - `continuous`: a continuous gradient with ticks
90
+ * - `diverging`: threshold bins with a highlighted midpoint
91
+ * - `categorical`: square swatches with labels in a horizontal row
92
+ * - `proportional-symbols`: nested circles with leader lines and labels
93
+ *
94
+ * The component is presentational and can be placed anywhere in page layout. The
95
+ * optional `noData` prop renders a separate fallback swatch for missing, unknown,
96
+ * or unavailable values across all legend modes.
97
+ */
98
+ declare const Legend: import("svelte").Component<Props, {}, "">;
99
+ type Legend = ReturnType<typeof Legend>;
100
+ export default Legend;
@@ -0,0 +1,143 @@
1
+ <!-- @component `Referral` renders a single referral card (a linked headline, kicker, time and thumbnail). It's used by `ReferralBlock` but can be dropped into any layout on its own. -->
2
+ <script lang="ts">
3
+ // Utils
4
+ import { getTime } from '../SiteHeader/NavBar/NavDropdown/StoryCard/time';
5
+
6
+ // Types
7
+ import type { ReferralItem, LinkTarget } from './types';
8
+
9
+ interface Props extends ReferralItem {
10
+ /**
11
+ * Link [target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target), e.g., `_blank` or `_parent`.
12
+ */
13
+ linkTarget?: LinkTarget;
14
+ /** Use the narrower thumbnail sizing, for tight layouts. */
15
+ compact?: boolean;
16
+ /** Render as a single full-width column instead of a half-width card. */
17
+ stacked?: boolean;
18
+ /** Add a class to the card's root element. */
19
+ class?: string;
20
+ }
21
+
22
+ let {
23
+ url,
24
+ kicker,
25
+ title,
26
+ imageUrl,
27
+ imageAlt = '',
28
+ time,
29
+ linkTarget = '_self',
30
+ compact = false,
31
+ stacked = false,
32
+ class: cls = '',
33
+ }: Props = $props();
34
+ </script>
35
+
36
+ <div class="referral {cls}" class:stacked>
37
+ <a
38
+ href={url}
39
+ target={linkTarget}
40
+ rel={linkTarget === '_blank' ? 'noreferrer' : null}
41
+ >
42
+ <div class="referral-pack flex justify-around my-0 mx-auto">
43
+ <div class="headline" class:xs={compact}>
44
+ <div
45
+ class="kicker m-0 body-caption leading-tighter"
46
+ data-chromatic="ignore"
47
+ >
48
+ {kicker}
49
+ </div>
50
+ <div
51
+ class="title m-0 body-caption leading-tighter"
52
+ data-chromatic="ignore"
53
+ >
54
+ {title}
55
+ </div>
56
+ {#if time}
57
+ <div
58
+ class="publish-time body-caption leading-tighter"
59
+ data-chromatic="ignore"
60
+ >
61
+ {getTime(new Date(time))}
62
+ </div>
63
+ {/if}
64
+ </div>
65
+ <div
66
+ class="image-container block m-0 overflow-hidden relative"
67
+ class:xs={compact}
68
+ >
69
+ <img
70
+ class="block object-cover m-0 w-full"
71
+ data-chromatic="ignore"
72
+ src={imageUrl}
73
+ alt={imageAlt}
74
+ />
75
+ </div>
76
+ </div>
77
+ </a>
78
+ </div>
79
+
80
+ <style>/* Generated from
81
+ https://utopia.fyi/space/calculator/?c=320,18,1.125,1280,21,1.25,7,3,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12
82
+ */
83
+ /* Generated from
84
+ https://utopia.fyi/space/calculator/?c=320,18,1.125,1280,21,1.25,7,3,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12
85
+ */
86
+ /* Scales by 1.125 */
87
+ a {
88
+ text-decoration: none;
89
+ }
90
+
91
+ .referral {
92
+ display: block;
93
+ width: calc(50% - 30px);
94
+ max-width: 450px;
95
+ margin-block-start: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
96
+ margin-block-end: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
97
+ }
98
+ .referral.stacked {
99
+ width: 100%;
100
+ }
101
+ .referral.stacked .headline {
102
+ width: calc(100% - 7rem);
103
+ }
104
+ .referral:hover .title {
105
+ text-decoration: underline;
106
+ }
107
+ .referral:hover img {
108
+ filter: brightness(85%);
109
+ }
110
+ .referral .headline {
111
+ display: inline-block;
112
+ width: calc(100% - 9rem);
113
+ padding-inline-end: clamp(0.56rem, 0.52rem + 0.21vw, 0.69rem);
114
+ }
115
+ .referral .headline .kicker {
116
+ font-size: var(--theme-font-size-xxs);
117
+ font-family: Knowledge, sans-serif;
118
+ }
119
+ .referral .headline .title {
120
+ margin-block-start: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
121
+ font-weight: 500;
122
+ font-size: var(--theme-font-size-sm);
123
+ color: var(--theme-colour-text-primary);
124
+ font-family: Knowledge, sans-serif;
125
+ }
126
+ .referral .headline .publish-time {
127
+ font-size: var(--theme-font-size-xxs);
128
+ font-family: Knowledge, sans-serif;
129
+ }
130
+ .referral .image-container {
131
+ border-radius: 0.25rem;
132
+ border: 1px solid var(--theme-colour-brand-rules);
133
+ width: 9rem;
134
+ }
135
+ .referral .image-container.xs {
136
+ width: 7rem;
137
+ }
138
+ .referral .image-container img {
139
+ width: 100%;
140
+ height: 100%;
141
+ object-fit: cover;
142
+ transition: filter 0.1s;
143
+ }</style>
@@ -0,0 +1,17 @@
1
+ import type { ReferralItem, LinkTarget } from './types';
2
+ interface Props extends ReferralItem {
3
+ /**
4
+ * Link [target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target), e.g., `_blank` or `_parent`.
5
+ */
6
+ linkTarget?: LinkTarget;
7
+ /** Use the narrower thumbnail sizing, for tight layouts. */
8
+ compact?: boolean;
9
+ /** Render as a single full-width column instead of a half-width card. */
10
+ stacked?: boolean;
11
+ /** Add a class to the card's root element. */
12
+ class?: string;
13
+ }
14
+ /** `Referral` renders a single referral card (a linked headline, kicker, time and thumbnail). It's used by `ReferralBlock` but can be dropped into any layout on its own. */
15
+ declare const Referral: import("svelte").Component<Props, {}, "">;
16
+ type Referral = ReturnType<typeof Referral>;
17
+ export default Referral;
@@ -2,16 +2,17 @@
2
2
  <script lang="ts">
3
3
  // Utils
4
4
  import { onMount } from 'svelte';
5
- import { getTime } from '../SiteHeader/NavBar/NavDropdown/StoryCard/time';
6
- import { articleIsNotCurrentPage } from './filterCurrentPage';
5
+ import { fetchReferrals } from './getReferrals';
7
6
 
8
7
  // Components
9
8
  import Block from '../Block/Block.svelte';
9
+ import Referral from './Referral.svelte';
10
10
 
11
11
  // Types
12
- import type { Article } from './types';
13
- import type { Referrals } from './types';
14
- type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
12
+ import type { ContainerWidth } from '../@types/global';
13
+ import type { ReferralItem, LinkTarget } from './types';
14
+
15
+ type ReferralBlockWidth = Exclude<ContainerWidth, 'narrower' | 'narrow'>;
15
16
 
16
17
  interface Props {
17
18
  /**
@@ -24,6 +25,12 @@
24
25
  * Collection alias, as defined in Arc Collections editor.
25
26
  */
26
27
  collection?: string;
28
+ /**
29
+ * Provide your own referrals instead of fetching recent stories from
30
+ * Reuters.com. When set, the `section`/`collection` fetch is skipped and
31
+ * these stories are rendered as-is.
32
+ */
33
+ stories?: ReferralItem[];
27
34
  /**
28
35
  * Number of referrals to show.
29
36
  */
@@ -31,7 +38,7 @@
31
38
  /**
32
39
  * Link [target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target), e.g., `_blank` or `_parent`.
33
40
  */
34
- linkTarget?: string;
41
+ linkTarget?: LinkTarget;
35
42
  /**
36
43
  * Add a heading to the referral block.
37
44
  */
@@ -39,7 +46,7 @@
39
46
  /**
40
47
  * Width of the component within the text well: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid'
41
48
  */
42
- width?: ContainerWidth;
49
+ width?: ReferralBlockWidth;
43
50
  /** Add an ID to target with SCSS. */
44
51
  id?: string;
45
52
  /** Add a class to target with SCSS. */
@@ -49,6 +56,7 @@
49
56
  let {
50
57
  section = '/world/',
51
58
  collection,
59
+ stories = [],
52
60
  number = 4,
53
61
  linkTarget = '_self',
54
62
  heading = '',
@@ -59,51 +67,27 @@
59
67
 
60
68
  let clientWidth = $state(0);
61
69
 
62
- const SECTION_API = 'recent-stories-by-sections-v1';
63
-
64
- /** @TODO - Check if collections alias API still exists*/
65
- const COLLECTION_API = 'articles-by-collection-alias-or-id-v1';
66
-
67
- let referrals: Article[] = $state([]);
68
-
69
- const getReferrals = async () => {
70
- if (typeof window === 'undefined') return;
71
- // fetch only reliably works on prod sites
72
- if (window?.location?.hostname !== 'www.reuters.com') return;
73
- const isCollection = Boolean(collection);
74
- const API = isCollection ? COLLECTION_API : SECTION_API;
75
- try {
76
- const response = await fetch(
77
- `https://www.reuters.com/pf/api/v3/content/fetch/${API}?` +
78
- new URLSearchParams({
79
- query: JSON.stringify({
80
- section_ids: isCollection ? undefined : section,
81
- collection_alias: isCollection ? collection : undefined,
82
- size: 20,
83
- website: 'reuters',
84
- }),
85
- })
86
- );
87
-
88
- const data = (await response.json()) as Referrals;
89
-
90
- const articles = data.result.articles
91
- .filter((a) => a?.headline_category || a?.kicker?.name)
92
- .filter((a) => a?.thumbnail?.url)
93
- .filter((a) => !a?.content?.third_party)
94
- .filter(articleIsNotCurrentPage)
95
- .slice(0, number);
96
-
97
- referrals = articles;
98
- } catch {
99
- console.warn('Unable to fetch referral links.');
100
- }
101
- };
102
-
103
- onMount(getReferrals);
70
+ let fetchedReferrals: ReferralItem[] = $state([]);
71
+
72
+ // Manually provided stories take precedence. Otherwise, only show fetched
73
+ // stories once the API returns the full requested number, which avoids
74
+ // rendering a partial block.
75
+ const referrals = $derived<ReferralItem[]>(
76
+ stories.length ? stories
77
+ : fetchedReferrals.length === number ? fetchedReferrals
78
+ : []
79
+ );
80
+
81
+ onMount(() => {
82
+ // Skip the network request entirely when stories are supplied by hand.
83
+ if (stories.length) return;
84
+ fetchReferrals({ section, collection, number }).then((items) => {
85
+ fetchedReferrals = items;
86
+ });
87
+ });
104
88
  </script>
105
89
 
106
- {#if referrals.length === number}
90
+ {#if referrals.length}
107
91
  <Block {width} {id} class="referrals-block {cls}">
108
92
  <div
109
93
  class="block-container"
@@ -124,65 +108,19 @@
124
108
  class:xs={clientWidth && clientWidth < 450}
125
109
  >
126
110
  {#each referrals as referral}
127
- <div class="referral">
128
- <a
129
- href="https://www.reuters.com{referral.canonical_url}"
130
- target={linkTarget}
131
- rel={linkTarget === '_blank' ? 'noreferrer' : null}
132
- >
133
- <div class="referral-pack flex justify-around my-0 mx-auto">
134
- <div
135
- class="headline"
136
- class:xs={clientWidth && clientWidth < 450}
137
- >
138
- <div
139
- class="kicker m-0 body-caption leading-tighter"
140
- data-chromatic="ignore"
141
- >
142
- {referral.headline_category || referral.kicker.name}
143
- </div>
144
- <div
145
- class="title m-0 body-caption leading-tighter"
146
- data-chromatic="ignore"
147
- >
148
- {referral.title}
149
- </div>
150
- <div
151
- class="publish-time body-caption leading-tighter"
152
- data-chromatic="ignore"
153
- >
154
- {getTime(new Date(referral.display_time))}
155
- </div>
156
- </div>
157
- <div
158
- class="image-container block m-0 overflow-hidden relative"
159
- class:xs={clientWidth && clientWidth < 450}
160
- >
161
- <img
162
- class="block object-cover m-0 w-full"
163
- data-chromatic="ignore"
164
- src={referral.thumbnail.url}
165
- alt={referral.thumbnail.alt_text ||
166
- referral.thumbnail.caption}
167
- />
168
- </div>
169
- </div>
170
- </a>
171
- </div>
111
+ <Referral
112
+ {...referral}
113
+ {linkTarget}
114
+ compact={clientWidth > 0 && clientWidth < 450}
115
+ stacked={clientWidth > 0 && clientWidth < 750}
116
+ />
172
117
  {/each}
173
118
  </div>
174
119
  </div>
175
120
  </Block>
176
121
  {/if}
177
122
 
178
- <style>/* Generated from
179
- https://utopia.fyi/space/calculator/?c=320,18,1.125,1280,21,1.25,7,3,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12
180
- */
181
- /* Generated from
182
- https://utopia.fyi/space/calculator/?c=320,18,1.125,1280,21,1.25,7,3,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12
183
- */
184
- /* Scales by 1.125 */
185
- div.block-container.stacked {
123
+ <style>div.block-container.stacked {
186
124
  display: flex;
187
125
  flex-direction: column;
188
126
  align-items: center;
@@ -196,61 +134,6 @@ div.heading.stacked {
196
134
  max-width: 450px;
197
135
  }
198
136
 
199
- .referral-container a {
200
- text-decoration: none;
201
- }
202
137
  .referral-container.stacked {
203
138
  max-width: 450px;
204
- }
205
- .referral-container.stacked .referral {
206
- width: 100%;
207
- }
208
- .referral-container.stacked .referral .headline {
209
- width: calc(100% - 7rem);
210
- }
211
- .referral-container .referral {
212
- display: block;
213
- width: calc(50% - 30px);
214
- max-width: 450px;
215
- margin-block-start: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
216
- margin-block-end: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);
217
- }
218
- .referral-container .referral:hover .title {
219
- text-decoration: underline;
220
- }
221
- .referral-container .referral:hover img {
222
- filter: brightness(85%);
223
- }
224
- .referral-container .referral .headline {
225
- display: inline-block;
226
- width: calc(100% - 9rem);
227
- padding-inline-end: clamp(0.56rem, 0.52rem + 0.21vw, 0.69rem);
228
- }
229
- .referral-container .referral .headline .kicker {
230
- font-size: var(--theme-font-size-xxs);
231
- font-family: Knowledge, sans-serif;
232
- }
233
- .referral-container .referral .headline .title {
234
- font-weight: 500;
235
- font-size: var(--theme-font-size-sm);
236
- color: var(--theme-colour-text-primary);
237
- font-family: Knowledge, sans-serif;
238
- }
239
- .referral-container .referral .headline .publish-time {
240
- font-size: var(--theme-font-size-xxs);
241
- font-family: Knowledge, sans-serif;
242
- }
243
- .referral-container .referral .image-container {
244
- border-radius: 0.25rem;
245
- border: 1px solid var(--theme-colour-brand-rules);
246
- width: 9rem;
247
- }
248
- .referral-container .referral .image-container.xs {
249
- width: 7rem;
250
- }
251
- .referral-container .referral .image-container img {
252
- width: 100%;
253
- height: 100%;
254
- object-fit: cover;
255
- transition: filter 0.1s;
256
139
  }</style>
@@ -1,4 +1,6 @@
1
- type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
1
+ import type { ContainerWidth } from '../@types/global';
2
+ import type { ReferralItem, LinkTarget } from './types';
3
+ type ReferralBlockWidth = Exclude<ContainerWidth, 'narrower' | 'narrow'>;
2
4
  interface Props {
3
5
  /**
4
6
  * Section ID, which is often the URL path to the section page on reuters.com.
@@ -10,6 +12,12 @@ interface Props {
10
12
  * Collection alias, as defined in Arc Collections editor.
11
13
  */
12
14
  collection?: string;
15
+ /**
16
+ * Provide your own referrals instead of fetching recent stories from
17
+ * Reuters.com. When set, the `section`/`collection` fetch is skipped and
18
+ * these stories are rendered as-is.
19
+ */
20
+ stories?: ReferralItem[];
13
21
  /**
14
22
  * Number of referrals to show.
15
23
  */
@@ -17,7 +25,7 @@ interface Props {
17
25
  /**
18
26
  * Link [target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target), e.g., `_blank` or `_parent`.
19
27
  */
20
- linkTarget?: string;
28
+ linkTarget?: LinkTarget;
21
29
  /**
22
30
  * Add a heading to the referral block.
23
31
  */
@@ -25,7 +33,7 @@ interface Props {
25
33
  /**
26
34
  * Width of the component within the text well: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid'
27
35
  */
28
- width?: ContainerWidth;
36
+ width?: ReferralBlockWidth;
29
37
  /** Add an ID to target with SCSS. */
30
38
  id?: string;
31
39
  /** Add a class to target with SCSS. */
@@ -0,0 +1,26 @@
1
+ import type { ReferralItem } from './types';
2
+ /**
3
+ * Environment gate for the live fetch. The Reuters.com referral API is only
4
+ * same-origin — and therefore CORS-accessible — on production, so by default we
5
+ * only fetch when running on www.reuters.com. Stories and tests can override
6
+ * `enabled` to feed mocked data through the real fetch/transform path without
7
+ * loosening this guard for real, non-production environments.
8
+ */
9
+ export declare const referralsApi: {
10
+ enabled: () => boolean;
11
+ };
12
+ interface FetchReferralsOptions {
13
+ /** Section ID/path passed to the recent-stories-by-section API. */
14
+ section?: string;
15
+ /** Collection alias passed to the articles-by-collection API. */
16
+ collection?: string;
17
+ /** Maximum number of referrals to return. */
18
+ number?: number;
19
+ }
20
+ /**
21
+ * Fetch recent Reuters.com stories for a section or collection and map them
22
+ * into referral items. Returns an empty array when fetching is disabled (see
23
+ * `referralsApi`) or the request fails.
24
+ */
25
+ export declare const fetchReferrals: ({ section, collection, number, }: FetchReferralsOptions) => Promise<ReferralItem[]>;
26
+ export {};
@@ -0,0 +1,58 @@
1
+ import { articleIsNotCurrentPage } from './filterCurrentPage';
2
+ const SECTION_API = 'recent-stories-by-sections-v1';
3
+ /** @TODO - Check if collections alias API still exists*/
4
+ const COLLECTION_API = 'articles-by-collection-alias-or-id-v1';
5
+ /** Map a fetched Reuters.com article into the referral shape. */
6
+ const articleToItem = (a) => ({
7
+ url: `https://www.reuters.com${a.canonical_url}`,
8
+ kicker: a.headline_category || a.kicker.name,
9
+ title: a.title,
10
+ imageUrl: a.thumbnail.url,
11
+ imageAlt: a.thumbnail.alt_text || a.thumbnail.caption || '',
12
+ time: new Date(a.display_time),
13
+ });
14
+ /**
15
+ * Environment gate for the live fetch. The Reuters.com referral API is only
16
+ * same-origin — and therefore CORS-accessible — on production, so by default we
17
+ * only fetch when running on www.reuters.com. Stories and tests can override
18
+ * `enabled` to feed mocked data through the real fetch/transform path without
19
+ * loosening this guard for real, non-production environments.
20
+ */
21
+ export const referralsApi = {
22
+ enabled: () => typeof window !== 'undefined' &&
23
+ window.location?.hostname === 'www.reuters.com',
24
+ };
25
+ /**
26
+ * Fetch recent Reuters.com stories for a section or collection and map them
27
+ * into referral items. Returns an empty array when fetching is disabled (see
28
+ * `referralsApi`) or the request fails.
29
+ */
30
+ export const fetchReferrals = async ({ section = '/world/', collection, number = 4, }) => {
31
+ if (!referralsApi.enabled())
32
+ return [];
33
+ const isCollection = Boolean(collection);
34
+ const API = isCollection ? COLLECTION_API : SECTION_API;
35
+ try {
36
+ const response = await fetch(`https://www.reuters.com/pf/api/v3/content/fetch/${API}?` +
37
+ new URLSearchParams({
38
+ query: JSON.stringify({
39
+ section_ids: isCollection ? undefined : section,
40
+ collection_alias: isCollection ? collection : undefined,
41
+ size: 20,
42
+ website: 'reuters',
43
+ }),
44
+ }));
45
+ const data = (await response.json());
46
+ return data.result.articles
47
+ .filter((a) => a?.headline_category || a?.kicker?.name)
48
+ .filter((a) => a?.thumbnail?.url)
49
+ .filter((a) => !a?.content?.third_party)
50
+ .filter(articleIsNotCurrentPage)
51
+ .slice(0, number)
52
+ .map(articleToItem);
53
+ }
54
+ catch {
55
+ console.warn('Unable to fetch referral links.');
56
+ return [];
57
+ }
58
+ };