@playpilot/tpi 5.33.0-beta.explore.6 → 5.33.0-beta.in-text.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.
- package/dist/link-injections.js +10 -10
- package/package.json +1 -1
- package/src/lib/api/ads.ts +1 -1
- package/src/lib/api/titles.ts +1 -13
- package/src/lib/color.ts +19 -0
- package/src/lib/data/translations.ts +0 -5
- package/src/lib/enums/SplitTest.ts +5 -0
- package/src/lib/fakeData.ts +0 -1
- package/src/lib/injection.ts +41 -0
- package/src/lib/modal.ts +6 -7
- package/src/lib/scss/global.scss +41 -0
- package/src/lib/types/config.d.ts +0 -12
- package/src/lib/types/title.d.ts +1 -4
- package/src/routes/+page.svelte +9 -8
- package/src/routes/components/Ads/TopScroll.svelte +18 -4
- package/src/routes/components/Debugger.svelte +8 -0
- package/src/routes/components/Icons/IconClose.svelte +1 -9
- package/src/routes/components/ListTitle.svelte +8 -7
- package/src/routes/components/Modal.svelte +24 -6
- package/src/routes/components/Share.svelte +23 -5
- package/src/routes/components/Title.svelte +22 -22
- package/src/routes/components/TitleModal.svelte +1 -4
- package/src/routes/elements/+page.svelte +2 -39
- package/src/tests/lib/api/ads.test.js +1 -0
- package/src/tests/routes/components/Share.test.js +12 -12
- package/src/tests/routes/components/Title.test.js +0 -13
- package/src/lib/explore.ts +0 -59
- package/src/lib/images/titles-list.webp +0 -0
- package/src/lib/trailer.ts +0 -22
- package/src/lib/types/api.d.ts +0 -6
- package/src/routes/components/Button.svelte +0 -73
- package/src/routes/components/Explore/Explore.svelte +0 -191
- package/src/routes/components/Explore/ExploreCallToAction.svelte +0 -58
- package/src/routes/components/Explore/ExploreModal.svelte +0 -15
- package/src/routes/components/Explore/Filter.svelte +0 -3
- package/src/routes/components/Explore/Search.svelte +0 -56
- package/src/routes/components/Icons/IconPlay.svelte +0 -3
- package/src/routes/components/Icons/IconSearch.svelte +0 -3
- package/src/routes/components/ListTitleSkeleton.svelte +0 -42
- package/src/routes/components/Trailer.svelte +0 -18
- package/src/routes/components/YouTubeEmbedOverlay.svelte +0 -96
- package/src/routes/explore/+page.svelte +0 -61
- package/src/tests/lib/api/titles.test.js +0 -55
- package/src/tests/lib/explore.test.js +0 -139
- package/src/tests/lib/trailer.test.js +0 -56
- package/src/tests/routes/components/Button.test.js +0 -28
- package/src/tests/routes/components/Explore/Explore.test.js +0 -133
- package/src/tests/routes/components/Explore/Search.test.js +0 -26
- package/src/tests/routes/components/Trailer.test.js +0 -20
- package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +0 -31
package/package.json
CHANGED
package/src/lib/api/ads.ts
CHANGED
|
@@ -14,7 +14,7 @@ export async function fetchAds(): Promise<Campaign[]> {
|
|
|
14
14
|
if (!apiToken) throw new Error('No token was provided')
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
|
-
const response = await api<Campaign[]>(`/ads/browse/?
|
|
17
|
+
const response = await api<Campaign[]>(`/ads/browse/?api-token=${apiToken}`)
|
|
18
18
|
|
|
19
19
|
return response
|
|
20
20
|
} catch (error: any) {
|
package/src/lib/api/titles.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import { getApiToken } from '$lib/token'
|
|
2
|
-
import type { APIPaginatedResult } from '$lib/types/api'
|
|
3
2
|
import type { TitleData } from '../types/title'
|
|
4
3
|
import { api } from './api'
|
|
5
4
|
|
|
6
|
-
export async function fetchTitles(params: Record<string, string | number> = {}): Promise<APIPaginatedResult<TitleData>> {
|
|
7
|
-
const apiToken = getApiToken()
|
|
8
|
-
|
|
9
|
-
if (!apiToken) throw new Error('No token was provided')
|
|
10
|
-
|
|
11
|
-
const paramsAsString = Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&')
|
|
12
|
-
const response = await api<APIPaginatedResult<TitleData>>(`/titles/browse?api-token=${apiToken}&` + paramsAsString)
|
|
13
|
-
|
|
14
|
-
return response
|
|
15
|
-
}
|
|
16
|
-
|
|
17
5
|
export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]> {
|
|
18
|
-
const response = await
|
|
6
|
+
const response = await api<{ results: TitleData[] }>(`/titles/browse?api-token=${getApiToken()}&related_to_sid=${title.sid}`)
|
|
19
7
|
|
|
20
8
|
return response.results
|
|
21
9
|
}
|
package/src/lib/color.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Adjust the lightness of an RGB color. */
|
|
2
|
+
export function colorLuminance(input: string, luminosity: number): string {
|
|
3
|
+
const match = input
|
|
4
|
+
.replace(/\s+/g, '')
|
|
5
|
+
.match(/^rgb\((\d+),(\d+),(\d+)\)$/i)
|
|
6
|
+
|
|
7
|
+
if (!match) return ''
|
|
8
|
+
|
|
9
|
+
const [, r, g, b] = match
|
|
10
|
+
|
|
11
|
+
const adjust = (v: string) => {
|
|
12
|
+
const color = parseInt(v, 10)
|
|
13
|
+
return Math.round(Math.min(Math.max(0, color + color * luminosity), 255))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `rgb(${adjust(r)}, ${adjust(g)}, ${adjust(b)})`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
@@ -126,11 +126,6 @@ export const translations = {
|
|
|
126
126
|
[Language.Swedish]: 'Liknande filmer och serier',
|
|
127
127
|
[Language.Danish]: 'Lignende film og serier',
|
|
128
128
|
},
|
|
129
|
-
'Watch Trailer': {
|
|
130
|
-
[Language.English]: 'Watch trailer',
|
|
131
|
-
[Language.Swedish]: 'Se trailer',
|
|
132
|
-
[Language.Danish]: 'Se trailer',
|
|
133
|
-
},
|
|
134
129
|
|
|
135
130
|
// Genres
|
|
136
131
|
'All': {
|
|
@@ -9,4 +9,9 @@ export const SplitTest = {
|
|
|
9
9
|
numberOfVariants: 2,
|
|
10
10
|
variantNames: ['Image', 'Label'] as string[],
|
|
11
11
|
},
|
|
12
|
+
InTextEngagement: {
|
|
13
|
+
key: 'in-text-engagement',
|
|
14
|
+
numberOfVariants: 4,
|
|
15
|
+
variantNames: ['Default', 'Clapper Icon', 'Play Icon', 'Animated Link'] as string[]
|
|
16
|
+
}
|
|
12
17
|
} as const
|
package/src/lib/fakeData.ts
CHANGED
|
@@ -38,7 +38,6 @@ export const title: TitleData = {
|
|
|
38
38
|
standing_poster: 'https://img.playpilot.tech/6239ee86a58f11efb0b50a58a9feac02/src/img?optimizer=image&class=2by3x18',
|
|
39
39
|
title: 'Dune: Prophecy',
|
|
40
40
|
original_title: 'Dune: Prophecy',
|
|
41
|
-
embeddable_url: null,
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
export const linkInjections: LinkInjection[] = [{
|
package/src/lib/injection.ts
CHANGED
|
@@ -10,10 +10,15 @@ import { getNumberOfOccurrencesInArray } from './array'
|
|
|
10
10
|
import { mobileBreakpoint } from './constants'
|
|
11
11
|
import { destroyAllModals, openModal } from './modal'
|
|
12
12
|
import InTextDisclaimer from '../routes/components/InTextDisclaimer.svelte'
|
|
13
|
+
import { getSplitTestVariantName, trackSplitTestAction } from './splitTest'
|
|
14
|
+
import { SplitTest } from './enums/SplitTest'
|
|
15
|
+
import { colorLuminance } from './color'
|
|
13
16
|
|
|
14
17
|
export const keyDataAttribute = 'data-playpilot-injection-key'
|
|
15
18
|
export const keySelector = `[${keyDataAttribute}]`
|
|
16
19
|
|
|
20
|
+
const linksIntersectionObserver = typeof window !== 'undefined' ? new IntersectionObserver(animateLink) : null
|
|
21
|
+
|
|
17
22
|
let currentlyHoveredInjection: EventTarget | null = null
|
|
18
23
|
let activePopoverInsertedComponent: object | null = null
|
|
19
24
|
let afterArticlePlaylinkInsertedComponent: object | null = null
|
|
@@ -306,6 +311,19 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
306
311
|
linkElement.target = '_blank'
|
|
307
312
|
linkElement.rel = 'noopener nofollow noreferrer'
|
|
308
313
|
|
|
314
|
+
// Part of a split test, insert an absolute positioned icon in the top right of links.
|
|
315
|
+
if (getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' || getSplitTestVariantName(SplitTest.InTextEngagement) === 'Play Icon') {
|
|
316
|
+
const iconElement = document.createElement('span')
|
|
317
|
+
iconElement.classList.add('playpilot-injection-info-icon')
|
|
318
|
+
iconElement.dataset.playpilotElement = 'true'
|
|
319
|
+
|
|
320
|
+
const playIcon = '<svg width="8" height="8" viewBox="0 0 8 8" fill="none"><path d="M7.66011 4.53958C8.1133 4.31131 8.1133 3.68869 7.6601 3.46042L0.930122 0.0706345C0.507292 -0.142339 8.99396e-08 0.151949 8.42451e-08 0.610212L0 7.38979C-5.69452e-09 7.84805 0.507291 8.14234 0.930122 7.92937L7.66011 4.53958Z" fill="currentColor"/></svg>'
|
|
321
|
+
const clapperIcon = '<svg width="10" height="12" viewBox="0 0 13 16" fill="none"><rect x="0.950195" y="6.56641" width="12.0498" height="8.80563" rx="1" fill="currentColor" /><rect y="5.92969" width="12" height="2.4" rx="0.5" transform="rotate(-29.6116 0 5.92969)" fill="currentColor" /><path d="M8.82314 11.4484C9.02159 11.3443 9.02159 11.0602 8.82314 10.956L5.87605 9.40918C5.69089 9.312 5.46875 9.44629 5.46875 9.6554L5.46875 12.749C5.46875 12.9582 5.69089 13.0924 5.87605 12.9953L8.82314 11.4484Z" fill="white" /></svg>'
|
|
322
|
+
|
|
323
|
+
iconElement.innerHTML = getSplitTestVariantName(SplitTest.InTextEngagement) === 'Clapper Icon' ? clapperIcon : playIcon
|
|
324
|
+
linkElement.insertAdjacentElement('beforeend', iconElement)
|
|
325
|
+
}
|
|
326
|
+
|
|
309
327
|
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
310
328
|
|
|
311
329
|
return { injectionElement, linkElement }
|
|
@@ -428,6 +446,8 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
428
446
|
|
|
429
447
|
event.preventDefault()
|
|
430
448
|
|
|
449
|
+
trackSplitTestAction(SplitTest.InTextEngagement, 'click')
|
|
450
|
+
|
|
431
451
|
playFallbackViewTransition(() => {
|
|
432
452
|
destroyLinkPopover(false)
|
|
433
453
|
openModal({ event, injection, data: injection.title_details })
|
|
@@ -462,10 +482,28 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
462
482
|
injectionElement.addEventListener('mouseleave', () => {
|
|
463
483
|
currentlyHoveredInjection = null
|
|
464
484
|
})
|
|
485
|
+
|
|
486
|
+
linksIntersectionObserver!.observe(injectionElement)
|
|
465
487
|
})
|
|
466
488
|
}
|
|
467
489
|
|
|
490
|
+
export function animateLink(entries: IntersectionObserverEntry[]): void {
|
|
491
|
+
if (getSplitTestVariantName(SplitTest.InTextEngagement) !== 'Animated Link') return
|
|
468
492
|
|
|
493
|
+
entries.forEach(entry => {
|
|
494
|
+
if (!entry.isIntersecting) return
|
|
495
|
+
|
|
496
|
+
const linkElement = entry.target.querySelector('a')
|
|
497
|
+
if (!linkElement) return
|
|
498
|
+
|
|
499
|
+
const linkColor = window.getComputedStyle(linkElement).color
|
|
500
|
+
|
|
501
|
+
;(entry.target as HTMLElement).style = `--animation-color: ${colorLuminance(linkColor, -0.5)}`
|
|
502
|
+
entry.target.classList.add('animate-injection')
|
|
503
|
+
|
|
504
|
+
linksIntersectionObserver!.unobserve(entry.target)
|
|
505
|
+
})
|
|
506
|
+
}
|
|
469
507
|
|
|
470
508
|
/**
|
|
471
509
|
* When a link is hovered, it is shown as a popover. The component is mounted when a mouse enters the link,
|
|
@@ -590,6 +628,9 @@ export function clearLinkInjection(key: string): void {
|
|
|
590
628
|
const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
|
|
591
629
|
if (!element) return
|
|
592
630
|
|
|
631
|
+
const playpilotElements = element.querySelectorAll('[data-playpilot-element]')
|
|
632
|
+
playpilotElements.forEach(element => element.remove())
|
|
633
|
+
|
|
593
634
|
const linkContent = element.querySelector('a')?.innerHTML
|
|
594
635
|
element.outerHTML = linkContent || ''
|
|
595
636
|
}
|
package/src/lib/modal.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { mount, unmount } from "svelte"
|
|
2
2
|
import { isHoldingSpecialKey } from "./event"
|
|
3
|
+
import TitleModal from "../routes/components/TitleModal.svelte"
|
|
3
4
|
import type { LinkInjection } from "./types/injection"
|
|
5
|
+
import ParticipantModal from "../routes/components/ParticipantModal.svelte"
|
|
4
6
|
import type { TitleData } from "./types/title"
|
|
5
7
|
import type { ParticipantData } from "./types/participant"
|
|
6
8
|
import { mobileBreakpoint } from "./constants"
|
|
7
9
|
import { getPlayPilotWrapperElement } from "./injection"
|
|
8
|
-
import TitleModal from "../routes/components/TitleModal.svelte"
|
|
9
|
-
import ParticipantModal from "../routes/components/ParticipantModal.svelte"
|
|
10
|
-
import ExploreModal from "../routes/components/Explore/ExploreModal.svelte"
|
|
11
10
|
|
|
12
|
-
type ModalType = 'title' | 'participant'
|
|
11
|
+
type ModalType = 'title' | 'participant'
|
|
13
12
|
|
|
14
13
|
type Modal = {
|
|
15
14
|
injection?: LinkInjection | null
|
|
@@ -47,9 +46,9 @@ export function openModal(
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
function getModalComponentByType({ type = 'title', target, data, props = {} }: { type: ModalType, target: Element, data: TitleData | ParticipantData | null, props?: Record<string, any> }) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
return type === 'title' ?
|
|
50
|
+
mount(TitleModal, { target, props: { title: data as TitleData, ...props } }) :
|
|
51
|
+
mount(ParticipantModal, { target, props: { participant: data as ParticipantData, ...props } })
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
function addModalToList({ type = 'title', injection = null, data, scrollPosition = 0, component }: Modal) {
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -7,8 +7,47 @@
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
@keyframes animate-injection {
|
|
11
|
+
0% {
|
|
12
|
+
background-position: 0% 0%;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
100% {
|
|
16
|
+
background-position: -300% 0%;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
[data-playpilot-injection-key] {
|
|
11
21
|
position: relative;
|
|
22
|
+
|
|
23
|
+
&.animate-injection a {
|
|
24
|
+
background: linear-gradient(to right, currentColor 50%, var(--animation-color), currentcolor);
|
|
25
|
+
background-clip: text;
|
|
26
|
+
-webkit-background-clip: text;
|
|
27
|
+
-webkit-text-fill-color: transparent;
|
|
28
|
+
background-size: 200% auto;
|
|
29
|
+
animation: animate-injection 750ms 500ms ease-in-out forwards;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.playpilot-injection-info-icon {
|
|
34
|
+
position: relative;
|
|
35
|
+
display: inline-block;
|
|
36
|
+
height: 1em;
|
|
37
|
+
|
|
38
|
+
svg {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
align-items: center;
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: -0.5em;
|
|
44
|
+
right: -0.25em;
|
|
45
|
+
font-size: 0.65em;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
h1 &, h2 &, h3 &, h4 &, h5 &, h6 & {
|
|
49
|
+
display: none
|
|
50
|
+
}
|
|
12
51
|
}
|
|
13
52
|
|
|
14
53
|
.playpilot-injection-highlight {
|
|
@@ -21,7 +60,9 @@
|
|
|
21
60
|
.playpilot-styled-scrollbar {
|
|
22
61
|
scrollbar-color: var(--playpilot-content-light) var(--playpilot-lighter);
|
|
23
62
|
scrollbar-width: thin;
|
|
63
|
+
}
|
|
24
64
|
|
|
65
|
+
.playpilot-styled-scrollbed {
|
|
25
66
|
&::-webkit-scrollbar {
|
|
26
67
|
width: margin(0.75);
|
|
27
68
|
}
|
|
@@ -39,16 +39,4 @@ export type ConfigResponse = {
|
|
|
39
39
|
in_text_disclaimer_text?: string
|
|
40
40
|
in_text_disclaimer_selector?: string
|
|
41
41
|
in_text_disclaimer_insert_position?: InsertPosition
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* These options are all relevant for the Explore component, which can be inserted as a widget on any page or as a modal.
|
|
45
|
-
* `explore_navigation_selector` is used to select the navigation element that should be copied and inserted _after_.
|
|
46
|
-
* `explore_navigation_label` will end up being the label of the navigation item, defaults to `Streaming Guide`.
|
|
47
|
-
* `explore_navigation_path` is the path that the navigation item will lead to, this will be set up by the third party.
|
|
48
|
-
* `explore_navigation_insert_position` is used to determine if the navigation item should be inserted before or after the given selector.
|
|
49
|
-
*/
|
|
50
|
-
explore_navigation_selector?: string
|
|
51
|
-
explore_navigation_label?: string
|
|
52
|
-
explore_navigation_path?: string
|
|
53
|
-
explore_navigation_insert_position?: InsertPosition
|
|
54
42
|
}
|
package/src/lib/types/title.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { ParticipantData } from './participant'
|
|
2
2
|
import type { PlaylinkData } from './playlink'
|
|
3
3
|
|
|
4
|
-
export type ContentType = 'movie' | 'series'
|
|
5
|
-
|
|
6
4
|
export type TitleData = {
|
|
7
5
|
sid: string
|
|
8
6
|
slug: string
|
|
@@ -11,7 +9,7 @@ export type TitleData = {
|
|
|
11
9
|
genres: string[]
|
|
12
10
|
year: number
|
|
13
11
|
imdb_score: number
|
|
14
|
-
type:
|
|
12
|
+
type: 'movie' | 'series'
|
|
15
13
|
providers: PlaylinkData[]
|
|
16
14
|
description: string | null
|
|
17
15
|
small_poster: string
|
|
@@ -19,7 +17,6 @@ export type TitleData = {
|
|
|
19
17
|
standing_poster: string
|
|
20
18
|
title: string
|
|
21
19
|
original_title: string
|
|
22
|
-
embeddable_url: string | null
|
|
23
20
|
length?: number
|
|
24
21
|
blurb?: string
|
|
25
22
|
participants?: ParticipantData[]
|
package/src/routes/+page.svelte
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
9
9
|
import { fetchAds } from '$lib/api/ads'
|
|
10
10
|
import { fetchConfig } from '$lib/api/config'
|
|
11
|
-
import { insertExplore, insertExploreIntoNavigation } from '$lib/explore'
|
|
12
11
|
import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/api/auth'
|
|
12
|
+
import { trackSplitTestView } from '$lib/splitTest'
|
|
13
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
13
14
|
import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
|
|
14
15
|
import Editor from './components/Editorial/Editor.svelte'
|
|
15
16
|
import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
|
|
@@ -43,8 +44,6 @@
|
|
|
43
44
|
if (isEditorialMode && !loading) rerender()
|
|
44
45
|
})
|
|
45
46
|
|
|
46
|
-
insertExplore()
|
|
47
|
-
|
|
48
47
|
onDestroy(clearLinkInjections)
|
|
49
48
|
|
|
50
49
|
// This function is called when a user has properly consented via tcfapi or if no consent is required.
|
|
@@ -58,7 +57,11 @@
|
|
|
58
57
|
fireQueuedTrackingEvents()
|
|
59
58
|
track(TrackingEvent.ArticlePageView)
|
|
60
59
|
|
|
61
|
-
if (aiInjections.length
|
|
60
|
+
if (!aiInjections.length && !manualInjections.length) return
|
|
61
|
+
|
|
62
|
+
window.PlayPilotLinkInjections.ads = await fetchAds()
|
|
63
|
+
|
|
64
|
+
trackSplitTestView(SplitTest.InTextEngagement)
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
async function initialize(): Promise<void> {
|
|
@@ -77,7 +80,6 @@
|
|
|
77
80
|
if (isUrlExcluded) return
|
|
78
81
|
|
|
79
82
|
if (config?.custom_style) insertCustomStyle(config.custom_style || '')
|
|
80
|
-
if (config?.explore_navigation_selector) insertExploreIntoNavigation()
|
|
81
83
|
|
|
82
84
|
setElements(config?.html_selector || '', config?.exclude_elements_selector || '')
|
|
83
85
|
} catch(error) {
|
|
@@ -230,7 +232,7 @@
|
|
|
230
232
|
</svelte:boundary>
|
|
231
233
|
{/if}
|
|
232
234
|
|
|
233
|
-
<Debugger />
|
|
235
|
+
<Debugger onrerender={rerender} />
|
|
234
236
|
</div>
|
|
235
237
|
|
|
236
238
|
{#if response?.pixels?.length}
|
|
@@ -246,8 +248,7 @@
|
|
|
246
248
|
<style lang="scss">
|
|
247
249
|
@import url('$lib/scss/global.scss');
|
|
248
250
|
|
|
249
|
-
.playpilot-link-injections
|
|
250
|
-
:global([data-playpilot-explore]) {
|
|
251
|
+
.playpilot-link-injections {
|
|
251
252
|
:global(*) {
|
|
252
253
|
box-sizing: border-box;
|
|
253
254
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
5
5
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
6
6
|
import { imageFromUUID } from '$lib/image'
|
|
7
|
-
import { trackSplitTestView } from '$lib/splitTest'
|
|
7
|
+
import { isInSplitTestVariant, trackSplitTestView } from '$lib/splitTest'
|
|
8
8
|
import { track } from '$lib/tracking'
|
|
9
9
|
import type { Campaign } from '$lib/types/campaign'
|
|
10
10
|
import Disclaimer from './Disclaimer.svelte'
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const { format, header, header_logo: logo, image_uuid: backgroundImageUUID } = $derived(content)
|
|
23
23
|
const { header: buttonLabel, url: href } = $derived(cta)
|
|
24
24
|
|
|
25
|
+
const inline = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
25
26
|
const simple = $derived(format === 'large')
|
|
26
27
|
|
|
27
28
|
const backgroundImage = $derived(imageFromUUID(backgroundImageUUID, ImageDimensions.TopScrollBackground))
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
target="_blank"
|
|
43
44
|
class="top-scroll"
|
|
44
45
|
class:simple
|
|
46
|
+
class:inline
|
|
45
47
|
tabindex="-1"
|
|
46
48
|
rel="sponsored"
|
|
47
49
|
style:--width="{clientWidth}px">
|
|
@@ -79,13 +81,17 @@
|
|
|
79
81
|
position: relative;
|
|
80
82
|
display: block;
|
|
81
83
|
width: 100%;
|
|
82
|
-
border-radius: $border-radius-size
|
|
84
|
+
border-radius: $border-radius-size;
|
|
83
85
|
background: black;
|
|
84
86
|
color: theme(top-scroll-text-color, white);
|
|
85
87
|
font-family: theme(top-scroll-font-family, font-family);
|
|
86
88
|
font-size: theme(top-scroll-font-size, font-size-base);
|
|
87
89
|
text-decoration: none;
|
|
88
90
|
line-height: 1.35;
|
|
91
|
+
|
|
92
|
+
&.inline {
|
|
93
|
+
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
94
|
+
}
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
.content {
|
|
@@ -146,7 +152,7 @@
|
|
|
146
152
|
right: 0;
|
|
147
153
|
bottom: 0;
|
|
148
154
|
left: 0;
|
|
149
|
-
border-radius: $border-radius-size
|
|
155
|
+
border-radius: $border-radius-size;
|
|
150
156
|
background-image: var(--background);
|
|
151
157
|
background-position: center;
|
|
152
158
|
background-size: cover;
|
|
@@ -155,6 +161,10 @@
|
|
|
155
161
|
.top-scroll:hover & {
|
|
156
162
|
filter: brightness(1.15);
|
|
157
163
|
}
|
|
164
|
+
|
|
165
|
+
.inline & {
|
|
166
|
+
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
167
|
+
}
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
.content-image {
|
|
@@ -162,10 +172,14 @@
|
|
|
162
172
|
max-width: 100%;
|
|
163
173
|
height: auto;
|
|
164
174
|
background: black;
|
|
165
|
-
border-radius: $border-radius-size
|
|
175
|
+
border-radius: $border-radius-size;
|
|
166
176
|
|
|
167
177
|
.top-scroll:hover & {
|
|
168
178
|
filter: brightness(1.15);
|
|
169
179
|
}
|
|
180
|
+
|
|
181
|
+
.inline & {
|
|
182
|
+
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
183
|
+
}
|
|
170
184
|
}
|
|
171
185
|
</style>
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import { getFullUrlPath } from '$lib/url'
|
|
4
4
|
import { onDestroy } from 'svelte'
|
|
5
5
|
|
|
6
|
+
interface Props {
|
|
7
|
+
onrerender: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { onrerender }: Props = $props()
|
|
11
|
+
|
|
6
12
|
const secrets = ['tpidebug', 'debugtpi']
|
|
7
13
|
const lastInputs: string[] = []
|
|
8
14
|
const isUsingBetaScript = !!document.querySelector('script[src*="scripts.playpilot.com/link-injection@next"]')
|
|
@@ -151,6 +157,8 @@
|
|
|
151
157
|
|
|
152
158
|
<hr />
|
|
153
159
|
|
|
160
|
+
<button onclick={onrerender}>Re-inject</button>
|
|
161
|
+
|
|
154
162
|
{#if isUsingBetaScript}
|
|
155
163
|
<small>You are using the beta version of TPI</small>
|
|
156
164
|
{:else}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
<
|
|
2
|
-
interface Props {
|
|
3
|
-
size?: number
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const { size = 16 }: Props = $props()
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
10
2
|
<path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
11
3
|
</svg>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { titleUrl } from '$lib/routes'
|
|
2
3
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
3
4
|
import { t } from '$lib/localization'
|
|
4
5
|
import { mergePlaylinks } from '$lib/playlink'
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
}
|
|
29
30
|
</script>
|
|
30
31
|
|
|
31
|
-
<
|
|
32
|
+
<a class="title" href={titleUrl(title)} {onclick} data-testid="title">
|
|
32
33
|
<div class="poster">
|
|
33
34
|
<TitlePoster {title} width={30} height={43} lazy />
|
|
34
35
|
</div>
|
|
@@ -80,7 +81,7 @@
|
|
|
80
81
|
<div class="action">
|
|
81
82
|
<IconArrow />
|
|
82
83
|
</div>
|
|
83
|
-
</
|
|
84
|
+
</a>
|
|
84
85
|
|
|
85
86
|
<style lang="scss">
|
|
86
87
|
.title {
|
|
@@ -89,18 +90,18 @@
|
|
|
89
90
|
align-items: center;
|
|
90
91
|
width: 100%;
|
|
91
92
|
background: theme(list-item-background, lighter);
|
|
92
|
-
padding:
|
|
93
|
+
padding: margin(0.5);
|
|
93
94
|
border: 0;
|
|
94
95
|
border-radius: theme(list-item-border-radius, border-radius);
|
|
95
96
|
text-decoration: none;
|
|
96
97
|
font-style: normal !important;
|
|
97
|
-
font-family: theme(font-family);
|
|
98
|
-
cursor: pointer;
|
|
99
98
|
|
|
100
99
|
&:hover {
|
|
101
100
|
filter: brightness(theme(list-item-hover-filter-brightness, hover-filter-brightness));
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
&:last-child {
|
|
104
|
+
border-bottom: 0;
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import SwipeHandle from './SwipeHandle.svelte'
|
|
7
7
|
import { onMount, setContext, type Snippet } from 'svelte'
|
|
8
8
|
import { prefersReducedMotion } from 'svelte/motion'
|
|
9
|
+
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
10
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
9
11
|
import { mobileBreakpoint } from '$lib/constants'
|
|
10
12
|
import { destroyAllModals, getPreviousModal, goBackToPreviousModal } from '$lib/modal'
|
|
11
13
|
import { focustrap } from '$lib/actions/focustrap'
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
onclose = () => null,
|
|
33
35
|
}: Props = $props()
|
|
34
36
|
|
|
37
|
+
const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
35
38
|
const historyHash = '#playpilot'
|
|
36
39
|
|
|
37
40
|
let windowWidth = $state(0)
|
|
@@ -92,15 +95,15 @@
|
|
|
92
95
|
onhashchange={close}
|
|
93
96
|
bind:innerWidth={windowWidth} />
|
|
94
97
|
|
|
95
|
-
<div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble} class:has-prepend={!!prepend}>
|
|
98
|
+
<div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
|
|
96
99
|
{#if prepend}
|
|
97
|
-
<div class="prepend" transition:scaleOrFly|global={{ y:
|
|
100
|
+
<div class="prepend" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
|
|
98
101
|
{@render prepend()}
|
|
99
102
|
</div>
|
|
100
103
|
{/if}
|
|
101
104
|
|
|
102
105
|
{#if bubble}
|
|
103
|
-
<div class="bubble" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
|
|
106
|
+
<div class="bubble" class:inline={inlineBubble} transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
|
|
104
107
|
{@render bubble()}
|
|
105
108
|
</div>
|
|
106
109
|
{/if}
|
|
@@ -282,13 +285,28 @@
|
|
|
282
285
|
.bubble {
|
|
283
286
|
z-index: 1;
|
|
284
287
|
position: relative;
|
|
285
|
-
width: 100
|
|
288
|
+
width: calc(100% - margin(1));
|
|
286
289
|
max-width: $max-width;
|
|
287
|
-
margin:
|
|
290
|
+
margin: margin(0.5);
|
|
288
291
|
|
|
289
292
|
@include desktop() {
|
|
290
293
|
width: 100%;
|
|
291
|
-
margin
|
|
294
|
+
margin: 0 0 margin(0.5);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
&.inline {
|
|
298
|
+
width: 100%;
|
|
299
|
+
margin: auto 0 0;
|
|
300
|
+
|
|
301
|
+
@include desktop() {
|
|
302
|
+
margin-top: 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.prepend + & {
|
|
307
|
+
&:not(.inline) {
|
|
308
|
+
margin-top: 0;
|
|
309
|
+
}
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
312
|
</style>
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import IconShare from './Icons/IconShare.svelte'
|
|
10
10
|
import IconLink from './Icons/IconLink.svelte'
|
|
11
11
|
import IconEmail from './Icons/IconEmail.svelte'
|
|
12
|
-
import Button from './Button.svelte'
|
|
13
12
|
import { onMount } from 'svelte'
|
|
14
13
|
|
|
15
14
|
interface Props {
|
|
@@ -56,9 +55,9 @@
|
|
|
56
55
|
<svelte:window onclick={() => showContextMenu = false} />
|
|
57
56
|
|
|
58
57
|
<div class="share">
|
|
59
|
-
<
|
|
60
|
-
<IconShare />
|
|
61
|
-
</
|
|
58
|
+
<button class="button" onclick={toggle} aria-label={t('Share')}>
|
|
59
|
+
<IconShare />
|
|
60
|
+
</button>
|
|
62
61
|
|
|
63
62
|
{#if showContextMenu}
|
|
64
63
|
<div class="context-menu" transition:scale={{ duration: 50, start: 0.85 }}>
|
|
@@ -80,11 +79,30 @@
|
|
|
80
79
|
position: relative;
|
|
81
80
|
}
|
|
82
81
|
|
|
82
|
+
.button {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
appearance: none;
|
|
88
|
+
border: 0;
|
|
89
|
+
border-radius: margin(3);
|
|
90
|
+
aspect-ratio: 1 / 1;
|
|
91
|
+
background: transparent;
|
|
92
|
+
color: theme(detail-text-color-alt, text-color-alt);
|
|
93
|
+
|
|
94
|
+
&:hover {
|
|
95
|
+
color: theme(detail-text-color, text-color);
|
|
96
|
+
background: theme(share-button-hover-background, content);
|
|
97
|
+
box-shadow: 0 0 0 2px theme(share-button-hover-background, content);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
.context-menu {
|
|
84
102
|
z-index: 10;
|
|
85
103
|
position: absolute;
|
|
86
104
|
bottom: calc(100% + margin(0.5));
|
|
87
|
-
|
|
105
|
+
right: 0;
|
|
88
106
|
max-width: margin(15);
|
|
89
107
|
border-radius: $border-radius;
|
|
90
108
|
background: theme(detail-background, lighter);
|