@playpilot/tpi 8.14.0-beta.4 → 8.14.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.
Files changed (31) hide show
  1. package/dist/editorial.mount.js +9 -9
  2. package/dist/link-injections.js +1 -1
  3. package/dist/mount.js +9 -9
  4. package/events.md +5 -0
  5. package/package.json +2 -2
  6. package/src/lib/api/titles.ts +0 -10
  7. package/src/lib/data/translations.ts +9 -4
  8. package/src/lib/enums/TrackingEvent.ts +3 -0
  9. package/src/lib/fakeData.ts +3 -3
  10. package/src/lib/inTextWidgets.ts +43 -0
  11. package/src/lib/injection.ts +7 -13
  12. package/src/lib/routes.ts +0 -8
  13. package/src/lib/types/config.d.ts +0 -5
  14. package/src/routes/+layout.svelte +2 -0
  15. package/src/routes/components/Debugger.svelte +0 -5
  16. package/src/routes/components/Description.svelte +0 -1
  17. package/src/routes/components/Explore/ExploreLayout.svelte +19 -2
  18. package/src/routes/components/Explore/ExploreRouter.svelte +2 -55
  19. package/src/routes/components/Explore/Routes/ExploreResults.svelte +2 -12
  20. package/src/routes/components/Modals/RailModal.svelte +3 -4
  21. package/src/routes/components/Playlinks/Playlinks.svelte +0 -1
  22. package/src/routes/components/Rails/Rail.svelte +4 -4
  23. package/src/routes/components/Title.svelte +4 -12
  24. package/src/routes/components/Widgets/InjectionsWidgetRail.svelte +51 -0
  25. package/src/tests/lib/api/titles.test.js +1 -23
  26. package/src/tests/lib/inTextWidgets.test.js +160 -0
  27. package/src/tests/lib/injection.test.js +3 -44
  28. package/src/tests/lib/routes.test.js +2 -14
  29. package/src/tests/routes/components/Widgets/InjectionsWidgetRail.test.js +28 -0
  30. package/src/routes/components/Explore/Routes/ExploreTitle.svelte +0 -94
  31. package/src/tests/routes/components/Explore/Routes/ExploreTitle.test.js +0 -87
package/events.md CHANGED
@@ -100,6 +100,11 @@ 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
+
103
108
  ### Various
104
109
  Event | Action | Info | Payload
105
110
  --- | --- | --- | ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.14.0-beta.4",
3
+ "version": "8.14.0",
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.1",
39
+ "svelte-tiny-slider": "^2.7.2",
40
40
  "typescript": "^5.9.3",
41
41
  "typescript-eslint": "^8.59.2",
42
42
  "vite": "^5.4.21",
@@ -27,13 +27,3 @@ 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, no_region_filter: true })
33
-
34
- const title = data.results[0]
35
-
36
- if (!title) throw new Error('No title was returned for sid: ' + sid)
37
-
38
- return title
39
- }
@@ -171,6 +171,11 @@ export const translations = {
171
171
  [Language.Swedish]: 'Upptäck och sök bland alla filmer och tv-serier',
172
172
  [Language.Danish]: 'Opdag og søg i alle film og tv-serier',
173
173
  },
174
+ 'Streaming Guide Disclaimer': {
175
+ [Language.English]: 'In collaboration with <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
176
+ [Language.Swedish]: 'I samarbete med <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
177
+ [Language.Danish]: 'I samarbejde med <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
178
+ },
174
179
  'Streaming Guide Description': {
175
180
  [Language.English]: 'Find where to watch movies online - the ultimate guide that helps you find the best movies and shows across streaming services.',
176
181
  [Language.Swedish]: 'Sök bland alla filmer och serier för att ta reda på var du kan streama dem',
@@ -281,10 +286,10 @@ export const translations = {
281
286
  [Language.Swedish]: 'Utforska',
282
287
  [Language.Danish]: 'Udforsk',
283
288
  },
284
- 'Page Not Found': {
285
- [Language.English]: 'Page not found',
286
- [Language.Swedish]: 'Sidan hittades inte',
287
- [Language.Danish]: 'Siden blev ikke fundet',
289
+ 'Mentioned In This Article': {
290
+ [Language.English]: 'Mentioned in this article',
291
+ [Language.Swedish]: 'Nämnda i den här artikeln',
292
+ [Language.Danish]: 'Nævnt i denne artikel',
288
293
  },
289
294
 
290
295
  // List titles
@@ -55,6 +55,9 @@ 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
+
58
61
  // Various
59
62
  ShareTitle: 'ali_share_title',
60
63
  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,
52
+ title_details: { ...title, sid: '1' },
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,
60
+ title_details: { ...title, sid: '2' },
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,
68
+ title_details: { ...title, sid: '3' },
69
69
  manual: false,
70
70
  }, {
71
71
  sid: '4',
@@ -0,0 +1,43 @@
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
+ }
@@ -5,7 +5,7 @@ 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 { exploreTitleUrl, titleUrl } from './routes'
8
+ import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
9
9
 
10
10
  export const keyDataAttribute = 'data-playpilot-injection-key'
11
11
  export const keySelector = `[${keyDataAttribute}]`
@@ -169,6 +169,8 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
169
169
  // The function itself will decide whether or not it should actually insert the component based on the config.
170
170
  if (document.querySelector(keySelector)) insertInTextDisclaimer(elements)
171
171
 
172
+ insertInTextWidgets(foundInjections)
173
+
172
174
  return mergedInjections.filter(i => i.title_details).map((injection, index) => {
173
175
  const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
174
176
  const duplicate = injection.duplicate ?? hasManualEquivalent
@@ -200,15 +202,11 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
200
202
  const injectionElement = document.createElement('span')
201
203
  injectionElement.dataset.playpilotInjectionKey = injection.key
202
204
 
203
- const openInExplore = !!window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore
204
-
205
- const href = openInExplore ? exploreTitleUrl(injection.title_details!) : titleUrl(injection.title_details!)
206
-
207
205
  const linkElement = document.createElement('a')
208
206
  linkElement.dataset.playpilotPosterUrl = injection.title_details?.standing_poster
209
207
  linkElement.innerText = injection.title
210
- linkElement.href = href
211
- linkElement.target = openInExplore ? '' : '_blank'
208
+ linkElement.href = injection.playpilot_url
209
+ linkElement.target = '_blank'
212
210
  linkElement.rel = 'noopener nofollow noreferrer'
213
211
 
214
212
  injectionElement.insertAdjacentElement('beforeend', linkElement)
@@ -313,12 +311,7 @@ function addCSSVariablesToLinks(): void {
313
311
 
314
312
  function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
315
313
  window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
316
-
317
- window.addEventListener('click', (event) => {
318
- if (window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore) return
319
-
320
- openModalForInjectedLink(event, injections)
321
- })
314
+ window.addEventListener('click', (event) => openModalForInjectedLink(event, injections))
322
315
 
323
316
  const createdInjectionElements = document.querySelectorAll<HTMLElement>(keySelector)
324
317
 
@@ -343,6 +336,7 @@ export function clearLinkInjections(): void {
343
336
 
344
337
  clearAfterArticlePlaylinks()
345
338
  clearInTextDisclaimer()
339
+ clearInTextWidgets()
346
340
  destroyAllModals(false)
347
341
  destroyLinkPopover(false)
348
342
  }
package/src/lib/routes.ts CHANGED
@@ -4,11 +4,3 @@ import type { TitleData } from './types/title'
4
4
  export function titleUrl(title: TitleData): string {
5
5
  return `${playPilotBaseUrl}/${title.type}/${title.slug}/`
6
6
  }
7
-
8
- export function exploreTitleUrl(title: TitleData): string {
9
- if (localStorage.getItem('tpi-open-explore-as-modal') === 'true') {
10
- return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=modal&sid=${title.sid}`
11
- }
12
-
13
- return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=title&sid=${title.sid}`
14
- }
@@ -97,11 +97,6 @@ export type ConfigResponse = {
97
97
  in_text_disclaimer_selector?: string
98
98
  in_text_disclaimer_insert_position?: InsertPosition
99
99
 
100
- /**
101
- * Open TPI links in explore rather than in modals in the article
102
- */
103
- open_tpi_links_in_explore?: boolean
104
-
105
100
  /**
106
101
  * These options are all relevant for the Explore component, which can be inserted as a widget on any page or as a modal.
107
102
  * `explore_navigation_selector` is used to select the navigation element that should be copied and inserted _after_.
@@ -70,6 +70,8 @@
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
+
73
75
  <h2>A matching link is already present</h2>
74
76
  <p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
75
77
 
@@ -226,11 +226,6 @@
226
226
 
227
227
  <hr />
228
228
 
229
- <button onclick={() => { localStorage.setItem('tpi-open-explore-as-modal', 'true'); onrerender() }}>Open links as modal in explore</button>
230
- <button onclick={() => { localStorage.setItem('tpi-open-explore-as-modal', 'false'); onrerender() }}>Open links as separate page in explore</button>
231
-
232
- <hr />
233
-
234
229
  <button onclick={() => shown = false}>Close</button>
235
230
  </div>
236
231
  {/if}
@@ -35,7 +35,6 @@
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%);
39
38
  }
40
39
 
41
40
  .paragraph {
@@ -52,6 +52,11 @@
52
52
  {t('Streaming Guide Subheading')}
53
53
  </div>
54
54
 
55
+ <p class="disclaimer">
56
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
57
+ {@html t('Streaming Guide Disclaimer')}
58
+ </p>
59
+
55
60
  {#if !useExploreRouter()}
56
61
  <p class="description">
57
62
  {t('Streaming Guide Description')}
@@ -117,7 +122,7 @@
117
122
  .heading,
118
123
  .subheading {
119
124
  color: theme(text-color);
120
- font-size: theme(explore-heading-size, clamp(margin(1.5), 5vw, margin(2)));
125
+ font-size: theme(explore-heading-font-size, clamp(margin(1.5), 5vw, margin(2)));
121
126
  font-weight: theme(explore-heading-font-weight, font-bold);
122
127
  text-transform: theme(explore-heading-text-transform, normal);
123
128
  line-height: theme(explore-heading-line-height, 1.5);
@@ -126,13 +131,25 @@
126
131
  .subheading {
127
132
  margin-top: margin(0.5);
128
133
  max-width: margin(15);
129
- font-size: theme(explore-subheading-size, clamp(margin(1), 2.5vw, margin(1.25)));
134
+ font-size: theme(explore-subheading-font-size, clamp(margin(1), 2.5vw, margin(1.25)));
130
135
 
131
136
  @include desktop {
132
137
  max-width: 100%;
133
138
  }
134
139
  }
135
140
 
141
+ .disclaimer {
142
+ margin: theme(explore-disclaimer-margin, 0);
143
+ font-size: theme(explore-diclaimer-font-size, font-size-small);
144
+ color: theme(explore-disclaimer-color, text-color-alt);
145
+ opacity: 0.75;
146
+
147
+ :global(a) {
148
+ color: inherit;
149
+ font-style: inherit;
150
+ }
151
+ }
152
+
136
153
  .description {
137
154
  max-width: theme(explore-header-max-width, 600px);
138
155
  margin: 0;
@@ -7,10 +7,6 @@
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'
11
- import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
12
- import { openModal } from '$lib/modal'
13
- import type { TitleData } from '$lib/types/title'
14
10
 
15
11
  const routes: ExploreRoute[] = [
16
12
  {
@@ -24,74 +20,25 @@
24
20
  key: 'home',
25
21
  component: ExploreHome,
26
22
  })
27
-
28
- routes.push({
29
- key: 'title',
30
- component: ExploreTitle,
31
- })
32
23
  }
33
24
 
34
- const initialRouteKey = getCurrentRouteParam() || routes[0].key
35
-
36
- let currentRoute: ExploreRoute = $state(routes.find(({ key }) => key === initialRouteKey) || routes[0])
25
+ let currentRoute: ExploreRoute = $state(routes[0])
37
26
  let searchQuery: string = $state('')
38
27
  let filter: ExploreFilter = $state({})
39
28
 
40
29
  const CurrentRouteComponent = $derived(currentRoute.component)
41
30
 
42
- if (initialRouteKey === 'modal') openModalViaRoute()
43
-
44
31
  $effect(() => {
45
32
  if (searchQuery) currentRoute = routes.find(route => route.key === 'results')!
46
33
  })
47
34
 
48
- function navigate(key: string, pushState: boolean = true): void {
35
+ function navigate(key: string): void {
49
36
  currentRoute = routes.find(route => route.key === key) || routes[0]
50
37
 
51
- const currentUrl = new URL(document.location.toString())
52
-
53
- if (key !== 'title' && key !== 'modal') currentUrl.searchParams.delete('sid')
54
-
55
- if (key === routes[0].key) currentUrl.searchParams.delete('route')
56
- else currentUrl.searchParams.set('route', currentRoute.key)
57
-
58
- if (pushState) history.pushState({}, '', currentUrl)
59
-
60
38
  track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
61
39
  }
62
-
63
- function onhashchange(): void {
64
- navigate(getCurrentRouteParam(), false)
65
- }
66
-
67
- function getCurrentRouteParam(): string {
68
- return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
69
- }
70
-
71
- // This is temporary while testing, please clean me up later
72
- async function openModalViaRoute(): Promise<void> {
73
- const currentUrl = new URL(document.location.toString())
74
- const sid = currentUrl.searchParams.get('sid')
75
-
76
- if (!sid) return
77
-
78
- const [title, railTitles] = (await Promise.allSettled([
79
- fetchTitleBySid(sid),
80
- fetchSimilarTitles({ sid } as unknown as TitleData)])
81
- ).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
82
-
83
- openModal({
84
- type: 'titles-rail',
85
- data: [(title as TitleData), ...(railTitles as TitleData[])],
86
- props: {
87
- onclose: () => navigate('home'),
88
- },
89
- })
90
- }
91
40
  </script>
92
41
 
93
- <svelte:window on:popstate={onhashchange} />
94
-
95
42
  <ExploreLayout {navigate} bind:searchQuery bind:filter>
96
43
  <CurrentRouteComponent {searchQuery} {filter} {navigate} />
97
44
  </ExploreLayout>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { onDestroy } from 'svelte'
3
2
  import { fetchTitles } from '$lib/api/titles'
4
3
  import { MetaEvent, TrackingEvent } from '$lib/enums/TrackingEvent'
5
4
  import { openModal } from '$lib/modal'
@@ -19,7 +18,8 @@
19
18
 
20
19
  interface Props {
21
20
  searchQuery?: string,
22
- filter?: ExploreFilter
21
+ filter?: ExploreFilter,
22
+ navigate?: (key: string) => void
23
23
  }
24
24
 
25
25
  const { searchQuery = '', filter = {} }: Props = $props()
@@ -51,10 +51,6 @@
51
51
  if (filter) setFilter()
52
52
  })
53
53
 
54
- onDestroy(() => {
55
- emptyFilter()
56
- })
57
-
58
54
  async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
59
55
  latestRequestId += 1
60
56
  const requestId = latestRequestId
@@ -112,12 +108,6 @@
112
108
  promise = getTitlesForFilter()
113
109
  }
114
110
 
115
- function emptyFilter(): void {
116
- for (const key of Object.keys(filter)) {
117
- delete filter[key]
118
- }
119
- }
120
-
121
111
  function resetTitles(): void {
122
112
  page = 1
123
113
  titles = []
@@ -12,11 +12,10 @@
12
12
  items: Record<string, any>[]
13
13
  initialIndex?: number
14
14
  onchange?: (index: number) => void
15
- onclose?: () => void
16
15
  each: Snippet<[item: any, currentIndex: number]>
17
16
  }
18
17
 
19
- const { items, initialIndex = 0, onchange = () => null, onclose = () => null, each }: Props = $props()
18
+ const { items, initialIndex = 0, onchange = () => null, each }: Props = $props()
20
19
 
21
20
  const transitionDuration = 300
22
21
 
@@ -36,7 +35,7 @@
36
35
  }
37
36
  </script>
38
37
 
39
- <Modal blur {onclose}>
38
+ <Modal blur>
40
39
  {#snippet dialog()}
41
40
  <div class="rail-modal" style:--transition-duration="{transitionDuration}ms">
42
41
  <TinySlider threshold={40} moveThreshold={40} transitionDuration={initialized ? transitionDuration : 0} bind:this={slider}>
@@ -71,7 +70,7 @@
71
70
  </div>
72
71
 
73
72
  <div class="close" transition:scale|global>
74
- <RoundButton size="42px" onclick={() => { onclose(); destroyAllModals() }} aria-label="Close">
73
+ <RoundButton size="42px" onclick={() => destroyAllModals()} aria-label="Close">
75
74
  <IconClose size={24} />
76
75
  </RoundButton>
77
76
  </div>
@@ -123,7 +123,6 @@
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%);
127
126
 
128
127
  &.list {
129
128
  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% + margin(2));
62
- margin: 0 margin(-1);
61
+ width: calc(100% + var(--rail-margin, margin(1)) * 2);
62
+ margin: 0 calc(var(--rail-margin, margin(1)) * -1);
63
63
 
64
64
  :global(.slider) {
65
- padding: 0 margin(1);
65
+ padding: 0 var(--rail-margin, margin(1));
66
66
  }
67
67
 
68
68
  :global(.slider-content > :last-child) {
69
- margin-right: margin(2);
69
+ margin-right: calc(var(--rail-margin, margin(1)) * 2);
70
70
  }
71
71
  }
72
72
 
@@ -135,7 +135,7 @@
135
135
 
136
136
  .content {
137
137
  position: relative;
138
- padding: theme(detail-padding, margin(1));
138
+ 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: theme(title-header-offset, margin(3));
175
+ padding-top: margin(3);
176
176
  }
177
177
 
178
178
  .small & {
@@ -217,11 +217,6 @@
217
217
  }
218
218
  }
219
219
 
220
- .main {
221
- z-index: 1;
222
- position: relative;
223
- }
224
-
225
220
  .imdb {
226
221
  display: flex;
227
222
  align-items: center;
@@ -239,8 +234,6 @@
239
234
  }
240
235
 
241
236
  .actions {
242
- z-index: 1;
243
- position: relative;
244
237
  grid-area: actions;
245
238
  display: flex;
246
239
  gap: margin(0.5);
@@ -252,15 +245,14 @@
252
245
  top: 0;
253
246
  left: 0;
254
247
  width: 100%;
255
- height: theme(detail-background-height, margin(20));
248
+ height: margin(20);
256
249
  border-radius: theme(detail-background-border-radius, 0px);
257
250
  overflow: hidden;
258
251
  background: theme(detail-background, lighter);
259
252
  mask-image: linear-gradient(to bottom, black 40%, transparent);
260
- opacity: theme(detail-background-opacity, 1);
261
253
 
262
254
  @include desktop() {
263
- height: theme(detail-background-height, margin(12));
255
+ height: margin(12);
264
256
  mask-image: linear-gradient(to bottom, black 60%, transparent);
265
257
  }
266
258
 
@@ -0,0 +1,51 @@
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>
@@ -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, fetchTitleBySid, fetchTitles } from '$lib/api/titles'
4
+ import { fetchSimilarTitles, 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,26 +74,4 @@ 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}&no_region_filter=true&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
- })
99
77
  })