@playpilot/tpi 8.10.0 → 8.10.2

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 (37) 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 +6 -6
  5. package/package.json +1 -1
  6. package/src/lib/api/youtubeAvailability.ts +3 -4
  7. package/src/lib/enums/TrackingEvent.ts +4 -4
  8. package/src/lib/fakeData.ts +44 -32
  9. package/src/lib/injection.ts +10 -17
  10. package/src/lib/modal.ts +3 -1
  11. package/src/lib/popover.ts +13 -12
  12. package/src/lib/types/injection.d.ts +5 -0
  13. package/src/routes/components/Editorial/Editor.svelte +2 -3
  14. package/src/routes/components/Editorial/EditorItem.svelte +23 -9
  15. package/src/routes/components/Editorial/ManualInjection.svelte +1 -0
  16. package/src/routes/components/{TitlePopover.svelte → InjectionPopover.svelte} +26 -9
  17. package/src/routes/components/ListTitle.svelte +27 -6
  18. package/src/routes/components/Participant.svelte +18 -6
  19. package/src/routes/components/Playlinks/Playlinks.svelte +2 -1
  20. package/src/routes/components/Title.svelte +1 -1
  21. package/src/routes/components/TrackAnyClick.svelte +15 -2
  22. package/src/routes/components/YouTubeEmbed.svelte +1 -1
  23. package/src/routes/elements/+page.svelte +3 -3
  24. package/src/tests/helpers.js +1 -0
  25. package/src/tests/lib/api/youtubeAvailability.test.js +3 -3
  26. package/src/tests/lib/injection.test.js +44 -3
  27. package/src/tests/lib/popover.test.js +7 -7
  28. package/src/tests/routes/components/Editorial/EditorItem.test.js +10 -0
  29. package/src/tests/routes/components/Editorial/ManualInjection.test.js +4 -0
  30. package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +2 -0
  31. package/src/tests/routes/components/InjectionPopover.test.js +117 -0
  32. package/src/tests/routes/components/ListTitle.test.js +7 -0
  33. package/src/tests/routes/components/Participant.test.js +7 -0
  34. package/src/tests/routes/components/Playlinks/AfterArticlePlaylinks.test.js +4 -0
  35. package/src/tests/routes/components/Playlinks/Playlinks.test.js +1 -1
  36. package/src/tests/routes/components/TrackAnyClick.test.js +137 -1
  37. 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.10.0",
3
+ "version": "8.10.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -1,6 +1,4 @@
1
1
  import { PUBLIC_YOUTUBE_AVAILABILITY_URL } from '$env/static/public'
2
- import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
- import { track } from '$lib/tracking'
4
2
 
5
3
  export async function isYouTubeVideoAvailableInRegion(videoId: string): Promise<boolean> {
6
4
  const region = window.PlayPilotLinkInjections?.region?.toUpperCase()
@@ -18,8 +16,9 @@ export async function isYouTubeVideoAvailableInRegion(videoId: string): Promise<
18
16
  if (!data.blocked && !!data.allowed && !data.allowed?.includes(region)) return false
19
17
 
20
18
  return true
21
- } catch (error: any) {
22
- track(TrackingEvent.YouTubeAvailabilityRequestFailed, null, { message: error.message })
19
+ } catch {
20
+ // Silently fail for now
21
+ // track(TrackingEvent.YouTubeAvailabilityRequestFailed, null, { message: error.message })
23
22
 
24
23
  return false
25
24
  }
@@ -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,
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,
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,
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,
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,
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,
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',
@@ -168,8 +168,7 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
168
168
  // The function itself will decide whether or not it should actually insert the component based on the config.
169
169
  if (document.querySelector(keySelector)) insertInTextDisclaimer(elements)
170
170
 
171
- return mergedInjections.filter(i => i.title_details).map((injection, index) => {
172
- // Favour manual injections over AI injections
171
+ return mergedInjections.filter(injection => hasValidTypeData(injection)).map((injection, index) => {
173
172
  const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
174
173
  const duplicate = injection.duplicate ?? hasManualEquivalent
175
174
 
@@ -307,9 +306,6 @@ function addCSSVariablesToLinks(): void {
307
306
  }
308
307
  }
309
308
 
310
- /**
311
- * Add event listeners to all injected links. These events are for both the popover and the modal.
312
- */
313
309
  function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
314
310
  window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
315
311
  window.addEventListener('click', (event) => openModalForInjectedLink(event, injections))
@@ -341,10 +337,6 @@ export function clearLinkInjections(): void {
341
337
  destroyLinkPopover(false)
342
338
  }
343
339
 
344
- /**
345
- * Clear specific link injection from the page
346
- * @param key Given of the injection to be removed from the page
347
- */
348
340
  export function clearLinkInjection(key: string): void {
349
341
  const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
350
342
  if (!element) return
@@ -384,11 +376,11 @@ export function separateLinkInjectionTypes(injections: LinkInjection[]): LinkInj
384
376
  }
385
377
 
386
378
  export function isValidInjection(injection: LinkInjection): boolean {
387
- return !injection.inactive && !injection.removed && !injection.duplicate && !!injection.title_details && isValidPlaylinkType(injection)
379
+ return !injection.inactive && !injection.removed && !injection.duplicate && hasValidTypeData(injection) && isValidPlaylinkType(injection)
388
380
  }
389
381
 
390
382
  /**
391
- * An injection can have be of various playlink types, when all are false equivalent, the link is not valid.
383
+ * An injection can be of various playlink types, when all are false equivalent, the link is not valid.
392
384
  * It should be treated similar to an inactive playlink in this case.
393
385
  */
394
386
  export function isValidPlaylinkType(injection: LinkInjection): boolean {
@@ -396,16 +388,10 @@ export function isValidPlaylinkType(injection: LinkInjection): boolean {
396
388
  return !!injection.after_article
397
389
  }
398
390
 
399
- /**
400
- * Filter links for in-text injections, removing after article, inactive, removed, duplicate, and items without title_details
401
- */
402
391
  export function filterInvalidInTextInjections(injections: LinkInjection[]): LinkInjection[] {
403
392
  return filterRemovedAndInactiveInjections(injections).filter(i => i.in_text !== false && isValidInjection(i))
404
393
  }
405
394
 
406
- /**
407
- * Filter links for after article injections, removing in-text only, inactive, removed, duplicate, and items without title_details
408
- */
409
395
  export function filterInvalidAfterArticleInjections(injections: LinkInjection[]): LinkInjection[] {
410
396
  return filterRemovedAndInactiveInjections(injections).filter(i => i.after_article === true && isValidInjection(i))
411
397
  }
@@ -450,6 +436,13 @@ export function isEquivalentInjection(injection1: LinkInjection, injection2: Lin
450
436
  return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
451
437
  }
452
438
 
439
+ export function hasValidTypeData(injection: LinkInjection): boolean {
440
+ if (injection.type === 'title' && !!injection.title_details) return true
441
+ if (injection.type === 'participant' && !!injection.participant_details) return true
442
+
443
+ return false
444
+ }
445
+
453
446
  export function getPlayPilotWrapperElement(): Element {
454
447
  return document.querySelector('[data-playpilot-link-injections]') || document.body
455
448
  }
package/src/lib/modal.ts CHANGED
@@ -133,7 +133,9 @@ export function openModalForInjectedLink(event: MouseEvent, injections: LinkInje
133
133
  event.preventDefault()
134
134
 
135
135
  playFallbackViewTransition(() => {
136
+ const data = injection.title_details || injection.participant_details
137
+
136
138
  destroyLinkPopover(false)
137
- openModal({ event, injection, data: injection.title_details })
139
+ openModal({ event, injection, data, type: injection.type })
138
140
  }, !prefersReducedMotion.current && window.innerWidth >= mobileBreakpoint && !window.matchMedia('(pointer: coarse)').matches)
139
141
  }
@@ -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()
@@ -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 }>
@@ -58,8 +58,7 @@
58
58
  const linkInjectionsString = $derived(JSON.stringify(linkInjections))
59
59
  const showControls = $derived(!aiRunning && allowEditing)
60
60
  const hasChanged = $derived(initialStateString && initialStateString !== linkInjectionsString)
61
- // Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
62
- const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
61
+ const filteredInjections = $derived(linkInjections.filter((i) => (i.title_details || i.participant_details) && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
63
62
  const sortedInjections = $derived(sortInjections(filteredInjections))
64
63
  const initialAiRunning = $derived(!loading && untrack(() => aiStatus.aiRunning))
65
64
 
@@ -75,7 +74,7 @@
75
74
  function sortInjections(injections: LinkInjection[]): LinkInjection[] {
76
75
  return injections.sort((a, b) => {
77
76
  if (a.failed !== b.failed) return a.failed ? 1 : -1
78
- return a.title_details!.title.localeCompare(b.title_details!.title)
77
+ return (a.title_details?.title || a.participant_details?.name)!.localeCompare((b.title_details?.title || b.participant_details?.name)!)
79
78
  })
80
79
  }
81
80
 
@@ -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;
@@ -169,6 +169,7 @@
169
169
  key: generateInjectionKey(selectedTitle.sid),
170
170
  title_details: selectedTitle,
171
171
  manual: true,
172
+ type: 'title',
172
173
  }
173
174
 
174
175
  onsave(linkInjection)
@@ -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>
@@ -8,15 +8,16 @@
8
8
 
9
9
  interface Props {
10
10
  title: TitleData
11
+ compact?: boolean
11
12
  onclick?: (event: MouseEvent) => void
12
13
  }
13
14
 
14
- const { title, onclick = () => null }: Props = $props()
15
+ const { title, compact = false, onclick = () => null }: Props = $props()
15
16
 
16
17
  const noAffiliate = !!window.PlayPilotLinkInjections?.no_affiliate
17
18
  </script>
18
19
 
19
- <button class="title" {onclick} data-testid="title">
20
+ <button class="title" class:compact {onclick} data-testid="title">
20
21
  <div class="poster">
21
22
  <TitlePoster {title} width={30} height={43} lazy />
22
23
  </div>
@@ -38,12 +39,14 @@
38
39
  {/if}
39
40
  </div>
40
41
 
41
- <div class="description" class:large={noAffiliate}>
42
- {title.description}
43
- </div>
42
+ {#if !compact}
43
+ <div class="description" class:large={noAffiliate}>
44
+ {title.description}
45
+ </div>
46
+ {/if}
44
47
 
45
48
  {#if !noAffiliate}
46
- <PlaylinksCompact playlinks={title.providers} />
49
+ <PlaylinksCompact playlinks={title.providers} size={compact ? 24 : 30} />
47
50
  {/if}
48
51
  </div>
49
52
 
@@ -84,6 +87,10 @@
84
87
  border-radius: theme(detail-image-border-radius, border-radius);
85
88
  background: theme(detail-image-background, content);
86
89
  overflow: hidden;
90
+
91
+ .compact & {
92
+ width: margin(3.5);
93
+ }
87
94
  }
88
95
 
89
96
  .content {
@@ -97,11 +104,17 @@
97
104
  @include desktop() {
98
105
  padding-right: margin(1);
99
106
  }
107
+
108
+ .compact & {
109
+ padding-left: margin(0.75);
110
+ padding-right: 0;
111
+ }
100
112
  }
101
113
 
102
114
  .heading {
103
115
  color: theme(list-item-title-text-color, text-color);
104
116
  font-weight: theme(list-item-title-font-weight, font-bold);
117
+ line-height: 1.2;
105
118
  }
106
119
 
107
120
  .meta {
@@ -116,6 +129,10 @@
116
129
  > div {
117
130
  text-transform: capitalize;
118
131
  }
132
+
133
+ .compact & {
134
+ margin: margin(0.125) 0 margin(0.25) 0;
135
+ }
119
136
  }
120
137
 
121
138
  .imdb {
@@ -152,5 +169,9 @@
152
169
  &:active {
153
170
  color: theme(list-item-action-hover-color, text-color);
154
171
  }
172
+
173
+ .compact & {
174
+ display: none;
175
+ }
155
176
  }
156
177
  </style>
@@ -13,9 +13,10 @@
13
13
 
14
14
  interface Props {
15
15
  participant: ParticipantData
16
+ small?: boolean
16
17
  }
17
18
 
18
- const { participant }: Props = $props()
19
+ const { participant, small = false }: Props = $props()
19
20
 
20
21
  const { name, birth_date, death_date } = $derived(participant)
21
22
 
@@ -52,8 +53,8 @@
52
53
  }
53
54
  </script>
54
55
 
55
- <div class="header">
56
- <div class="heading" use:heading={2} id="name">{name}</div>
56
+ <div class="header" class:small>
57
+ <div class="heading" use:heading={2} id="heading">{name}</div>
57
58
 
58
59
  {#if birth_date}
59
60
  <p class="dates">
@@ -63,11 +64,13 @@
63
64
  </div>
64
65
 
65
66
  <div class="content">
66
- <div class="heading small" use:heading={3} id="credits">{t('Credits')}</div>
67
+ {#if !small}
68
+ <div class="heading subheading" use:heading={3} id="credits">{t('Credits')}</div>
69
+ {/if}
67
70
 
68
71
  <div class="list">
69
72
  {#each titles as title}
70
- <ListTitle {title} onclick={(event) => openModal({ event, data: title })} />
73
+ <ListTitle {title} compact={small} onclick={(event) => openModal({ event, data: title })} />
71
74
  {/each}
72
75
 
73
76
  {#if loading}
@@ -88,6 +91,10 @@
88
91
  font-size: theme(detail-font-size, font-size-base);
89
92
  line-height: theme(participant-description-line-height, normal);
90
93
  color: theme(detail-text-color, text-color);
94
+
95
+ &.small {
96
+ padding: margin(1);
97
+ }
91
98
  }
92
99
 
93
100
  .heading {
@@ -98,7 +105,7 @@
98
105
  line-height: normal;
99
106
  font-style: theme(detail-title-font-style, normal);
100
107
 
101
- &.small {
108
+ &.subheading {
102
109
  margin: 0 0 margin(0.5);
103
110
  font-size: theme(detail-title-small-font-size, margin(1.25));
104
111
  }
@@ -116,6 +123,11 @@
116
123
  font-size: theme(detail-font-size, font-size-base);
117
124
  line-height: normal;
118
125
  font-style: normal;
126
+
127
+ ::view-transition-old(content),
128
+ ::view-transition-new(content) {
129
+ height: 100%;
130
+ }
119
131
  }
120
132
 
121
133
  .list {
@@ -22,6 +22,7 @@
22
22
  const { playlinks, title }: Props = $props()
23
23
 
24
24
  const isModal = getContext('scope') === 'modal'
25
+ const type = getContext('type')
25
26
  const displayAd = getFirstAdOfType('card')
26
27
  const categorize = !!window.PlayPilotLinkInjections?.config?.categorize_playlinks
27
28
 
@@ -37,7 +38,7 @@
37
38
  const list = $derived(outerWidth < 500 || !!displayAd || mergedPlaylinks.length === 1)
38
39
 
39
40
  function onclick(playlink: string): void {
40
- track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.TitlePopoverPlaylinkClick, title, { playlink })
41
+ track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.InjectionPopoverPlaylinkClick, title, { playlink, type })
41
42
  }
42
43
 
43
44
  function categorizePlaylinks(playlinks: PlaylinkData[]): CategorizedPlaylinks {