@playpilot/tpi 8.13.0-beta.2 → 8.14.0-beta.1

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.
package/events.md CHANGED
@@ -100,11 +100,6 @@ Event | Action | Info | Payload
100
100
  `ali_display_ad_playlink_click` | _Fires any time the playlink linked to the display ad is clicked_ | | `campaign_name`
101
101
  `ali_ads_fetch_failed` | _Fires whenever ads tried to but failed to fetch_ | | `status` (Response status code)
102
102
 
103
- ## Widgets
104
- Event | Action | Info | Payload
105
- --- | --- | --- | ---
106
- `ali_widget_article_rail_title_click` | _Fires when a title in an tpi rail widget is clicked_ | | `Title`
107
-
108
103
  ### Various
109
104
  Event | Action | Info | Payload
110
105
  --- | --- | --- | ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.13.0-beta.2",
3
+ "version": "8.14.0-beta.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -36,7 +36,7 @@
36
36
  "svelte": "5.44.1",
37
37
  "svelte-check": "^4.0.0",
38
38
  "svelte-preprocess": "^6.0.3",
39
- "svelte-tiny-slider": "^2.7.2",
39
+ "svelte-tiny-slider": "^2.7.1",
40
40
  "typescript": "^5.9.3",
41
41
  "typescript-eslint": "^8.59.2",
42
42
  "vite": "^5.4.21",
@@ -27,3 +27,13 @@ export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]>
27
27
 
28
28
  return response.results
29
29
  }
30
+
31
+ export async function fetchTitleBySid(sid: string): Promise<TitleData> {
32
+ const data = await fetchTitles({ sids: sid })
33
+
34
+ const title = data.results[0]
35
+
36
+ if (!title) throw new Error('No title was returned')
37
+
38
+ return title
39
+ }
@@ -281,10 +281,10 @@ export const translations = {
281
281
  [Language.Swedish]: 'Utforska',
282
282
  [Language.Danish]: 'Udforsk',
283
283
  },
284
- 'Mentioned In This Article': {
285
- [Language.English]: 'Mentioned in this article',
286
- [Language.Swedish]: 'Nämnda i den här artikeln',
287
- [Language.Danish]: 'Nævnt i denne artikel',
284
+ 'Page Not Found': {
285
+ [Language.English]: 'Page not found',
286
+ [Language.Swedish]: 'Sidan hittades inte',
287
+ [Language.Danish]: 'Siden blev ikke fundet',
288
288
  },
289
289
 
290
290
  // List titles
@@ -55,9 +55,6 @@ export const TrackingEvent = {
55
55
  SplitTestView: 'ali_split_test_view',
56
56
  SplitTestAction: 'ali_split_test_action',
57
57
 
58
- // Widgets
59
- WidgetArticleRailTitleClick: 'ali_widget_article_rail_title_click',
60
-
61
58
  // Various
62
59
  ShareTitle: 'ali_share_title',
63
60
  SaveTitle: 'ali_save_title',
@@ -49,7 +49,7 @@ export const linkInjections: LinkInjection[] = [{
49
49
  sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
50
50
  playpilot_url: 'https://playpilot.com/movie/example/',
51
51
  key: 'some-key-1',
52
- title_details: { ...title, sid: '1' },
52
+ title_details: title,
53
53
  }, {
54
54
  sid: '2',
55
55
  title: 'The Long Kiss Goodnight',
@@ -57,7 +57,7 @@ export const linkInjections: LinkInjection[] = [{
57
57
  playpilot_url: 'https://playpilot.com/movie/example-2/',
58
58
  key: 'some-key-2',
59
59
  after_article: false,
60
- title_details: { ...title, sid: '2' },
60
+ title_details: title,
61
61
  }, {
62
62
  sid: '3',
63
63
  title: 'Nobody',
@@ -65,7 +65,7 @@ export const linkInjections: LinkInjection[] = [{
65
65
  playpilot_url: 'https://playpilot.com/movie/example-3/',
66
66
  key: 'some-key-3',
67
67
  after_article: true,
68
- title_details: { ...title, sid: '3' },
68
+ title_details: title,
69
69
  manual: false,
70
70
  }, {
71
71
  sid: '4',
@@ -5,7 +5,6 @@ import { destroyAllModals, openModalForInjectedLink } from './modal'
5
5
  import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverOnMouseleave, isPopoverActive, openPopoverForInjectedLink } from './popover'
6
6
  import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
7
7
  import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
8
- import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
9
8
 
10
9
  export const keyDataAttribute = 'data-playpilot-injection-key'
11
10
  export const keySelector = `[${keyDataAttribute}]`
@@ -169,8 +168,6 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
169
168
  // The function itself will decide whether or not it should actually insert the component based on the config.
170
169
  if (document.querySelector(keySelector)) insertInTextDisclaimer(elements)
171
170
 
172
- insertInTextWidgets(foundInjections)
173
-
174
171
  return mergedInjections.filter(i => i.title_details).map((injection, index) => {
175
172
  const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
176
173
  const duplicate = injection.duplicate ?? hasManualEquivalent
@@ -336,7 +333,6 @@ export function clearLinkInjections(): void {
336
333
 
337
334
  clearAfterArticlePlaylinks()
338
335
  clearInTextDisclaimer()
339
- clearInTextWidgets()
340
336
  destroyAllModals(false)
341
337
  destroyLinkPopover(false)
342
338
  }
@@ -70,8 +70,6 @@
70
70
  <p>De tre ’Jurassic World’-film kunne have givet indtryk af, at der ikke skal meget til, før dinosaurer igen ville kunne dominere kloden. Men det har vist sig ikke at holde stik.</p>
71
71
  <p>Het komt elk jaar voor dat films met torenhoge budgetten flink floppen aan de box-office, ongeacht de kwaliteit van de film. Dit is het geval bij een aantal films die dit jaar zijn uitgekomen. Denk aan Mickey 17, Black Bag en de onlangs uitgebrachte animatiefilm Elio. In 2015 was de superheldenfilm Fantastic Four een van de grootste flops.</p>
72
72
 
73
- <div data-playpilot-widget="tpi-rail"></div>
74
-
75
73
  <h2>A matching link is already present</h2>
76
74
  <p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
77
75
 
@@ -35,6 +35,7 @@
35
35
  <style lang="scss">
36
36
  .description:first-child {
37
37
  margin: margin(-1) 0 margin(1);
38
+ max-width: theme(description-max-width, 100%);
38
39
  }
39
40
 
40
41
  .paragraph {
@@ -7,6 +7,7 @@
7
7
  import ExploreHome from './Routes/ExploreHome.svelte'
8
8
  import ExploreResults from './Routes/ExploreResults.svelte'
9
9
  import ExploreLayout from './ExploreLayout.svelte'
10
+ import ExploreTitle from './Routes/ExploreTitle.svelte'
10
11
 
11
12
  const routes: ExploreRoute[] = [
12
13
  {
@@ -20,9 +21,16 @@
20
21
  key: 'home',
21
22
  component: ExploreHome,
22
23
  })
24
+
25
+ routes.push({
26
+ key: 'title',
27
+ component: ExploreTitle,
28
+ })
23
29
  }
24
30
 
25
- let currentRoute: ExploreRoute = $state(routes[0])
31
+ const initialRouteKey = getCurrentRouteParam() || routes[0].key
32
+
33
+ let currentRoute: ExploreRoute = $state(routes.find(({ key }) => key === initialRouteKey) || routes[0])
26
34
  let searchQuery: string = $state('')
27
35
  let filter: ExploreFilter = $state({})
28
36
 
@@ -35,10 +43,27 @@
35
43
  function navigate(key: string): void {
36
44
  currentRoute = routes.find(route => route.key === key) || routes[0]
37
45
 
46
+ const currentUrl = new URL(document.location.toString())
47
+
48
+ if (key === routes[0].key) currentUrl.searchParams.delete('route')
49
+ else currentUrl.searchParams.set('route', currentRoute.key)
50
+
51
+ history.pushState({}, '', currentUrl)
52
+
38
53
  track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
39
54
  }
55
+
56
+ function onhashchange(): void {
57
+ navigate(getCurrentRouteParam())
58
+ }
59
+
60
+ function getCurrentRouteParam(): string {
61
+ return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
62
+ }
40
63
  </script>
41
64
 
65
+ <svelte:window on:popstate={onhashchange} />
66
+
42
67
  <ExploreLayout {navigate} bind:searchQuery bind:filter>
43
68
  <CurrentRouteComponent {searchQuery} {filter} {navigate} />
44
69
  </ExploreLayout>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { onDestroy } from 'svelte'
2
3
  import { fetchTitles } from '$lib/api/titles'
3
4
  import { MetaEvent, TrackingEvent } from '$lib/enums/TrackingEvent'
4
5
  import { openModal } from '$lib/modal'
@@ -18,8 +19,7 @@
18
19
 
19
20
  interface Props {
20
21
  searchQuery?: string,
21
- filter?: ExploreFilter,
22
- navigate?: (key: string) => void
22
+ filter?: ExploreFilter
23
23
  }
24
24
 
25
25
  const { searchQuery = '', filter = {} }: Props = $props()
@@ -51,6 +51,10 @@
51
51
  if (filter) setFilter()
52
52
  })
53
53
 
54
+ onDestroy(() => {
55
+ emptyFilter()
56
+ })
57
+
54
58
  async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
55
59
  latestRequestId += 1
56
60
  const requestId = latestRequestId
@@ -108,6 +112,12 @@
108
112
  promise = getTitlesForFilter()
109
113
  }
110
114
 
115
+ function emptyFilter(): void {
116
+ for (const key of Object.keys(filter)) {
117
+ delete filter[key]
118
+ }
119
+ }
120
+
111
121
  function resetTitles(): void {
112
122
  page = 1
113
123
  titles = []
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import { fetchTitleBySid } from '$lib/api/titles'
3
+ import { mobileBreakpoint } from '$lib/constants'
4
+ import { t } from '$lib/localization'
5
+ import Button from '../../Button.svelte'
6
+ import IconArrow from '../../Icons/IconArrow.svelte'
7
+ import Title from '../../Title.svelte'
8
+
9
+ interface Props {
10
+ navigate?: (key: string) => void
11
+ }
12
+
13
+ const { navigate = () => null }: Props = $props()
14
+
15
+ let screenWidth = $state(window.innerWidth)
16
+
17
+ const isMobile = $derived(screenWidth < mobileBreakpoint)
18
+ const sid = new URL(document.location.toString()).searchParams.get('sid')
19
+ </script>
20
+
21
+ <svelte:window bind:innerWidth={screenWidth} />
22
+
23
+ {#snippet empty()}
24
+ {@render back()}
25
+
26
+ <p class="empty">{t('Page Not Found')}</p>
27
+ {/snippet}
28
+
29
+ {#snippet back(offset = false)}
30
+ <div class="back" class:offset>
31
+ <Button variant="link" onclick={() => { navigate('home') }}>
32
+ <IconArrow direction="left" />
33
+ {t('Home')}
34
+ </Button>
35
+ </div>
36
+ {/snippet}
37
+
38
+ {#if sid}
39
+ {#await fetchTitleBySid(sid)}
40
+ {@render back()}
41
+
42
+ Loading...
43
+ {:then title}
44
+ {@render back(true)}
45
+
46
+ <div class="title" data-testid="title">
47
+ <Title {title} useVideoBackground={isMobile} />
48
+ </div>
49
+ {:catch}
50
+ {@render empty()}
51
+ {/await}
52
+ {:else}
53
+ {@render empty()}
54
+ {/if}
55
+
56
+ <style lang="scss">
57
+ .title {
58
+ --playpilot-description-max-width: 600px;
59
+ --playpilot-playlinks-max-width: 600px;
60
+ position: relative;
61
+ border-radius: theme(border-radius);
62
+ overflow: hidden;
63
+ margin-top: margin(1);
64
+
65
+ @include desktop() {
66
+ --playpilot-detail-header-offset: #{margin(5)};
67
+ --playpilot-detail-background-height: #{margin(35)};
68
+ --playpilot-detail-background-opacity: 0.5;
69
+ }
70
+ }
71
+
72
+ .back {
73
+ --playpilot-button-text-color: white;
74
+ font-weight: theme(font-bold);
75
+
76
+ &.offset {
77
+ z-index: 1;
78
+ position: absolute;
79
+ margin-left: margin(1);
80
+ margin-top: margin(1);
81
+ }
82
+ }
83
+
84
+ .empty {
85
+ margin-top: margin(1);
86
+ padding: margin(1);
87
+ border: 1px solid theme(content);
88
+ max-width: margin(20);
89
+ border-radius: theme(border-radius);
90
+ font-size: theme(font-size-large);
91
+ font-weight: theme(font-bold);
92
+ color: theme(text-color);
93
+ }
94
+ </style>
@@ -123,6 +123,7 @@
123
123
  display: grid;
124
124
  grid-template-columns: repeat(auto-fill, minmax(margin(15), 1fr));
125
125
  gap: margin(0.5);
126
+ max-width: theme(playlinks-max-width, 100%);
126
127
 
127
128
  &.list {
128
129
  grid-template-columns: 1fr;
@@ -58,15 +58,15 @@
58
58
  .rail {
59
59
  --gap: var(--rail-gap, #{margin(0.5)});
60
60
  position: relative;
61
- width: calc(100% + var(--rail-margin, margin(1)) * 2);
62
- margin: 0 calc(var(--rail-margin, margin(1)) * -1);
61
+ width: calc(100% + margin(2));
62
+ margin: 0 margin(-1);
63
63
 
64
64
  :global(.slider) {
65
- padding: 0 var(--rail-margin, margin(1));
65
+ padding: 0 margin(1);
66
66
  }
67
67
 
68
68
  :global(.slider-content > :last-child) {
69
- margin-right: calc(var(--rail-margin, margin(1)) * 2);
69
+ margin-right: margin(2);
70
70
  }
71
71
  }
72
72
 
@@ -135,7 +135,7 @@
135
135
 
136
136
  .content {
137
137
  position: relative;
138
- padding: margin(1);
138
+ padding: theme(detail-padding, margin(1));
139
139
  color: theme(detail-text-color, text-color);
140
140
  font-family: theme(detail-font-family, font-family);
141
141
  font-weight: theme(detail-font-weight, normal);
@@ -172,7 +172,7 @@
172
172
 
173
173
  @include desktop {
174
174
  display: block;
175
- padding-top: margin(3);
175
+ padding-top: theme(title-header-offset, margin(3));
176
176
  }
177
177
 
178
178
  .small & {
@@ -217,6 +217,11 @@
217
217
  }
218
218
  }
219
219
 
220
+ .main {
221
+ z-index: 1;
222
+ position: relative;
223
+ }
224
+
220
225
  .imdb {
221
226
  display: flex;
222
227
  align-items: center;
@@ -234,6 +239,8 @@
234
239
  }
235
240
 
236
241
  .actions {
242
+ z-index: 1;
243
+ position: relative;
237
244
  grid-area: actions;
238
245
  display: flex;
239
246
  gap: margin(0.5);
@@ -245,14 +252,15 @@
245
252
  top: 0;
246
253
  left: 0;
247
254
  width: 100%;
248
- height: margin(20);
255
+ height: theme(detail-background-height, margin(20));
249
256
  border-radius: theme(detail-background-border-radius, 0px);
250
257
  overflow: hidden;
251
258
  background: theme(detail-background, lighter);
252
259
  mask-image: linear-gradient(to bottom, black 40%, transparent);
260
+ opacity: theme(detail-background-opacity, 1);
253
261
 
254
262
  @include desktop() {
255
- height: margin(12);
263
+ height: theme(detail-background-height, margin(12));
256
264
  mask-image: linear-gradient(to bottom, black 60%, transparent);
257
265
  }
258
266
 
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest'
2
2
 
3
3
  import { api } from '$lib/api/api'
4
- import { fetchSimilarTitles, fetchTitles } from '$lib/api/titles'
4
+ import { fetchSimilarTitles, fetchTitleBySid, fetchTitles } from '$lib/api/titles'
5
5
  import { title } from '$lib/fakeData'
6
6
  import { getApiToken } from '$lib/token'
7
7
  import { fakeFetch } from '../../helpers'
@@ -74,4 +74,26 @@ describe('$lib/api/titles', () => {
74
74
  await expect(async () => await fetchSimilarTitles(title)).rejects.toThrow()
75
75
  })
76
76
  })
77
+
78
+ describe('fetchTitleBySid', () => {
79
+ it('Should call api with given sid and return the first result', async () => {
80
+ vi.mocked(api).mockResolvedValueOnce({ results: [title] })
81
+
82
+ const response = await fetchTitleBySid(title.sid)
83
+
84
+ expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&sids=${title.sid}&language=en-US&include_count=false`)
85
+ expect(response).toEqual(title)
86
+ })
87
+
88
+ it('Should throw when no title is returned', async () => {
89
+ vi.mocked(api).mockResolvedValueOnce({ results: [] })
90
+
91
+ await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow('No title was returned')
92
+ })
93
+
94
+ it('Should throw when api returns error', async () => {
95
+ vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
96
+ await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow()
97
+ })
98
+ })
77
99
  })
@@ -0,0 +1,87 @@
1
+ import { render, waitFor, fireEvent } from '@testing-library/svelte'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import TitleDetail from '../../../../../routes/components/Explore/Routes/ExploreTitle.svelte'
5
+ import { fetchTitleBySid } from '$lib/api/titles'
6
+ import { title } from '$lib/fakeData'
7
+
8
+ vi.mock('$lib/api/titles', () => ({
9
+ fetchTitleBySid: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('/src/routes/components/Title.svelte', () => ({
13
+ default: vi.fn(),
14
+ }))
15
+
16
+ describe('ExploreTitle.svelte', () => {
17
+ beforeEach(() => {
18
+ vi.resetAllMocks()
19
+ vi.mocked(fetchTitleBySid).mockResolvedValue(title)
20
+
21
+ history.pushState({}, '', '/')
22
+ })
23
+
24
+ it('Should render empty state when no sid is in the URL', async () => {
25
+ const { getByText } = render(TitleDetail)
26
+
27
+ await waitFor(() => {
28
+ expect(getByText('Page not found')).toBeTruthy()
29
+ })
30
+ })
31
+
32
+ it('Should call fetchTitleBySid with sid from URL', async () => {
33
+ history.pushState({}, '', '?sid=some-sid')
34
+
35
+ render(TitleDetail)
36
+
37
+ expect(fetchTitleBySid).toHaveBeenCalledWith('some-sid')
38
+ })
39
+
40
+ it('Should render loading state while fetching', async () => {
41
+ history.pushState({}, '', '?sid=some-sid')
42
+ vi.mocked(fetchTitleBySid).mockReturnValue(new Promise(() => {}))
43
+
44
+ const { getByText } = render(TitleDetail)
45
+
46
+ expect(getByText('Loading...')).toBeTruthy()
47
+ })
48
+
49
+ it('Should render the title when fetchTitleBySid resolves', async () => {
50
+ history.pushState({}, '', '?sid=some-sid')
51
+
52
+ const { getByTestId } = render(TitleDetail)
53
+
54
+ await waitFor(() => {
55
+ expect(getByTestId('title')).toBeTruthy()
56
+ })
57
+ })
58
+
59
+ it('Should render empty state when fetchTitleBySid rejects', async () => {
60
+ history.pushState({}, '', '?sid=some-sid')
61
+ vi.mocked(fetchTitleBySid).mockRejectedValue(new Error('Not found'))
62
+
63
+ const { getByText } = render(TitleDetail)
64
+
65
+ await waitFor(() => {
66
+ expect(getByText('Page not found')).toBeTruthy()
67
+ })
68
+ })
69
+
70
+ it('Should call navigate with "home" when back button is clicked', async () => {
71
+ history.pushState({}, '', '?sid=some-sid')
72
+
73
+ const navigate = vi.fn()
74
+
75
+ const { getByText } = render(TitleDetail, { navigate })
76
+
77
+ await fireEvent.click(getByText('Home'))
78
+
79
+ expect(navigate).toHaveBeenCalledWith('home')
80
+ })
81
+
82
+ it('Should not call fetchTitleBySid when no sid is in the URL', async () => {
83
+ render(TitleDetail)
84
+
85
+ expect(fetchTitleBySid).not.toHaveBeenCalled()
86
+ })
87
+ })
@@ -1,43 +0,0 @@
1
- import { mount, unmount } from 'svelte'
2
- import type { LinkInjection } from './types/injection'
3
- import InjectionsWidgetRail from '../routes/components/Widgets/InjectionsWidgetRail.svelte'
4
-
5
- const widgets: Record<string, any> = {
6
- 'tpi-rail': InjectionsWidgetRail,
7
- }
8
-
9
- export const inTextWidgetSelector = '[data-playpilot-widget]'
10
-
11
- export let inTextWidgetInsertedComponents: any[] = []
12
-
13
- export function insertInTextWidgets(linkInjections: LinkInjection[]): void {
14
- clearInTextWidgets()
15
-
16
- if (!linkInjections.length) return
17
-
18
- const targets = document.querySelectorAll<HTMLElement>(inTextWidgetSelector)
19
-
20
- targets.forEach(target => {
21
- const widget = target.dataset.playpilotWidget || ''
22
-
23
- const component = widgets[widget]
24
-
25
- if (!component) return
26
-
27
- const insertedComponent = mount(component, {
28
- target,
29
- props: {
30
- linkInjections,
31
- },
32
- })
33
-
34
- inTextWidgetInsertedComponents.push(insertedComponent)
35
- })
36
- }
37
-
38
- export function clearInTextWidgets(): void {
39
- inTextWidgetInsertedComponents.forEach(component => unmount(component))
40
- document.querySelectorAll('[data-playpilot-widget]').forEach(element => element.innerHTML = '')
41
-
42
- inTextWidgetInsertedComponents = []
43
- }
@@ -1,51 +0,0 @@
1
- <script lang="ts">
2
- import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
- import { t } from '$lib/localization'
4
- import { track } from '$lib/tracking'
5
- import type { LinkInjection } from '$lib/types/injection'
6
- import type { TitleData } from '$lib/types/title'
7
- import TitlesRail from '../Rails/TitlesRail.svelte'
8
-
9
- interface Props {
10
- linkInjections: LinkInjection[]
11
- }
12
-
13
- const { linkInjections }: Props = $props()
14
-
15
- let element: HTMLElement | null = $state(null)
16
-
17
- // @ts-ignore
18
- const heading = $derived(element?.parentNode?.dataset.heading || t('Mentioned In This Article'))
19
-
20
- const titles: TitleData[] = $derived.by(() => {
21
- const uniqueTitles: TitleData[] = []
22
-
23
- linkInjections.forEach(injection => {
24
- const title = injection.title_details!
25
-
26
- if (!title) return
27
- if (uniqueTitles.some(t => t.sid === title.sid)) return
28
-
29
- uniqueTitles.push(title)
30
- })
31
-
32
- return uniqueTitles
33
- })
34
- </script>
35
-
36
- {#if titles.length}
37
- <div class="widget" bind:this={element} data-testid="widget">
38
- <TitlesRail {titles} {heading} onclick={(title) => track(TrackingEvent.WidgetArticleRailTitleClick, title)} />
39
- </div>
40
- {/if}
41
-
42
- <style lang="scss">
43
- .widget {
44
- --rail-margin: 0;
45
- --playpilot-rails-arrow-background: black;
46
- --playpilot-detail-background-light: black;
47
- --playpilot-rail-title-text-color: currentColor;
48
- --playpilot-rail-text-color: currentColor;
49
- margin: margin(1) 0;
50
- }
51
- </style>