@playpilot/tpi 5.12.0-beta.similar.1 → 5.12.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.
Files changed (44) hide show
  1. package/dist/link-injections.js +10 -11
  2. package/events.md +1 -0
  3. package/package.json +1 -2
  4. package/src/lib/enums/SplitTest.ts +1 -2
  5. package/src/lib/enums/TrackingEvent.ts +2 -1
  6. package/src/lib/fakeData.ts +0 -70
  7. package/src/lib/linkInjection.ts +55 -12
  8. package/src/lib/playlink.ts +1 -4
  9. package/src/lib/splitTest.ts +9 -2
  10. package/src/lib/types/title.d.ts +0 -2
  11. package/src/routes/+page.svelte +0 -1
  12. package/src/routes/components/Icons/IconClose.svelte +1 -1
  13. package/src/routes/components/Icons/IconScrollIndicator.svelte +3 -0
  14. package/src/routes/components/Modal.svelte +7 -41
  15. package/src/routes/components/Playlinks.svelte +3 -6
  16. package/src/routes/components/Popover.svelte +111 -12
  17. package/src/routes/components/Title.svelte +18 -33
  18. package/src/routes/components/TitleModal.svelte +3 -2
  19. package/src/routes/components/TitlePopover.svelte +2 -5
  20. package/src/tests/lib/linkInjection.test.js +35 -9
  21. package/src/tests/lib/playlink.test.js +10 -25
  22. package/src/tests/lib/splitTest.test.js +31 -3
  23. package/src/tests/routes/components/Modal.test.js +19 -51
  24. package/src/tests/routes/components/Title.test.js +0 -8
  25. package/src/tests/setup.js +10 -0
  26. package/src/lib/modal.ts +0 -81
  27. package/src/lib/types/participant.d.ts +0 -14
  28. package/src/routes/components/Icons/IconArrow.svelte +0 -17
  29. package/src/routes/components/ListTitle.svelte +0 -172
  30. package/src/routes/components/Participant.svelte +0 -88
  31. package/src/routes/components/ParticipantModal.svelte +0 -30
  32. package/src/routes/components/PlaylinkIcon.svelte +0 -41
  33. package/src/routes/components/Rails/ParticipantsRail.svelte +0 -56
  34. package/src/routes/components/Rails/Rail.svelte +0 -91
  35. package/src/routes/components/Rails/SimilarRail.svelte +0 -16
  36. package/src/routes/components/Rails/TitlesRail.svelte +0 -95
  37. package/src/routes/components/Tabs.svelte +0 -47
  38. package/src/routes/components/TitlePoster.svelte +0 -30
  39. package/src/tests/lib/modal.test.js +0 -148
  40. package/src/tests/routes/components/ListTitle.test.js +0 -82
  41. package/src/tests/routes/components/PlaylinkIcon.test.js +0 -27
  42. package/src/tests/routes/components/Rails/ParticipantsRail.test.js +0 -41
  43. package/src/tests/routes/components/Rails/TitleRail.test.js +0 -38
  44. package/src/tests/routes/components/TitlePoster.test.js +0 -20
package/events.md CHANGED
@@ -23,6 +23,7 @@ Event | Action | Info | Payload
23
23
  --- | --- | --- | ---
24
24
  `ali_article_page_view` | _Fires any time an article is visited_ | This event will fire right after all data is fetched and will fire regardless of if there are injections or not | It will fire even on pages where injections are disabled. | -
25
25
  `ali_links_injected` | _Fires as long as any injections are injected into the article_ | Includes an object with the number of injections for this article with `manual` and `ai` as two separate numbers. | `manual` (number of manual injection), `ai` (number of ai injections)
26
+ `ali_injection_visible` | _Fires as soon as a link is visible on the screen_ | Fires once for each link in an article. | |
26
27
 
27
28
  ### Modal
28
29
  Event | Action | Info | Payload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.12.0-beta.similar.1",
3
+ "version": "5.12.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -32,7 +32,6 @@
32
32
  "svelte": "^5.0.0",
33
33
  "svelte-check": "^4.0.0",
34
34
  "svelte-preprocess": "^6.0.3",
35
- "svelte-tiny-slider": "^2.2.0",
36
35
  "typescript": "^5.0.0",
37
36
  "typescript-eslint": "^8.32.1",
38
37
  "vite": "^5.0.3",
@@ -2,7 +2,6 @@ export const SplitTest = {
2
2
  TopScrollFormat: {
3
3
  key: 'top_scroll_format',
4
4
  numberOfVariants: 2,
5
- // Variant 0 is separate bubble
6
- // Variant 1 is inline bubble
5
+ variantNames: ['Separated', 'Inline'] as string[]
7
6
  },
8
7
  } as const
@@ -2,6 +2,7 @@
2
2
  export const TrackingEvent = Object.freeze({
3
3
  ArticlePageView: 'ali_article_page_view',
4
4
  ArticleInjected: 'ali_links_injected',
5
+ InjectionVisible: 'ali_injection_visible',
5
6
 
6
7
  TitleModalView: 'ali_title_modal_view',
7
8
  TitleModalClose: 'ali_title_modal_close',
@@ -33,5 +34,5 @@ export const TrackingEvent = Object.freeze({
33
34
  DisplayedAdPlaylickClick: 'ali_display_ad_playlink_click',
34
35
 
35
36
  SplitTestView: 'ali_split_test_view',
36
- SplitTestAction: 'ali_split_test_action'
37
+ SplitTestAction: 'ali_split_test_action',
37
38
  })
@@ -1,5 +1,4 @@
1
1
  import type { LinkInjection } from "./types/injection"
2
- import type { ParticipantData } from "./types/participant"
3
2
  import type { TitleData } from "./types/title"
4
3
 
5
4
  export const title: TitleData = {
@@ -70,72 +69,3 @@ export const linkInjections: LinkInjection[] = [{
70
69
  playpilot_url: 'https://playpilot.com/movie/example-4/',
71
70
  key: 'some-key-4',
72
71
  }]
73
-
74
- export const participants: ParticipantData[] = [
75
- {
76
- sid: 'pr5C5W',
77
- name: 'James Franco',
78
- birth_date: '1978-04-19',
79
- death_date: null,
80
- jobs: ['actor'],
81
- image: null,
82
- image_uuid: null,
83
- gender: 'Male',
84
- character: 'Will Rodman (archive footage) (uncredited)',
85
- },
86
- {
87
- sid: 'pr8bZm',
88
- name: 'Thomas Rosales Jr.',
89
- birth_date: '1948-02-03',
90
- death_date: null,
91
- jobs: ['actor'],
92
- image: null,
93
- image_uuid: null,
94
- gender: 'Male',
95
- character: 'Old Man',
96
- },
97
- {
98
- sid: 'pr45Dp',
99
- name: 'Barack Obama',
100
- birth_date: '1961-08-04',
101
- death_date: null,
102
- jobs: ['actor'],
103
- image: null,
104
- image_uuid: null,
105
- gender: 'Male',
106
- character: 'Self (archive footage) (uncredited)',
107
- },
108
- {
109
- sid: 'pr6DnN',
110
- name: 'Gary Oldman',
111
- birth_date: '1958-03-21',
112
- death_date: null,
113
- jobs: ['actor'],
114
- image: null,
115
- image_uuid: null,
116
- gender: 'Male',
117
- character: 'Dreyfus',
118
- },
119
- {
120
- sid: 'pr7GK8',
121
- name: 'Michael Papajohn',
122
- birth_date: '1964-11-07',
123
- death_date: null,
124
- jobs: ['actor'],
125
- image: null,
126
- image_uuid: null,
127
- gender: 'Male',
128
- character: 'Cannon-Gunner',
129
- },
130
- {
131
- sid: 'pr88KG',
132
- name: 'Judy Greer',
133
- birth_date: '1975-07-20',
134
- death_date: null,
135
- jobs: ['actor'],
136
- image: null,
137
- image_uuid: null,
138
- gender: 'Female',
139
- character: 'Cornelia',
140
- },
141
- ]
@@ -1,4 +1,5 @@
1
1
  import { mount, unmount } from 'svelte'
2
+ import TitleModal from '../routes/components/TitleModal.svelte'
2
3
  import TitlePopover from '../routes/components/TitlePopover.svelte'
3
4
  import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
4
5
  import { cleanPhrase, findNumberOfMatchesInString, findShortestMatchBetweenPhrases, findTextNodeContaining, getIndexOfPhraseInElement, getNumberOfLeadingAndTrailingSpaces, isNodeInLink, replaceBetween, replaceStartingFrom } from './text'
@@ -8,14 +9,19 @@ import { playFallbackViewTransition } from './viewTransition'
8
9
  import { prefersReducedMotion } from 'svelte/motion'
9
10
  import { getNumberOfOccurrencesInArray } from './array'
10
11
  import { mobileBreakpoint } from './constants'
11
- import { destroyAllModals, openModal } from './modal'
12
+ import { isEditorialModeEnabled } from './auth'
13
+ import { track } from './tracking'
14
+ import { TrackingEvent } from './enums/TrackingEvent'
12
15
 
13
16
  const keyDataAttribute = 'data-playpilot-injection-key'
14
17
  const keySelector = `[${keyDataAttribute}]`
15
18
 
19
+ const linksIntersectionObserver = typeof window !== 'undefined' ? new IntersectionObserver(trackLinkIntersection) : null
20
+
16
21
  let currentlyHoveredInjection: EventTarget | null = null
17
22
  let activePopoverInsertedComponent: object | null = null
18
23
  let afterArticlePlaylinkInsertedComponent: object | null = null
24
+ let activeModalInsertedComponent: object | null = null
19
25
 
20
26
  /**
21
27
  * Return a list of all valid text containing elements that may get injected into.
@@ -347,7 +353,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
347
353
 
348
354
  playFallbackViewTransition(() => {
349
355
  destroyLinkPopover(false)
350
- openModal({ event, injection, data: injection.title_details })
356
+ openLinkModal(event, injection)
351
357
  }, !prefersReducedMotion.current && window.innerWidth >= mobileBreakpoint && !window.matchMedia("(pointer: coarse)").matches)
352
358
  })
353
359
 
@@ -365,7 +371,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
365
371
 
366
372
  const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
367
373
 
368
- // Open and close popover on mouseenter/mouseleave
374
+ // Open and close popover on mouseenter/mouseleave and add link to intersection observer
369
375
  createdInjectionElements.forEach((injectionElement) => {
370
376
  const key = injectionElement.dataset.playpilotInjectionKey
371
377
  const injection = injections.find(injection => key === injection.key)
@@ -376,9 +382,50 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
376
382
  if (!activePopoverInsertedComponent) openLinkPopover(event, injection)
377
383
  })
378
384
  injectionElement.addEventListener('mouseleave', () => currentlyHoveredInjection = null)
385
+
386
+ // Add link to observer, this is used to track when a link enters the viewport
387
+ linksIntersectionObserver!.observe(injectionElement)
388
+ })
389
+ }
390
+
391
+ /**
392
+ * Track each link as it enters the viewport. The link is removed from the list as soon as it has been tracked once.
393
+ * Does not fire at all in editorial mode.
394
+ */
395
+ export function trackLinkIntersection(entries: IntersectionObserverEntry[]): void {
396
+ if (isEditorialModeEnabled()) return
397
+
398
+ entries.forEach(entry => {
399
+ if (!entry.isIntersecting) return
400
+
401
+ track(TrackingEvent.InjectionVisible)
402
+ linksIntersectionObserver!.unobserve(entry.target)
379
403
  })
380
404
  }
381
405
 
406
+ /**
407
+ * Open modal for the corresponding injection by mounting the component and saving it to a variable.
408
+ * Ignore clicks that used modifier keys or that were not left click.
409
+ */
410
+ function openLinkModal(event: MouseEvent, injection: LinkInjection): void {
411
+ if (isHoldingSpecialKey(event)) return
412
+ if (activeModalInsertedComponent) return
413
+
414
+ event.preventDefault()
415
+
416
+ activeModalInsertedComponent = mount(TitleModal, { target: getPlayPilotWrapperElement(), props: { title: injection.title_details!, onclose: destroyLinkModal } })
417
+ }
418
+
419
+ /**
420
+ * Unmount the modal, removing it from the dom
421
+ */
422
+ function destroyLinkModal(outro: boolean = true): void {
423
+ if (!activeModalInsertedComponent) return
424
+
425
+ unmount(activeModalInsertedComponent, { outro })
426
+ activeModalInsertedComponent = null
427
+ }
428
+
382
429
  /**
383
430
  * When a link is hovered, it is shown as a popover. The component is mounted when a mouse enters the link,
384
431
  * and removed when clicked or on mouseleave.
@@ -436,13 +483,7 @@ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections:
436
483
  target.dataset.playpilotAfterArticlePlaylinks = 'true'
437
484
  insertElement.insertAdjacentElement(insertPosition, target)
438
485
 
439
- afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, {
440
- target,
441
- props: {
442
- linkInjections: injections,
443
- onclickmodal: (event, injection) => openModal({ event, injection, data: injection.title_details })
444
- }
445
- })
486
+ afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal: (event, injection) => openLinkModal(event, injection) } })
446
487
  }
447
488
 
448
489
  function clearAfterArticlePlaylinks(): void {
@@ -461,8 +502,10 @@ export function clearLinkInjections(): void {
461
502
  elements.forEach((element) => clearLinkInjection(element.getAttribute(keyDataAttribute) || ''))
462
503
 
463
504
  clearAfterArticlePlaylinks()
464
- destroyAllModals(false)
505
+ destroyLinkModal(false)
465
506
  destroyLinkPopover(false)
507
+
508
+ linksIntersectionObserver?.disconnect()
466
509
  }
467
510
 
468
511
  /**
@@ -568,6 +611,6 @@ export function isEquivalentInjection(injection1: LinkInjection, injection2: Lin
568
611
  return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
569
612
  }
570
613
 
571
- export function getPlayPilotWrapperElement(): Element {
614
+ function getPlayPilotWrapperElement(): Element {
572
615
  return document.querySelector('[data-playpilot-link-injections]') || document.body
573
616
  }
@@ -2,14 +2,11 @@ import type { PlaylinkData } from "./types/playlink"
2
2
 
3
3
  /**
4
4
  * Merge playlinks of the same provider of BUY and RENT categories into a shared TVOD category.
5
- * Also remove playlinks without logos, as these are likely sub providers.
6
5
  */
7
6
  export function mergePlaylinks(playlinks: PlaylinkData[]): PlaylinkData[] {
8
- const filtered = playlinks.filter(playlink => !!playlink.logo_url)
9
-
10
7
  let merged: PlaylinkData[] = []
11
8
 
12
- for (const playlink of filtered) {
9
+ for (const playlink of playlinks) {
13
10
  let newPlaylink = playlink
14
11
  const existingPlaylink = merged.find(p => p.name === newPlaylink.name)
15
12
 
@@ -4,6 +4,7 @@ import { track } from "./tracking"
4
4
  type SplitTest = {
5
5
  key: string
6
6
  numberOfVariants: number
7
+ variantNames?: string[]
7
8
  }
8
9
 
9
10
  /**
@@ -28,18 +29,24 @@ export function getSplitTestVariantIndex(test: SplitTest): number {
28
29
  return Math.floor(identifier * test.numberOfVariants)
29
30
  }
30
31
 
32
+ export function getSplitTestVariantName(test: SplitTest): string {
33
+ const variantIndex = getSplitTestVariantIndex(test)
34
+
35
+ return test.variantNames?.[variantIndex] || variantIndex.toString()
36
+ }
37
+
31
38
  export function isInSplitTestVariant(test: SplitTest, variant = 1): boolean {
32
39
  return getSplitTestVariantIndex(test) === variant
33
40
  }
34
41
 
35
42
  export function trackSplitTestView(test: SplitTest): void {
36
- const variant = getSplitTestVariantIndex(test)
43
+ const variant = getSplitTestVariantName(test)
37
44
 
38
45
  track(TrackingEvent.SplitTestView, null, { key: test.key, variant })
39
46
  }
40
47
 
41
48
  export function trackSplitTestAction(test: SplitTest, action: string): void {
42
- const variant = getSplitTestVariantIndex(test) ? 1 : 0
49
+ const variant = getSplitTestVariantName(test)
43
50
 
44
51
  track(TrackingEvent.SplitTestAction, null, { key: test.key, variant, action })
45
52
  }
@@ -1,4 +1,3 @@
1
- import type { ParticipantData } from "./participant"
2
1
  import type { PlaylinkData } from "./playlink"
3
2
 
4
3
  export type TitleData = {
@@ -19,5 +18,4 @@ export type TitleData = {
19
18
  original_title: string
20
19
  length?: number
21
20
  blurb?: string
22
- participants?: ParticipantData[]
23
21
  }
@@ -14,7 +14,6 @@
14
14
  import TrackingPixels from './components/TrackingPixels.svelte'
15
15
  import Debugger from './components/Debugger.svelte'
16
16
  import { fetchAds } from '$lib/ads'
17
- import ParticipantModal from './components/ParticipantModal.svelte';
18
17
 
19
18
  let parentElement: HTMLElement | null = $state(null)
20
19
  let elements: HTMLElement[] = $state([])
@@ -1,3 +1,3 @@
1
1
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
2
- <path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2
+ <path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
3
3
  </svg>
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 -960 960 960" width="24px" height="24px" fill="currentColor">
2
+ <path d="m480-320 160-160-56-56-64 64v-168h-80v168l-64-64-56 56 160 160Zm0 240q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
3
+ </svg>
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { fade, fly, scale, type TransitionConfig } from 'svelte/transition'
3
3
  import IconClose from './Icons/IconClose.svelte'
4
- import IconArrow from './Icons/IconArrow.svelte'
5
4
  import RoundButton from './RoundButton.svelte'
6
5
  import DragHandle from './DragHandle.svelte'
7
6
  import { onMount, setContext, type Snippet } from 'svelte'
@@ -9,32 +8,23 @@
9
8
  import { isInSplitTestVariant } from '$lib/splitTest'
10
9
  import { SplitTest } from '$lib/enums/SplitTest'
11
10
  import { mobileBreakpoint } from '$lib/constants'
12
- import { destroyAllModals, getPreviousModal, goBackToPreviousModal } from '$lib/modal'
13
11
 
14
12
  interface Props {
15
13
  children: Snippet
16
14
  bubble?: Snippet | null
17
15
  prepend?: Snippet | null
18
16
  tall?: boolean
19
- closeButtonStyle?: 'shadow' | 'flat'
17
+ onclose?: () => void
20
18
  onscroll?: () => void
21
19
  }
22
20
 
23
- const {
24
- children,
25
- bubble,
26
- prepend,
27
- tall = false,
28
- closeButtonStyle = 'shadow',
29
- onscroll = () => null,
30
- }: Props = $props()
21
+ const { children, bubble, prepend, tall = false, onclose = () => null, onscroll = () => null }: Props = $props()
31
22
 
32
23
  const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
33
24
 
34
25
  let windowWidth = $state(0)
35
26
  let dialogElement: HTMLElement | null = $state(null)
36
27
  let dialogOffset: number = $state(0)
37
- let hasPreviousModal = $state(false)
38
28
 
39
29
  const isMobile = $derived(windowWidth < mobileBreakpoint)
40
30
 
@@ -47,8 +37,6 @@
47
37
  const baseOverflowStyle = document.body.style.overflowY
48
38
  document.body.style.overflowY = 'hidden'
49
39
 
50
- hasPreviousModal = !!getPreviousModal()
51
-
52
40
  return () => document.body.style.overflowY = baseOverflowStyle || ''
53
41
  })
54
42
 
@@ -60,7 +48,7 @@
60
48
  }
61
49
  </script>
62
50
 
63
- <svelte:window onkeydown={({ key }) => { if (key === 'Escape') destroyAllModals() }} bind:innerWidth={windowWidth} />
51
+ <svelte:window onkeydown={({ key }) => { if (key === 'Escape') onclose() }} bind:innerWidth={windowWidth} />
64
52
 
65
53
  <div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
66
54
  {#if prepend}
@@ -77,21 +65,13 @@
77
65
 
78
66
  {#if isMobile}
79
67
  <div class="drag-handle" transition:scaleOrFly|global>
80
- <DragHandle target={dialogElement!} onpassed={() => destroyAllModals()} />
68
+ <DragHandle target={dialogElement!} onpassed={() => onclose()} />
81
69
  </div>
82
70
  {/if}
83
71
 
84
72
  <div class="dialog" class:tall {onscroll} bind:this={dialogElement} role="dialog" aria-labelledby="title" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new>
85
- {#if hasPreviousModal}
86
- <div class="close back {closeButtonStyle}">
87
- <RoundButton onclick={() => goBackToPreviousModal()} aria-label="Back">
88
- <IconArrow direction="left" />
89
- </RoundButton>
90
- </div>
91
- {/if}
92
-
93
- <div class="close {closeButtonStyle}">
94
- <RoundButton onclick={() => destroyAllModals()} aria-label="Close">
73
+ <div class="close">
74
+ <RoundButton onclick={() => onclose()}>
95
75
  <IconClose />
96
76
  </RoundButton>
97
77
  </div>
@@ -101,7 +81,7 @@
101
81
 
102
82
  <!-- svelte-ignore a11y_click_events_have_key_events -->
103
83
  <!-- svelte-ignore a11y_no_static_element_interactions -->
104
- <div class="backdrop" onclick={() => destroyAllModals()}></div>
84
+ <div class="backdrop" onclick={() => onclose()}></div>
105
85
  </div>
106
86
 
107
87
  <style lang="scss">
@@ -213,20 +193,6 @@
213
193
  &:hover {
214
194
  filter: brightness(1.1);
215
195
  }
216
-
217
- &.flat {
218
- --playpilot-button-background: transparent;
219
- --playpilot-button-shadow: none;
220
- }
221
-
222
- &.back {
223
- right: auto;
224
- left: margin(1);
225
-
226
- &.flat {
227
- left: margin(0.5);
228
- }
229
- }
230
196
  }
231
197
 
232
198
  .prepend {
@@ -8,7 +8,6 @@
8
8
  import { heading } from '$lib/actions/heading'
9
9
  import { getContext } from 'svelte'
10
10
  import Playlink from './Playlink.svelte'
11
- import Display from './Ads/Display.svelte'
12
11
  import { campaignToPlaylink, getFirstAdOfType } from '$lib/ads'
13
12
 
14
13
  interface Props {
@@ -28,7 +27,9 @@
28
27
  // otherwise break the layout in ways that don't make sense to fix.
29
28
  const list = $derived(outerWidth < 500 || !!displayAd)
30
29
 
31
- const mergedPlaylink = $derived(mergePlaylinks(playlinks))
30
+ // Remove any playlinks without logos, these are likely sub providers.
31
+ const filteredPlaylinks = $derived(playlinks.filter(playlink => !!playlink.logo_url))
32
+ const mergedPlaylink = $derived(mergePlaylinks(filteredPlaylinks))
32
33
 
33
34
  function onclick(playlink: string): void {
34
35
  track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.TitlePopoverPlaylinkClick, title, { playlink })
@@ -58,10 +59,6 @@
58
59
  <div class="empty" data-testid="playlinks-empty">
59
60
  {t('Title Unavailable')}
60
61
  </div>
61
-
62
- {#if displayAd}
63
- <Display campaign={displayAd} compact={!isModal} />
64
- {/if}
65
62
  {/if}
66
63
  </div>
67
64