@playpilot/tpi 8.10.1 → 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.
- package/.env +1 -1
- package/dist/editorial.mount.js +9 -9
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +6 -6
- package/package.json +1 -1
- package/src/lib/api/youtubeAvailability.ts +3 -4
- package/src/lib/enums/TrackingEvent.ts +4 -4
- package/src/lib/fakeData.ts +44 -32
- package/src/lib/injection.ts +10 -17
- package/src/lib/modal.ts +3 -1
- package/src/lib/popover.ts +13 -12
- package/src/lib/types/injection.d.ts +5 -0
- package/src/routes/components/Editorial/Editor.svelte +2 -3
- package/src/routes/components/Editorial/EditorItem.svelte +23 -9
- package/src/routes/components/Editorial/ManualInjection.svelte +1 -0
- package/src/routes/components/{TitlePopover.svelte → InjectionPopover.svelte} +26 -9
- package/src/routes/components/ListTitle.svelte +27 -6
- package/src/routes/components/Participant.svelte +18 -6
- package/src/routes/components/Playlinks/Playlinks.svelte +2 -1
- package/src/routes/components/Title.svelte +1 -1
- package/src/routes/elements/+page.svelte +3 -3
- package/src/tests/helpers.js +1 -0
- package/src/tests/lib/api/youtubeAvailability.test.js +3 -3
- package/src/tests/lib/injection.test.js +44 -3
- package/src/tests/lib/popover.test.js +7 -7
- package/src/tests/routes/components/Editorial/EditorItem.test.js +10 -0
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +4 -0
- package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +2 -0
- package/src/tests/routes/components/InjectionPopover.test.js +117 -0
- package/src/tests/routes/components/ListTitle.test.js +7 -0
- package/src/tests/routes/components/Participant.test.js +7 -0
- package/src/tests/routes/components/Playlinks/AfterArticlePlaylinks.test.js +4 -0
- package/src/tests/routes/components/Playlinks/Playlinks.test.js +1 -1
- package/src/tests/routes/components/TitlePopover.test.js +0 -78
package/package.json
CHANGED
|
@@ -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
|
|
22
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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',
|
package/src/lib/fakeData.ts
CHANGED
|
@@ -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',
|
package/src/lib/injection.ts
CHANGED
|
@@ -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(
|
|
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 &&
|
|
379
|
+
return !injection.inactive && !injection.removed && !injection.duplicate && hasValidTypeData(injection) && isValidPlaylinkType(injection)
|
|
388
380
|
}
|
|
389
381
|
|
|
390
382
|
/**
|
|
391
|
-
* An injection can
|
|
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.
|
|
139
|
+
openModal({ event, injection, data, type: injection.type })
|
|
138
140
|
}, !prefersReducedMotion.current && window.innerWidth >= mobileBreakpoint && !window.matchMedia('(pointer: coarse)').matches)
|
|
139
141
|
}
|
package/src/lib/popover.ts
CHANGED
|
@@ -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
|
|
19
|
-
currentlyHoveredInjection =
|
|
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 !==
|
|
19
|
+
if (currentlyHoveredInjection !== currentTarget) return
|
|
25
20
|
|
|
26
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
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;
|
|
@@ -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 {
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
42
|
-
{
|
|
43
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
&.
|
|
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.
|
|
41
|
+
track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.InjectionPopoverPlaylinkClick, title, { playlink, type })
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function categorizePlaylinks(playlinks: PlaylinkData[]): CategorizedPlaylinks {
|