@playpilot/tpi 5.14.0 → 5.16.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.
- package/dist/link-injections.js +10 -9
- package/package.json +2 -1
- package/src/lib/ads.ts +5 -0
- package/src/lib/consent.ts +13 -0
- package/src/lib/enums/SplitTest.ts +4 -0
- package/src/lib/fakeData.ts +70 -0
- package/src/lib/linkInjection.ts +11 -29
- package/src/lib/modal.ts +97 -0
- package/src/lib/playlink.ts +4 -1
- package/src/lib/splitTest.ts +5 -0
- package/src/lib/tracking.ts +3 -0
- package/src/lib/types/consent.d.ts +9 -0
- package/src/lib/types/participant.d.ts +14 -0
- package/src/lib/types/script.d.ts +3 -0
- package/src/lib/types/title.d.ts +2 -0
- package/src/main.ts +21 -1
- package/src/routes/+layout.svelte +20 -3
- package/src/routes/+page.svelte +14 -10
- package/src/routes/components/Consent.svelte +72 -0
- package/src/routes/components/Icons/IconArrow.svelte +22 -0
- package/src/routes/components/Icons/IconClose.svelte +1 -1
- package/src/routes/components/Icons/IconIMDb.svelte +9 -1
- package/src/routes/components/ListTitle.svelte +204 -0
- package/src/routes/components/Modal.svelte +63 -13
- package/src/routes/components/Participant.svelte +92 -0
- package/src/routes/components/ParticipantModal.svelte +31 -0
- package/src/routes/components/Playlink.svelte +16 -4
- package/src/routes/components/PlaylinkIcon.svelte +41 -0
- package/src/routes/components/PlaylinkLabel.svelte +37 -0
- package/src/routes/components/Playlinks.svelte +1 -3
- package/src/routes/components/Rails/ParticipantsRail.svelte +56 -0
- package/src/routes/components/Rails/Rail.svelte +91 -0
- package/src/routes/components/Rails/SimilarRail.svelte +16 -0
- package/src/routes/components/Rails/TitlesRail.svelte +95 -0
- package/src/routes/components/Tabs.svelte +47 -0
- package/src/routes/components/Title.svelte +19 -16
- package/src/routes/components/TitleModal.svelte +3 -3
- package/src/routes/components/TitlePoster.svelte +30 -0
- package/src/routes/components/TrackingPixels.svelte +7 -3
- package/src/tests/lib/ads.test.js +24 -1
- package/src/tests/lib/consent.test.js +50 -0
- package/src/tests/lib/linkInjection.test.js +10 -22
- package/src/tests/lib/modal.test.js +148 -0
- package/src/tests/lib/playlink.test.js +25 -10
- package/src/tests/lib/splitTest.test.js +30 -6
- package/src/tests/lib/tracking.test.js +18 -3
- package/src/tests/routes/components/Consent.test.js +69 -0
- package/src/tests/routes/components/ListTitle.test.js +84 -0
- package/src/tests/routes/components/Modal.test.js +51 -19
- package/src/tests/routes/components/Playlink.test.js +22 -1
- package/src/tests/routes/components/PlaylinkIcon.test.js +27 -0
- package/src/tests/routes/components/PlaylinkLabel.test.js +19 -0
- package/src/tests/routes/components/Rails/ParticipantsRail.test.js +41 -0
- package/src/tests/routes/components/Rails/TitleRail.test.js +38 -0
- package/src/tests/routes/components/TitleModal.test.js +6 -0
- package/src/tests/routes/components/TitlePopover.test.js +6 -0
- package/src/tests/routes/components/TitlePoster.test.js +20 -0
- package/src/tests/routes/components/TrackingPixels.test.js +15 -1
- package/src/tests/setup.js +14 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { openModal } from '$lib/modal'
|
|
3
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
4
|
+
import Rail from './Rail.svelte'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
participants: ParticipantData[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { participants }: Props = $props()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<Rail heading="Cast">
|
|
14
|
+
{#each participants.slice(0, 15) as participant}
|
|
15
|
+
<button class="participant" onclick={event => openModal({ event, type: 'participant', data: participant })}>
|
|
16
|
+
<span class="truncate">{participant.name}</span>
|
|
17
|
+
|
|
18
|
+
<div class="character truncate">{participant.character}</div>
|
|
19
|
+
</button>
|
|
20
|
+
{/each}
|
|
21
|
+
</Rail>
|
|
22
|
+
|
|
23
|
+
<style lang="scss">
|
|
24
|
+
.participant {
|
|
25
|
+
display: block;
|
|
26
|
+
flex: 0 0 10rem;
|
|
27
|
+
width: 10rem;
|
|
28
|
+
padding: margin(0.5);
|
|
29
|
+
border: 0;
|
|
30
|
+
border-radius: var(--playpilot-cast-border-radius, var(--playpilot-playlink-border-radius, margin(0.5)));
|
|
31
|
+
background: var(--playpilot-cast-background, var(--playpilot-playlink-background, var(--playpilot-lighter)));
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
font-family: inherit;
|
|
34
|
+
text-align: left;
|
|
35
|
+
color: inherit;
|
|
36
|
+
font-size: var(--playpilot-cast-font-size, var(--playpilot-playlinks-font-size, margin(0.75)));
|
|
37
|
+
white-space: nowrap;
|
|
38
|
+
|
|
39
|
+
&:hover,
|
|
40
|
+
&:active {
|
|
41
|
+
filter: var(--playpilot-cast-hover-filter, var(--playpilot-playlink-hover-filter, brightness(1.1)));
|
|
42
|
+
text-decoration: none !important;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.character {
|
|
47
|
+
color: var(--playpilot-cast-character-text-color, var(--playpilot-text-color-alt));
|
|
48
|
+
font-style: italic;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.truncate {
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import TinySlider from 'svelte-tiny-slider'
|
|
4
|
+
import IconArrow from '../Icons/IconArrow.svelte'
|
|
5
|
+
import { heading as _heading } from '$lib/actions/heading'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
heading?: string
|
|
9
|
+
children: Snippet
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { heading = '', children }: Props = $props()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if heading}
|
|
16
|
+
<div class="heading" use:_heading={2}>{heading}</div>
|
|
17
|
+
{/if}
|
|
18
|
+
|
|
19
|
+
<div class="rail">
|
|
20
|
+
<TinySlider allowWheel>
|
|
21
|
+
{@render children()}
|
|
22
|
+
|
|
23
|
+
{#snippet controls({ setIndex, currentIndex, reachedEnd })}
|
|
24
|
+
{#if currentIndex > 0}
|
|
25
|
+
<button class="arrow left" onclick={() => setIndex(currentIndex - 1)}><IconArrow direction="left" /></button>
|
|
26
|
+
{/if}
|
|
27
|
+
|
|
28
|
+
{#if !reachedEnd}
|
|
29
|
+
<button class="arrow right" onclick={() => setIndex(currentIndex + 1)}><IconArrow /></button>
|
|
30
|
+
{/if}
|
|
31
|
+
{/snippet}
|
|
32
|
+
</TinySlider>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<style lang="scss">
|
|
36
|
+
.heading {
|
|
37
|
+
margin: margin(1) 0 margin(0.5);
|
|
38
|
+
font-size: var(--playpilot-rail-title-font-size, 18px);
|
|
39
|
+
line-height: normal;
|
|
40
|
+
font-weight: inherit;
|
|
41
|
+
color: var(--playpilot-rail-title-text-color, var(--playpilot-text-color));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.rail {
|
|
45
|
+
--gap: #{margin(0.5)};
|
|
46
|
+
position: relative;
|
|
47
|
+
width: calc(100% + margin(2));
|
|
48
|
+
margin: 0 margin(-1);
|
|
49
|
+
|
|
50
|
+
:global(.slider) {
|
|
51
|
+
padding: 0 margin(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:global(.slider-content > :last-child) {
|
|
55
|
+
margin-right: margin(2);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.arrow {
|
|
60
|
+
display: none;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
position: absolute;
|
|
64
|
+
left: margin(2);
|
|
65
|
+
top: 50%;
|
|
66
|
+
width: margin(2);
|
|
67
|
+
height: margin(2);
|
|
68
|
+
padding: 0;
|
|
69
|
+
margin: 0;
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
transform: translateX(-50%) translateY(-50%);
|
|
73
|
+
background: var(--playpilot-rails-arrow-background, var(--playpilot-content-light));
|
|
74
|
+
z-index: 2;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
color: var(--playpilot-rails-arrow-color, var(--playpilot-detail-text-color, white));
|
|
77
|
+
|
|
78
|
+
@media (hover: hover) {
|
|
79
|
+
display: flex;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
filter: brightness(1.2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&.right {
|
|
87
|
+
left: auto;
|
|
88
|
+
right: 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { TitleData } from '$lib/types/title'
|
|
3
|
+
import TitlesRail from './TitlesRail.svelte'
|
|
4
|
+
|
|
5
|
+
const titles = fetchTitles()
|
|
6
|
+
|
|
7
|
+
async function fetchTitles(): Promise<TitleData[]> {
|
|
8
|
+
// This is just a fake loading state for now
|
|
9
|
+
await new Promise(res => setTimeout(res, 500))
|
|
10
|
+
|
|
11
|
+
// Imagine this being a fetch request that returns titles instead.
|
|
12
|
+
return (window.PlayPilotLinkInjections?.evaluated_link_injections?.map(i => i.title_details) || []) as TitleData[]
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<TitlesRail {titles} heading="Similar movies & shows" />
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import TitlePoster from '../TitlePoster.svelte'
|
|
3
|
+
import Rail from './Rail.svelte'
|
|
4
|
+
import { playPilotBaseUrl } from '$lib/constants'
|
|
5
|
+
import type { TitleData } from '$lib/types/title'
|
|
6
|
+
import { openModal } from '$lib/modal'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
titles: Promise<TitleData[]> | TitleData[]
|
|
10
|
+
heading?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { titles, heading = '' }: Props = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="titles">
|
|
17
|
+
<Rail {heading}>
|
|
18
|
+
{#await titles}
|
|
19
|
+
{#each { length: 8 }}
|
|
20
|
+
<div class="title" data-testid="skeleton">
|
|
21
|
+
<div class="poster"></div>
|
|
22
|
+
|
|
23
|
+
<div class="heading">
|
|
24
|
+
<span class="skeleton"> </span>
|
|
25
|
+
<span class="skeleton"> </span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{/each}
|
|
29
|
+
{:then titles}
|
|
30
|
+
{#each titles as title}
|
|
31
|
+
{@const href = `${playPilotBaseUrl}/${title.type}/${title.slug}`}
|
|
32
|
+
|
|
33
|
+
<div class="title" data-testid="title">
|
|
34
|
+
<a class="poster" {href} onclick={(event) => openModal({ event, data: title })}>
|
|
35
|
+
<TitlePoster {title} width={96} height={144} />
|
|
36
|
+
</a>
|
|
37
|
+
|
|
38
|
+
<a {href} class="heading" onclick={(event) => openModal({ event, data: title })}>
|
|
39
|
+
{title.title}
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
{/each}
|
|
43
|
+
{/await}
|
|
44
|
+
</Rail>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<style lang="scss">
|
|
48
|
+
$width: margin(6);
|
|
49
|
+
|
|
50
|
+
.title {
|
|
51
|
+
width: $width;
|
|
52
|
+
|
|
53
|
+
&:hover,
|
|
54
|
+
&:active {
|
|
55
|
+
filter: brightness(1.1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.poster {
|
|
60
|
+
display: block;
|
|
61
|
+
border-radius: var(--playpilot-rail-border-radius, var(--playpilot-playlink-border-radius, margin(0.5)));
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
width: 100%;
|
|
64
|
+
aspect-ratio: 2 / 3;
|
|
65
|
+
background: var(--playpilot-detail-background-light, var(--playpilot-lighter));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.heading {
|
|
69
|
+
display: -webkit-box;
|
|
70
|
+
padding-top: margin(0.5);
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
color: var(--playpilot-detail-text-color, var(--playpilot-text-color-alt)) !important;
|
|
74
|
+
font-size: var(--playpilot-rail-font-size, var(--playpilot-detail-font-size-small, 12px));
|
|
75
|
+
font-style: normal !important;
|
|
76
|
+
line-height: 1.2;
|
|
77
|
+
line-clamp: 2;
|
|
78
|
+
-webkit-line-clamp: 2;
|
|
79
|
+
-webkit-box-orient: vertical;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.skeleton {
|
|
84
|
+
display: block;
|
|
85
|
+
height: 1em;
|
|
86
|
+
width: 100%;
|
|
87
|
+
background: var(--playpilot-detail-background-light, var(--playpilot-lighter));
|
|
88
|
+
border-radius: margin(2);
|
|
89
|
+
|
|
90
|
+
&:last-child {
|
|
91
|
+
margin-top: margin(0.3);
|
|
92
|
+
width: 50%;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Option {
|
|
3
|
+
label: string
|
|
4
|
+
value: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
options: Option[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { options }: Props = $props()
|
|
12
|
+
|
|
13
|
+
let active = $state(options[0]?.value)
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="tabs">
|
|
17
|
+
{#each options as { label, value }}
|
|
18
|
+
<button class="tab" class:active={value === active} onclick={() => active = value}>{label}</button>
|
|
19
|
+
{/each}
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<style lang="scss">
|
|
23
|
+
.tabs {
|
|
24
|
+
display: flex;
|
|
25
|
+
gap: margin(0.5);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.tab {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
appearance: none;
|
|
31
|
+
background: transparent;
|
|
32
|
+
padding: margin(0.25) margin(0.5);
|
|
33
|
+
border: 1px solid currentColor;
|
|
34
|
+
border-radius: var(--playpilot-tabs-border-radius, margin(0.25));
|
|
35
|
+
transition: opacity 100ms;
|
|
36
|
+
font-family: inherit;
|
|
37
|
+
font-weight: var(--playpilot-tabs-font-weight, 500);
|
|
38
|
+
color: var(--playpilot-tabs-text-color, var(--playpilot-detail-text-color, var(--playpilot-text-color-alt)));
|
|
39
|
+
font-size: inherit;
|
|
40
|
+
|
|
41
|
+
&.active,
|
|
42
|
+
&:hover {
|
|
43
|
+
background: var(--playpilot-tabs-background-active, var(--playpilot-content));
|
|
44
|
+
color: var(--playpilot-tabs-text-color-active, var(--playpilot-detail-text-color, var(--playpilot-text-color)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
import Playlinks from './Playlinks.svelte'
|
|
4
4
|
import Description from './Description.svelte'
|
|
5
5
|
import IconIMDb from './Icons/IconIMDb.svelte'
|
|
6
|
+
import ParticipantsRail from './Rails/ParticipantsRail.svelte'
|
|
7
|
+
import SimilarRail from './Rails/SimilarRail.svelte'
|
|
8
|
+
import TitlePoster from './TitlePoster.svelte'
|
|
6
9
|
import { t } from '$lib/localization'
|
|
7
10
|
import type { TitleData } from '$lib/types/title'
|
|
8
11
|
import { heading } from '$lib/actions/heading'
|
|
9
12
|
import { removeImageUrlPrefix } from '$lib/image'
|
|
13
|
+
import { participants } from '$lib/fakeData'
|
|
10
14
|
|
|
11
15
|
interface Props {
|
|
12
16
|
title: TitleData
|
|
@@ -22,13 +26,8 @@
|
|
|
22
26
|
|
|
23
27
|
<div class="content" class:small data-playpilot-link-injections-title>
|
|
24
28
|
<div class="header">
|
|
25
|
-
<div class="
|
|
26
|
-
<
|
|
27
|
-
class="poster"
|
|
28
|
-
class:loaded={posterLoaded}
|
|
29
|
-
src={removeImageUrlPrefix(title.standing_poster)}
|
|
30
|
-
alt="Movie poster for '{title.title}'"
|
|
31
|
-
onload={() => posterLoaded = true} />
|
|
29
|
+
<div class="poster" class:loaded={posterLoaded}>
|
|
30
|
+
<TitlePoster {title} onload={() => posterLoaded = true} />
|
|
32
31
|
</div>
|
|
33
32
|
|
|
34
33
|
<div class="heading" use:heading={2} class:truncate={small} id="title">{title.title}</div>
|
|
@@ -56,6 +55,15 @@
|
|
|
56
55
|
{#if !small}
|
|
57
56
|
<Description text={title.description} blurb={title.blurb} />
|
|
58
57
|
{/if}
|
|
58
|
+
|
|
59
|
+
<!-- Temporarily not available on production as there is not yet an API endpoint for either -->
|
|
60
|
+
{#if process.env.NODE_ENV !== 'production'}
|
|
61
|
+
{#if true || title.participants?.length}
|
|
62
|
+
<ParticipantsRail participants={participants} />
|
|
63
|
+
{/if}
|
|
64
|
+
|
|
65
|
+
<SimilarRail />
|
|
66
|
+
{/if}
|
|
59
67
|
</div>
|
|
60
68
|
</div>
|
|
61
69
|
|
|
@@ -103,14 +111,14 @@
|
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
&.small {
|
|
106
|
-
font-size: var(--playpilot-detail-font-size, 12px);
|
|
114
|
+
font-size: var(--playpilot-detail-font-size-small, 12px);
|
|
107
115
|
line-height: 1.45;
|
|
108
|
-
padding-bottom: margin(
|
|
116
|
+
padding-bottom: margin(1);
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
.header {
|
|
113
|
-
padding: margin(
|
|
121
|
+
padding: margin(3) 0 margin(1);
|
|
114
122
|
background: transparent;
|
|
115
123
|
|
|
116
124
|
.small & {
|
|
@@ -118,12 +126,6 @@
|
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
.top {
|
|
122
|
-
display: flex;
|
|
123
|
-
justify-content: space-between;
|
|
124
|
-
align-items: flex-end;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
129
|
.poster {
|
|
128
130
|
display: block;
|
|
129
131
|
width: margin(4.5);
|
|
@@ -132,6 +134,7 @@
|
|
|
132
134
|
border-radius: var(--playpilot-detail-image-border-radius, margin(0.5));
|
|
133
135
|
background: var(--playpilot-detail-image-background, var(--playpilot-content));
|
|
134
136
|
box-shadow: var(--playpilot-detail-image-shadow, var(--playpilot-shadow));
|
|
137
|
+
overflow: hidden;
|
|
135
138
|
opacity: 0;
|
|
136
139
|
|
|
137
140
|
&.loaded {
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
import { getFirstAdOfType } from '$lib/ads'
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
|
-
onclose: () => void,
|
|
14
13
|
title: TitleData
|
|
14
|
+
initialScrollPosition?: number
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const {
|
|
17
|
+
const { title, initialScrollPosition = 0 }: Props = $props()
|
|
18
18
|
|
|
19
19
|
const topScrollAd = getFirstAdOfType('top_scroll')
|
|
20
20
|
const displayAd = getFirstAdOfType('card')
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
{/if}
|
|
50
50
|
{/snippet}
|
|
51
51
|
|
|
52
|
-
<Modal {
|
|
52
|
+
<Modal {onscroll} {initialScrollPosition} prepend={displayAd ? prepend : null} bubble={topScrollAd ? bubble : null} tall>
|
|
53
53
|
<Title {title} />
|
|
54
54
|
</Modal>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { imagePlaceholderDataUrl } from '$lib/constants'
|
|
3
|
+
import { removeImageUrlPrefix } from '$lib/image'
|
|
4
|
+
import type { TitleData } from '$lib/types/title'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title: TitleData
|
|
8
|
+
width?: number
|
|
9
|
+
height?: number
|
|
10
|
+
onload?: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { title, width = 96, height = 144, onload = () => null }: Props = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<img
|
|
17
|
+
src={removeImageUrlPrefix(title.standing_poster)}
|
|
18
|
+
alt="Movie poster for '{title.title}'"
|
|
19
|
+
{width}
|
|
20
|
+
{height}
|
|
21
|
+
onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl}
|
|
22
|
+
{onload} />
|
|
23
|
+
|
|
24
|
+
<style lang="scss">
|
|
25
|
+
img {
|
|
26
|
+
display: block;
|
|
27
|
+
width: 100%;
|
|
28
|
+
height: auto;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { hasConsentedTo } from '$lib/consent'
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
pixels: string[]
|
|
4
6
|
}
|
|
@@ -6,9 +8,11 @@
|
|
|
6
8
|
const { pixels }: Props = $props()
|
|
7
9
|
</script>
|
|
8
10
|
|
|
9
|
-
{#
|
|
10
|
-
|
|
11
|
-
{
|
|
11
|
+
{#if hasConsentedTo('pixels')}
|
|
12
|
+
{#each pixels as src}
|
|
13
|
+
<img {src} alt="" />
|
|
14
|
+
{/each}
|
|
15
|
+
{/if}
|
|
12
16
|
|
|
13
17
|
<style lang="scss">
|
|
14
18
|
img {
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
|
2
2
|
import { fakeFetch } from '../helpers'
|
|
3
3
|
|
|
4
4
|
import { fetchAds, getFirstAdOfType } from '$lib/ads'
|
|
5
|
+
import { hasConsentedTo } from '$lib/consent'
|
|
5
6
|
import { track } from '$lib/tracking'
|
|
6
7
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
7
8
|
|
|
@@ -9,11 +10,17 @@ vi.mock('$lib/tracking', () => ({
|
|
|
9
10
|
track: vi.fn(),
|
|
10
11
|
}))
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
vi.mock('$lib/consent', () => ({
|
|
14
|
+
hasConsentedTo: vi.fn(() => true),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
describe('$lib/ads', () => {
|
|
13
18
|
afterEach(() => {
|
|
14
19
|
vi.resetAllMocks()
|
|
15
20
|
// @ts-ignore
|
|
16
21
|
window.PlayPilotLinkInjections = null
|
|
22
|
+
|
|
23
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => true)
|
|
17
24
|
})
|
|
18
25
|
|
|
19
26
|
describe('fetchAds', () => {
|
|
@@ -42,6 +49,13 @@ describe('$lib/api', () => {
|
|
|
42
49
|
expect(global.fetch).not.toHaveBeenCalled()
|
|
43
50
|
})
|
|
44
51
|
|
|
52
|
+
it('Should not call fetch if user did not consent to ads', async () => {
|
|
53
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
54
|
+
|
|
55
|
+
await fetchAds()
|
|
56
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
57
|
+
})
|
|
58
|
+
|
|
45
59
|
it('Should fire track event when ads failed to fetch', async () => {
|
|
46
60
|
fakeFetch({ ok: false, status: 505 })
|
|
47
61
|
|
|
@@ -64,5 +78,14 @@ describe('$lib/api', () => {
|
|
|
64
78
|
|
|
65
79
|
expect(getFirstAdOfType('top_scroll')).toBe(null)
|
|
66
80
|
})
|
|
81
|
+
|
|
82
|
+
it('Should not return ads if valid ad was given but user did not consent to ads', async () => {
|
|
83
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
84
|
+
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'top_scroll' }] }
|
|
87
|
+
|
|
88
|
+
expect(getFirstAdOfType('top_scroll')).toBe(null)
|
|
89
|
+
})
|
|
67
90
|
})
|
|
68
91
|
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { setConsent, hasConsentedTo } from '$lib/consent'
|
|
4
|
+
|
|
5
|
+
describe('$lib/consent', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
window.PlayPilotLinkInjections = null
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('setConsent', () => {
|
|
12
|
+
it('Should set consent in window object to given values', async () => {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
window.PlayPilotLinkInjections = {}
|
|
15
|
+
|
|
16
|
+
setConsent({ ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false })
|
|
17
|
+
|
|
18
|
+
expect(window.PlayPilotLinkInjections.consents).toEqual({ ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false })
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('hasConsentedTo', () => {
|
|
23
|
+
it('Should return values for given keys from window object', async () => {
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
window.PlayPilotLinkInjections = {
|
|
26
|
+
consents: { ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(hasConsentedTo('ads')).toBe(true)
|
|
30
|
+
expect(hasConsentedTo('affiliate')).toBe(true)
|
|
31
|
+
expect(hasConsentedTo('pixels')).toBe(true)
|
|
32
|
+
expect(hasConsentedTo('split_tests')).toBe(false)
|
|
33
|
+
expect(hasConsentedTo('tracking')).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('Should return true regardless of values if require_consent is false', async () => {
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
window.PlayPilotLinkInjections = {
|
|
39
|
+
require_consent: false,
|
|
40
|
+
consents: { ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(hasConsentedTo('ads')).toBe(true)
|
|
44
|
+
expect(hasConsentedTo('affiliate')).toBe(true)
|
|
45
|
+
expect(hasConsentedTo('pixels')).toBe(true)
|
|
46
|
+
expect(hasConsentedTo('split_tests')).toBe(true)
|
|
47
|
+
expect(hasConsentedTo('tracking')).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
|
4
4
|
import { injectLinksInDocument, clearLinkInjections, clearLinkInjection, getLinkInjectionElements, insertAfterArticlePlaylinks, getLinkInjectionsParentElement, isAvailableAsManualInjection, filterRemovedAndInactiveInjections, isEquivalentInjection, filterInvalidInTextInjections, filterInvalidAfterArticleInjections, isValidInjection, isValidPlaylinkType, removePlayPilotTitleLinks, trackLinkIntersection } from '$lib/linkInjection'
|
|
5
5
|
import { mount, unmount } from 'svelte'
|
|
6
6
|
import { fakeFetch, generateInjection } from '../helpers'
|
|
7
|
+
import { openModal } from '$lib/modal'
|
|
7
8
|
import { track } from '$lib/tracking'
|
|
8
9
|
|
|
9
10
|
vi.mock('svelte', () => ({
|
|
@@ -11,6 +12,13 @@ vi.mock('svelte', () => ({
|
|
|
11
12
|
unmount: vi.fn(),
|
|
12
13
|
}))
|
|
13
14
|
|
|
15
|
+
vi.mock('$lib/modal', () => ({
|
|
16
|
+
openModal: vi.fn(),
|
|
17
|
+
destroyAllModals: vi.fn(),
|
|
18
|
+
getPreviousModal: vi.fn(),
|
|
19
|
+
goBackToPreviousModal: vi.fn(),
|
|
20
|
+
}))
|
|
21
|
+
|
|
14
22
|
vi.mock('$lib/tracking', () => ({
|
|
15
23
|
track: vi.fn(),
|
|
16
24
|
}))
|
|
@@ -311,7 +319,7 @@ describe('linkInjection.js', () => {
|
|
|
311
319
|
expect(() => injectLinksInDocument(elements, { aiInjections: linkInjections, manualInjections: [] })).not.toThrow()
|
|
312
320
|
})
|
|
313
321
|
|
|
314
|
-
it('Should
|
|
322
|
+
it('Should open modal when link is clicked', async () => {
|
|
315
323
|
document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
|
|
316
324
|
|
|
317
325
|
const elements = Array.from(document.body.querySelectorAll('p'))
|
|
@@ -322,27 +330,7 @@ describe('linkInjection.js', () => {
|
|
|
322
330
|
const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
|
|
323
331
|
await fireEvent.click(link)
|
|
324
332
|
|
|
325
|
-
expect(
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
it('Should not mount modal multiple times if modal is already open', async () => {
|
|
329
|
-
const sentence = 'This is a sentence with an injection.'
|
|
330
|
-
document.body.innerHTML = `<p>${sentence}</p>`
|
|
331
|
-
|
|
332
|
-
const elements = Array.from(document.body.querySelectorAll('p'))
|
|
333
|
-
|
|
334
|
-
const linkInjections = [
|
|
335
|
-
generateInjection(sentence, 'a sentence'),
|
|
336
|
-
generateInjection(sentence, 'an injection'),
|
|
337
|
-
]
|
|
338
|
-
|
|
339
|
-
injectLinksInDocument(elements, { aiInjections: linkInjections, manualInjections: [] })
|
|
340
|
-
|
|
341
|
-
const links = document.querySelectorAll('a')
|
|
342
|
-
await fireEvent.click(links[0])
|
|
343
|
-
await fireEvent.click(links[1])
|
|
344
|
-
|
|
345
|
-
expect(mount).toHaveBeenCalledTimes(1)
|
|
333
|
+
expect(openModal).toHaveBeenCalled()
|
|
346
334
|
})
|
|
347
335
|
|
|
348
336
|
it('Should not fire given onclick function when clicked with modifier keys or not left click', async () => {
|