@playpilot/tpi 8.17.1 → 8.18.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 (43) hide show
  1. package/.env +1 -1
  2. package/dist/editorial.mount.js +9 -9
  3. package/dist/link-injections.js +1 -1
  4. package/dist/mount.js +9 -9
  5. package/package.json +1 -1
  6. package/src/lib/api/titles.ts +2 -2
  7. package/src/lib/enums/TrackingEvent.ts +4 -4
  8. package/src/lib/fakeData.ts +44 -32
  9. package/src/lib/injection.ts +10 -9
  10. package/src/lib/modal.ts +3 -1
  11. package/src/lib/popover.ts +13 -12
  12. package/src/lib/scss/_mixins.scss +27 -0
  13. package/src/lib/scss/global.scss +0 -27
  14. package/src/lib/tracking.ts +2 -0
  15. package/src/lib/types/injection.d.ts +5 -0
  16. package/src/routes/components/Editorial/Editor.svelte +8 -5
  17. package/src/routes/components/Editorial/EditorItem.svelte +23 -9
  18. package/src/routes/components/Editorial/ManualInjection.svelte +1 -0
  19. package/src/routes/components/Explore/ExploreLayout.svelte +3 -1
  20. package/src/routes/components/Explore/Filter/Dropdown.svelte +3 -1
  21. package/src/routes/components/{TitlePopover.svelte → InjectionPopover.svelte} +26 -9
  22. package/src/routes/components/ListTitle.svelte +27 -6
  23. package/src/routes/components/Modals/Modal.svelte +2 -0
  24. package/src/routes/components/Modals/RailModal.svelte +3 -1
  25. package/src/routes/components/Participant.svelte +18 -6
  26. package/src/routes/components/Playlinks/Playlinks.svelte +2 -1
  27. package/src/routes/components/Popover.svelte +2 -1
  28. package/src/routes/components/Title.svelte +1 -1
  29. package/src/routes/elements/+page.svelte +3 -3
  30. package/src/tests/helpers.js +1 -0
  31. package/src/tests/lib/api/titles.test.js +4 -4
  32. package/src/tests/lib/injection.test.js +44 -3
  33. package/src/tests/lib/popover.test.js +7 -7
  34. package/src/tests/lib/tracking.test.js +8 -0
  35. package/src/tests/routes/components/Editorial/EditorItem.test.js +10 -0
  36. package/src/tests/routes/components/Editorial/ManualInjection.test.js +4 -0
  37. package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +2 -0
  38. package/src/tests/routes/components/InjectionPopover.test.js +117 -0
  39. package/src/tests/routes/components/ListTitle.test.js +7 -0
  40. package/src/tests/routes/components/Participant.test.js +7 -0
  41. package/src/tests/routes/components/Playlinks/AfterArticlePlaylinks.test.js +4 -0
  42. package/src/tests/routes/components/Playlinks/Playlinks.test.js +1 -1
  43. package/src/tests/routes/components/TitlePopover.test.js +0 -78
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.17.1",
3
+ "version": "8.18.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -18,9 +18,9 @@ export async function fetchTitles(params: Record<string, any> = {}): Promise<API
18
18
  }
19
19
 
20
20
  if (params.region !== null) {
21
- const region = params.region || await getRegionBasedOnIp()
21
+ let region = params.region || await getRegionBasedOnIp()
22
22
 
23
- if (!await isUserRegionSupported(region)) return { next: '', previous: '', results: [] }
23
+ if (!await isUserRegionSupported(region)) region = 'gl'
24
24
 
25
25
  params.region = region
26
26
  }
@@ -16,10 +16,10 @@ export const TrackingEvent = {
16
16
  ParticipantModalClose: 'ali_participant_modal_close',
17
17
 
18
18
  // Popover
19
- TitlePopoverView: 'ali_title_popover_view',
20
- TitlePopoverClose: 'ali_title_popover_close',
21
- TitlePopoverSaveClick: 'ali_title_popover_save_click',
22
- TitlePopoverPlaylinkClick: 'ali_title_popover_playlink_click',
19
+ InjectionPopoverView: 'ali_injection_popover_view',
20
+ InjectionPopoverClose: 'ali_injection_popover_close',
21
+ InjectionPopoverSaveClick: 'ali_injection_popover_save_click',
22
+ InjectionPopoverPlaylinkClick: 'ali_injection_popover_playlink_click',
23
23
 
24
24
  // Rails
25
25
  SimilarTitleClick: 'ali_similar_title_click',
@@ -43,38 +43,6 @@ export const title: TitleData = {
43
43
  embeddable_url: null,
44
44
  }
45
45
 
46
- export const linkInjections: LinkInjection[] = [{
47
- sid: '1',
48
- title: 'Quan',
49
- sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
50
- playpilot_url: 'https://playpilot.com/movie/example/',
51
- key: 'some-key-1',
52
- title_details: { ...title, sid: '1' },
53
- }, {
54
- sid: '2',
55
- title: 'The Long Kiss Goodnight',
56
- sentence: 'The movie is in a similar vein to successful films such as The Long Kiss Goodnight and Nobody',
57
- playpilot_url: 'https://playpilot.com/movie/example-2/',
58
- key: 'some-key-2',
59
- after_article: false,
60
- title_details: { ...title, sid: '2' },
61
- }, {
62
- sid: '3',
63
- title: 'Nobody',
64
- sentence: 'The movie is in a similar vein to successful films such as The Long Kiss Goodnight and Nobody',
65
- playpilot_url: 'https://playpilot.com/movie/example-3/',
66
- key: 'some-key-3',
67
- after_article: true,
68
- title_details: { ...title, sid: '3' },
69
- manual: false,
70
- }, {
71
- sid: '4',
72
- title: 'The Wheel of Time',
73
- sentence: '(The more I think about the Bene Gesserit the more I see how much they influenced not just the Jedi in Star Wars but also the Aes Sedai in The Wheel of Time and even the sorceresses in The Witcher books, though Herbert\'s order is the most ominous and terrifying of them all)',
74
- playpilot_url: 'https://playpilot.com/movie/example-4/',
75
- key: 'some-key-4',
76
- }]
77
-
78
46
  export const participants: ParticipantData[] = [
79
47
  {
80
48
  sid: 'pr5C5W',
@@ -144,6 +112,50 @@ export const participants: ParticipantData[] = [
144
112
  },
145
113
  ]
146
114
 
115
+ export const linkInjections: LinkInjection[] = [{
116
+ sid: '1',
117
+ title: 'Quan',
118
+ sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
119
+ playpilot_url: 'https://playpilot.com/movie/example/',
120
+ key: 'some-key-1',
121
+ title_details: { ...title, sid: '1' },
122
+ type: 'title',
123
+ }, {
124
+ sid: '2',
125
+ title: 'The Long Kiss Goodnight',
126
+ sentence: 'The movie is in a similar vein to successful films such as The Long Kiss Goodnight and Nobody',
127
+ playpilot_url: 'https://playpilot.com/movie/example-2/',
128
+ key: 'some-key-2',
129
+ after_article: false,
130
+ title_details: { ...title, sid: '2' },
131
+ type: 'title',
132
+ }, {
133
+ sid: '3',
134
+ title: 'Nobody',
135
+ sentence: 'The movie is in a similar vein to successful films such as The Long Kiss Goodnight and Nobody',
136
+ playpilot_url: 'https://playpilot.com/movie/example-3/',
137
+ key: 'some-key-3',
138
+ after_article: true,
139
+ title_details: { ...title, sid: '3' },
140
+ manual: false,
141
+ type: 'title',
142
+ }, {
143
+ sid: '4',
144
+ title: 'The Wheel of Time',
145
+ sentence: '(The more I think about the Bene Gesserit the more I see how much they influenced not just the Jedi in Star Wars but also the Aes Sedai in The Wheel of Time and even the sorceresses in The Witcher books, though Herbert\'s order is the most ominous and terrifying of them all)',
146
+ playpilot_url: 'https://playpilot.com/movie/example-4/',
147
+ key: 'some-key-4',
148
+ type: 'title',
149
+ }, {
150
+ sid: '5',
151
+ title: 'Jack Black',
152
+ sentence: 'Jason Momoa (”Aquaman”), Jack Black (”Nacho Libre”) och Jennifer Coolidge (”The White Lotus”) medverkar i den Jared Hess-regisserade (”Napolen Dynamite”) filmen.',
153
+ playpilot_url: 'https://playpilot.com/name/pr875J-jack-black/',
154
+ key: 'some-key-5',
155
+ participant_details: participants[2],
156
+ type: 'participant',
157
+ }]
158
+
147
159
  export const campaign: Campaign = {
148
160
  campaign_format: 'card',
149
161
  campaign_type: 'image',
@@ -172,7 +172,7 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
172
172
 
173
173
  insertInTextWidgets(foundInjections)
174
174
 
175
- return mergedInjections.filter(i => i.title_details).map((injection, index) => {
175
+ return mergedInjections.filter(injection => hasValidTypeData(injection)).map((injection, index) => {
176
176
  const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
177
177
  const duplicate = injection.duplicate ?? hasManualEquivalent
178
178
 
@@ -390,11 +390,11 @@ export function separateLinkInjectionTypes(injections: LinkInjection[]): LinkInj
390
390
  }
391
391
 
392
392
  export function isValidInjection(injection: LinkInjection): boolean {
393
- return !injection.inactive && !injection.removed && !injection.duplicate && !!injection.title_details && isValidPlaylinkType(injection)
393
+ return !injection.inactive && !injection.removed && !injection.duplicate && hasValidTypeData(injection) && isValidPlaylinkType(injection)
394
394
  }
395
395
 
396
396
  /**
397
- * An injection can have be of various playlink types, when all are false equivalent, the link is not valid.
397
+ * An injection can be of various playlink types, when all are false equivalent, the link is not valid.
398
398
  * It should be treated similar to an inactive playlink in this case.
399
399
  */
400
400
  export function isValidPlaylinkType(injection: LinkInjection): boolean {
@@ -402,16 +402,10 @@ export function isValidPlaylinkType(injection: LinkInjection): boolean {
402
402
  return !!injection.after_article
403
403
  }
404
404
 
405
- /**
406
- * Filter links for in-text injections, removing after article, inactive, removed, duplicate, and items without title_details
407
- */
408
405
  export function filterInvalidInTextInjections(injections: LinkInjection[]): LinkInjection[] {
409
406
  return filterRemovedAndInactiveInjections(injections).filter(i => i.in_text !== false && isValidInjection(i))
410
407
  }
411
408
 
412
- /**
413
- * Filter links for after article injections, removing in-text only, inactive, removed, duplicate, and items without title_details
414
- */
415
409
  export function filterInvalidAfterArticleInjections(injections: LinkInjection[]): LinkInjection[] {
416
410
  return filterRemovedAndInactiveInjections(injections).filter(i => i.after_article === true && isValidInjection(i))
417
411
  }
@@ -456,6 +450,13 @@ export function isEquivalentInjection(injection1: LinkInjection, injection2: Lin
456
450
  return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
457
451
  }
458
452
 
453
+ export function hasValidTypeData(injection: LinkInjection): boolean {
454
+ if (injection.type === 'title' && !!injection.title_details) return true
455
+ if (injection.type === 'participant' && !!injection.participant_details) return true
456
+
457
+ return false
458
+ }
459
+
459
460
  export function getPlayPilotWrapperElement(): Element {
460
461
  return document.querySelector('[data-playpilot-link-injections]') || document.body
461
462
  }
package/src/lib/modal.ts CHANGED
@@ -131,7 +131,9 @@ export function openModalForInjectedLink(event: MouseEvent, injections: LinkInje
131
131
  event.preventDefault()
132
132
 
133
133
  playFallbackViewTransition(() => {
134
+ const data = injection.title_details || injection.participant_details
135
+
134
136
  destroyLinkPopover(false)
135
- openModal({ event, injection, data: injection.title_details })
137
+ openModal({ event, injection, data, type: injection.type })
136
138
  }, !prefersReducedMotion.current && window.innerWidth >= mobileBreakpoint && !window.matchMedia('(pointer: coarse)').matches)
137
139
  }
@@ -1,29 +1,30 @@
1
1
  import { mount, unmount } from 'svelte'
2
- import TitlePopover from '../routes/components/TitlePopover.svelte'
3
2
  import { getPlayPilotWrapperElement, keyDataAttribute, keySelector } from './injection'
4
3
  import type { LinkInjection } from './types/injection'
4
+ import InjectionPopover from '../routes/components/InjectionPopover.svelte'
5
5
 
6
6
  export let currentlyHoveredInjection: EventTarget | null = null
7
7
  export let activePopoverInsertedComponent: object | null = null
8
8
 
9
- /**
10
- * When a link is hovered it is shown as a popover. The component is mounted when a mouse enters the link,
11
- * and removed when clicked or on mouseleave.
12
- */
13
9
  export function openPopoverForInjectedLink(event: MouseEvent, injection: LinkInjection): void {
14
- // Skip touch devices
15
10
  if (window.matchMedia('(pointer: coarse)').matches) return
16
11
  if (activePopoverInsertedComponent) destroyLinkPopover()
17
12
 
18
- const target = event.currentTarget as Element
19
- currentlyHoveredInjection = target
13
+ const currentTarget = event.currentTarget as Element
14
+ currentlyHoveredInjection = currentTarget
20
15
 
21
16
  // Only show if the link is hovered for more than 100ms. This is to prevent the popover from showing
22
17
  // when a user just happens to mouseover as they are moving their mouse about.
23
18
  setTimeout(() => {
24
- if (currentlyHoveredInjection !== target) return // User is no longer hovering this link
19
+ if (currentlyHoveredInjection !== currentTarget) return
25
20
 
26
- activePopoverInsertedComponent = mount(TitlePopover, { target: getPlayPilotWrapperElement(), props: { event, title: injection.title_details! } })
21
+ const target = getPlayPilotWrapperElement()
22
+
23
+ if (injection.type === 'title') {
24
+ activePopoverInsertedComponent = mount(InjectionPopover, { target, props: { event, type: 'title', data: injection.title_details! } })
25
+ } else if (injection.type === 'participant') {
26
+ activePopoverInsertedComponent = mount(InjectionPopover, { target, props: { event, type: 'participant', data: injection.participant_details! } })
27
+ }
27
28
  }, 100)
28
29
  }
29
30
 
@@ -44,7 +45,7 @@ export async function destroyLinkPopover(outro: boolean = true): Promise<void> {
44
45
  // In that case we remove the element straight from the dom.
45
46
  // Doing this will prevent the outro animation from playing, but this being a fallback, that's ok.
46
47
  // TODO: Find the actual cause of this bug.
47
- document.querySelectorAll<HTMLElement>('[data-playpilot-title-popover]').forEach(element => element.remove())
48
+ document.querySelectorAll<HTMLElement>('[data-playpilot-injection-popover]').forEach(element => element.remove())
48
49
  }
49
50
 
50
51
  export function destroyLinkPopoverOnMouseleave(event: MouseEvent): void {
@@ -53,7 +54,7 @@ export function destroyLinkPopoverOnMouseleave(event: MouseEvent): void {
53
54
  const target = event.target as Element
54
55
 
55
56
  // Mousemove is inside of popover or link that popover
56
- if (target.hasAttribute('data-playpilot-title-popover') || target.closest('[data-playpilot-title-popover]') ||
57
+ if (target.hasAttribute('data-playpilot-injection-popover') || target.closest('[data-playpilot-injection-popover]') ||
57
58
  target.hasAttribute(keyDataAttribute) || target.closest(keySelector)) return
58
59
 
59
60
  destroyLinkPopover()
@@ -37,3 +37,30 @@
37
37
  @content;
38
38
  }
39
39
  }
40
+
41
+ @mixin styled-scrollbar() {
42
+ scrollbar-color: var(--playpilot-content-light) transparent;
43
+ scrollbar-width: thin;
44
+
45
+ &::-webkit-scrollbar {
46
+ width: margin(0.75);
47
+ }
48
+
49
+ &::-webkit-scrollbar-track {
50
+ background: var(--playpilot-light);
51
+ }
52
+
53
+ &::-webkit-scrollbar-thumb {
54
+ border: 2px solid var(--playpilot-light);
55
+ border-radius: margin(1);
56
+ background: transparent;
57
+
58
+ &:hover {
59
+ background: var(--playpilot-content-light);
60
+ }
61
+
62
+ &:active {
63
+ background: var(--playpilot-text-color-alt);
64
+ }
65
+ }
66
+ }
@@ -18,33 +18,6 @@
18
18
  scroll-margin: margin(5);
19
19
  }
20
20
 
21
- .playpilot-styled-scrollbar {
22
- scrollbar-color: var(--playpilot-content-light) var(--playpilot-lighter);
23
- scrollbar-width: thin;
24
-
25
- &::-webkit-scrollbar {
26
- width: margin(0.75);
27
- }
28
-
29
- &::-webkit-scrollbar-track {
30
- background: var(--playpilot-light);
31
- }
32
-
33
- &::-webkit-scrollbar-thumb {
34
- border: 2px solid var(--playpilot-light);
35
- border-radius: margin(1);
36
- background: var(--playpilot-lighter);
37
-
38
- &:hover {
39
- background: var(--playpilot-content-light);
40
- }
41
-
42
- &:active {
43
- background: var(--playpilot-text-color-alt);
44
- }
45
- }
46
- }
47
-
48
21
  .playpilot-modal-open {
49
22
  overflow-y: hidden !important;
50
23
  }
@@ -23,6 +23,8 @@ export async function track(event: string, title: TitleData | null = null, paylo
23
23
  return
24
24
  }
25
25
 
26
+ if ((window.PlayPilotLinkInjections.tracked_events?.length || 0) > 500) return
27
+
26
28
  const headers = new Headers({ 'Content-Type': 'application/json' })
27
29
 
28
30
  if (title) {
@@ -1,5 +1,8 @@
1
+ import type { ParticipantData } from './participant'
1
2
  import type { TitleData } from './title'
2
3
 
4
+ export type LinkInjectionDataType = 'title' | 'participant'
5
+
3
6
  export type LinkInjection = {
4
7
  sid: string
5
8
  title: string
@@ -7,6 +10,7 @@ export type LinkInjection = {
7
10
  playpilot_url: string
8
11
  key: string
9
12
  title_details?: TitleData
13
+ participant_details?: ParticipantData
10
14
  phrase_before?: string | null
11
15
  phrase_after?: string | null
12
16
  inactive?: boolean
@@ -19,6 +23,7 @@ export type LinkInjection = {
19
23
  removed?: boolean
20
24
  duplicate?: boolean
21
25
  matchingElement?: null | Element
26
+ type: LinkInjectionDataType
22
27
  }
23
28
 
24
29
  export type LinkInjectionRanges = Record<string, { elementIndex: number, from: number, to: number }>
@@ -59,8 +59,7 @@
59
59
  const linkInjectionsString = $derived(JSON.stringify(linkInjections))
60
60
  const showControls = $derived(!aiRunning && allowEditing)
61
61
  const hasChanged = $derived(initialStateString && initialStateString !== linkInjectionsString)
62
- // Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
63
- const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
62
+ const filteredInjections = $derived(linkInjections.filter((i) => (i.title_details || i.participant_details) && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
64
63
  const sortedInjections = $derived(sortInjections(filteredInjections))
65
64
  const initialAiRunning = $derived(!loading && untrack(() => aiStatus.aiRunning))
66
65
 
@@ -76,7 +75,7 @@
76
75
  function sortInjections(injections: LinkInjection[]): LinkInjection[] {
77
76
  return injections.sort((a, b) => {
78
77
  if (a.failed !== b.failed) return a.failed ? 1 : -1
79
- return a.title_details!.title.localeCompare(b.title_details!.title)
78
+ return (a.title_details?.title || a.participant_details?.name)!.localeCompare((b.title_details?.title || b.participant_details?.name)!)
80
79
  })
81
80
  }
82
81
 
@@ -147,7 +146,7 @@
147
146
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:400,600,700">
148
147
  </svelte:head>
149
148
 
150
- <section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
149
+ <section class="editor" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
151
150
  {#if editorElement && !loading}
152
151
  <div class="handles">
153
152
  <div class="handle">
@@ -227,7 +226,7 @@
227
226
 
228
227
  {#if manualInjectionActive}
229
228
  <div
230
- class="panel playpilot-styled-scrollbar"
229
+ class="panel"
231
230
  style:top="{scrollDistance}px"
232
231
  transition:fly={{ x: Math.min(window.innerWidth, 320), duration: 200, opacity: 1 }}>
233
232
  <ManualInjection
@@ -260,6 +259,8 @@
260
259
  overflow-y: auto;
261
260
  overflow-x: hidden;
262
261
  line-height: normal;
262
+
263
+ @include styled-scrollbar;
263
264
  }
264
265
 
265
266
  .panel-open {
@@ -389,5 +390,7 @@
389
390
  padding: margin(1.5);
390
391
  background: theme(dark);
391
392
  overflow-y: auto;
393
+
394
+ @include styled-scrollbar;
392
395
  }
393
396
  </style>
@@ -11,7 +11,6 @@
11
11
  import { track } from '$lib/tracking'
12
12
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
13
13
  import type { LinkInjection } from '$lib/types/injection'
14
- import type { TitleData } from '$lib/types/title'
15
14
  import { cleanPhrase, truncateAroundPhrase } from '$lib/text'
16
15
  import { isValidPlaylinkType } from '$lib/injection'
17
16
  import { getLinkInjectionElements, getLinkInjectionsParentElement } from '$lib/injectionElements'
@@ -27,9 +26,8 @@
27
26
 
28
27
  const { linkInjection = $bindable(), onremove = () => null, onhighlight = () => null }: Props = $props()
29
28
 
30
- const { key, sentence, title_details, failed, failed_message, inactive } = $derived(linkInjection || {})
29
+ const { key, type, sentence, title_details, participant_details, failed, failed_message, inactive } = $derived(linkInjection || {})
31
30
 
32
- const title: TitleData = $derived(title_details!)
33
31
  const truncatedSentence = $derived(truncateAroundPhrase(linkInjection.sentence, linkInjection.title, 60))
34
32
 
35
33
  let expanded = $state(false)
@@ -38,7 +36,7 @@
38
36
  let element: HTMLElement | null = $state(null)
39
37
 
40
38
  onMount(() => {
41
- if (failed) track(TrackingEvent.InjectionFailed, title, { phrase: linkInjection.title, sentence})
39
+ if (failed) track(TrackingEvent.InjectionFailed, type === 'title' ? title_details : null, { phrase: linkInjection.title, sentence})
42
40
  })
43
41
 
44
42
  /**
@@ -113,10 +111,14 @@
113
111
  bind:this={element}
114
112
  out:slide|global={{ duration: 200 }}>
115
113
  <div class="header">
116
- <img class="poster" src={removeImageUrlPrefix(title.standing_poster)} alt="" width="32" height="48" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
114
+ {#if type === 'title'}
115
+ <img class="poster" src={removeImageUrlPrefix(title_details!.standing_poster)} alt="" width="32" height="48" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
116
+ {:else}
117
+ <div class="placeholder-image"></div>
118
+ {/if}
117
119
 
118
120
  <div class="info">
119
- <div class="title">{title.title}</div>
121
+ <div class="title">{title_details?.title || participant_details?.name}</div>
120
122
 
121
123
  <div class="sentence">
122
124
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
@@ -143,9 +145,13 @@
143
145
  <Alert>{failed_message}</Alert>
144
146
  {:else}
145
147
  <div class="actions">
146
- <button class="expand" onclick={() => expanded = !expanded} aria-label="Expand" aria-expanded={expanded}>
147
- <IconChevron {expanded} />
148
- </button>
148
+ {#if type === 'title'}
149
+ <button class="expand" onclick={() => expanded = !expanded} aria-label="Expand" aria-expanded={expanded}>
150
+ <IconChevron {expanded} />
151
+ </button>
152
+ {:else}
153
+ <div></div>
154
+ {/if}
149
155
 
150
156
  {#if !isValidPlaylinkType(linkInjection)}
151
157
  <div class="warning" transition:fade={{ duration: 100 }} aria-label="Invalid playlink settings">
@@ -210,6 +216,14 @@
210
216
  }
211
217
  }
212
218
 
219
+ .placeholder-image {
220
+ flex: 0 0 margin(2);
221
+ width: margin(2);
222
+ height: margin(2);
223
+ border-radius: 50%;
224
+ background: theme(content);
225
+ }
226
+
213
227
  .title {
214
228
  font-size: margin(0.875);
215
229
  word-break: break-word;
@@ -170,6 +170,7 @@
170
170
  key: generateInjectionKey(selectedTitle.sid),
171
171
  title_details: selectedTitle,
172
172
  manual: true,
173
+ type: 'title',
173
174
  }
174
175
 
175
176
  onsave(linkInjection)
@@ -40,7 +40,7 @@
40
40
  })
41
41
  </script>
42
42
 
43
- <div class="explore playpilot-styled-scrollbar" bind:this={element} bind:clientWidth style:height style:--playpilot-explore-width="{clientWidth}px">
43
+ <div class="explore" bind:this={element} bind:clientWidth style:height style:--playpilot-explore-width="{clientWidth}px">
44
44
  <div class="header" role="banner">
45
45
  <div>
46
46
  <div class="divider"></div>
@@ -108,6 +108,8 @@
108
108
  font-weight: theme(detail-font-weight, normal);
109
109
  font-size: theme(detail-font-size, font-size-base);
110
110
 
111
+ @include styled-scrollbar;
112
+
111
113
  :global(*) {
112
114
  box-sizing: border-box;
113
115
  }
@@ -68,7 +68,7 @@
68
68
 
69
69
  {#if active}
70
70
  <div
71
- class="content playpilot-styled-scrollbar"
71
+ class="content"
72
72
  style:--max-height={maxHeight ? `${maxHeight}px` : null}
73
73
  style:--offset={offset ? `${offset}px` : null}
74
74
  bind:this={contentElement}
@@ -96,6 +96,8 @@
96
96
  overflow-y: auto;
97
97
  overflow-x: hidden;
98
98
 
99
+ @include styled-scrollbar;
100
+
99
101
  .left & {
100
102
  right: auto;
101
103
  left: var(--offset, 0px);
@@ -3,35 +3,48 @@
3
3
  import { track } from '$lib/tracking'
4
4
  import { getFirstAdOfType } from '$lib/api/ads'
5
5
  import type { TitleData } from '$lib/types/title'
6
- import { onMount } from 'svelte'
6
+ import type { ParticipantData } from '$lib/types/participant'
7
+ import type { LinkInjectionDataType } from '$lib/types/injection'
8
+ import { onMount, setContext } from 'svelte'
7
9
  import { isPixelAllowed } from '$lib/pixel'
8
10
  import { trackViaPixel } from '@playpilot/retargeting-tracking'
9
11
  import Popover from './Popover.svelte'
10
12
  import Title from './Title.svelte'
13
+ import Participant from './Participant.svelte'
11
14
  import TopScroll from './Ads/TopScroll.svelte'
12
15
  import Display from './Ads/Display.svelte'
13
16
 
14
17
  interface Props {
15
18
  event: MouseEvent
16
- title: TitleData
19
+ data: TitleData | ParticipantData
20
+ type?: LinkInjectionDataType
17
21
  }
18
22
 
19
- const { event, title }: Props = $props()
23
+ const { event, data, type = 'title' }: Props = $props()
24
+
25
+ setContext('type', type)
20
26
 
21
27
  const topScroll = getFirstAdOfType('top_scroll')
22
28
  const displayAd = getFirstAdOfType('card')
23
29
 
24
30
  let element: HTMLElement | null = $state(null)
25
31
 
26
- track(TrackingEvent.TitlePopoverView, title)
32
+ if (type === 'title') track(TrackingEvent.InjectionPopoverView, data as TitleData, { type })
33
+ if (type === 'participant') track(TrackingEvent.InjectionPopoverView, null, { type, participant: (data as ParticipantData).name })
27
34
 
28
- if (isPixelAllowed()) trackViaPixel(MetaEvent.TitleInterest, { title: title.title, source: MetaSource.Card })
35
+ if (isPixelAllowed() && type === 'title') trackViaPixel(MetaEvent.TitleInterest, { title: (data as TitleData).title, source: MetaSource.Card })
29
36
 
30
37
  onMount(() => {
31
38
  setOffset()
32
39
 
33
40
  const openTimestamp = Date.now()
34
- return () => track(TrackingEvent.TitlePopoverClose, title, { time_spent: Date.now() - openTimestamp })
41
+
42
+ return () => {
43
+ const endTimestamp = Date.now() - openTimestamp
44
+
45
+ if (type === 'title') track(TrackingEvent.InjectionPopoverClose, data as TitleData, { type, time_spent: endTimestamp })
46
+ if (type === 'participant') track(TrackingEvent.InjectionPopoverClose, null, { type, participant: (data as ParticipantData).name, time_spent: endTimestamp })
47
+ }
35
48
  })
36
49
 
37
50
  /**
@@ -68,14 +81,18 @@
68
81
  {/if}
69
82
  {/snippet}
70
83
 
71
- <div class="title-popover" bind:this={element} data-playpilot-title-popover role="region" aria-labelledby="title">
84
+ <div class="injection-popover" bind:this={element} data-playpilot-injection-popover role="region" aria-labelledby="heading">
72
85
  <Popover append={displayAd ? append : null} bubble={topScroll ? bubble : null}>
73
- <Title {title} small />
86
+ {#if type === 'title'}
87
+ <Title title={data as TitleData} small />
88
+ {:else if type === 'participant'}
89
+ <Participant participant={data as ParticipantData} small />
90
+ {/if}
74
91
  </Popover>
75
92
  </div>
76
93
 
77
94
  <style lang="scss">
78
- .title-popover {
95
+ .injection-popover {
79
96
  position: absolute;
80
97
  }
81
98
  </style>