@playpilot/tpi 6.3.0-beta.5 → 6.3.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 -10
- package/package.json +1 -1
- package/src/lib/injection.ts +0 -1
- package/src/lib/scss/global.scss +0 -36
- package/src/lib/types/global.d.ts +6 -0
- package/src/lib/types/playlink.d.ts +1 -0
- package/src/routes/+page.svelte +3 -5
- package/src/routes/components/Consent.svelte +10 -2
- package/src/routes/components/Debugger.svelte +3 -0
- package/src/routes/components/Explore/Explore.svelte +6 -1
- package/src/routes/components/Explore/Filter/Search.svelte +4 -2
- package/src/routes/components/Modal.svelte +37 -25
- package/src/routes/components/Playlinks/Playlink.svelte +11 -1
- package/src/routes/components/Playlinks/PlaylinkIcon.svelte +7 -1
- package/src/routes/components/SwipeHandle.svelte +17 -8
- package/src/routes/components/TitlePopover.svelte +0 -4
- package/src/routes/components/TrackingPixels.svelte +11 -5
- package/src/tests/routes/components/Consent.test.js +5 -1
- package/src/tests/routes/components/Explore/Explore.test.js +22 -1
- package/src/tests/routes/components/Playlinks/Playlink.test.js +32 -0
- package/src/tests/routes/components/Playlinks/PlaylinkIcon.test.js +12 -0
- package/src/tests/routes/components/TrackingPixels.test.js +19 -1
- package/src/routes/components/PostersScrollReveal.svelte +0 -89
- package/src/tests/routes/components/PostersScrollReveal.test.js +0 -107
package/package.json
CHANGED
package/src/lib/injection.ts
CHANGED
|
@@ -291,7 +291,6 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
291
291
|
injectionElement.dataset.playpilotInjectionKey = injection.key
|
|
292
292
|
|
|
293
293
|
const linkElement = document.createElement('a')
|
|
294
|
-
linkElement.dataset.playpilotPosterUrl = injection.title_details?.standing_poster
|
|
295
294
|
linkElement.innerText = injection.title
|
|
296
295
|
linkElement.href = injection.playpilot_url
|
|
297
296
|
linkElement.target = '_blank'
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -99,39 +99,3 @@ h1, h2, h3, h4, h5 {
|
|
|
99
99
|
::view-transition-new(playpilot-title-content) {
|
|
100
100
|
height: 100%;
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
@keyframes playpilot-poster-fly-in {
|
|
104
|
-
from {
|
|
105
|
-
opacity: 0;
|
|
106
|
-
transform: translateX(-50%) translateY(1.5rem);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
@keyframes playpilot-poster-fly-out {
|
|
111
|
-
to {
|
|
112
|
-
opacity: 0;
|
|
113
|
-
transform: translateX(-50%) translateY(0.5rem);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.playpilot-floating-poster {
|
|
118
|
-
opacity: 0;
|
|
119
|
-
position: absolute;
|
|
120
|
-
bottom: calc(100% + 0.15em);
|
|
121
|
-
left: 50%;
|
|
122
|
-
transform: translateX(-50%);
|
|
123
|
-
width: 40px;
|
|
124
|
-
height: auto;
|
|
125
|
-
border-radius: theme(border-radius-small);
|
|
126
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
127
|
-
transform-origin: bottom center;
|
|
128
|
-
|
|
129
|
-
&.loaded {
|
|
130
|
-
animation: playpilot-poster-fly-in 500ms cubic-bezier(0.25, 1.75, 0.5, 1);
|
|
131
|
-
opacity: 1;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
&.animating-out {
|
|
135
|
-
animation: playpilot-poster-fly-out 150ms forwards;
|
|
136
|
-
}
|
|
137
|
-
}
|
package/src/routes/+page.svelte
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import Consent from './components/Consent.svelte'
|
|
19
19
|
import Debugger from './components/Debugger.svelte'
|
|
20
20
|
import UserJourney from './components/UserJourney.svelte'
|
|
21
|
-
import PostersScrollReveal from './components/PostersScrollReveal.svelte'
|
|
22
21
|
|
|
23
22
|
let parentElement: HTMLElement | null = $state(null)
|
|
24
23
|
let elements: HTMLElement[] = $state([])
|
|
@@ -73,12 +72,12 @@
|
|
|
73
72
|
|
|
74
73
|
window.PlayPilotLinkInjections.config = config
|
|
75
74
|
|
|
76
|
-
isUrlExcluded = !!(config?.exclude_urls_pattern && url.match(new RegExp(config.exclude_urls_pattern)))
|
|
77
|
-
if (isUrlExcluded) return
|
|
78
|
-
|
|
79
75
|
if (config?.custom_style) insertCustomStyle(config.custom_style || '')
|
|
80
76
|
if (config?.explore_navigation_selector) insertExploreIntoNavigation()
|
|
81
77
|
|
|
78
|
+
isUrlExcluded = !!(config?.exclude_urls_pattern && url.match(new RegExp(config.exclude_urls_pattern)))
|
|
79
|
+
if (isUrlExcluded) return
|
|
80
|
+
|
|
82
81
|
setElements(config?.html_selector || '', config?.exclude_elements_selector || '')
|
|
83
82
|
} catch(error) {
|
|
84
83
|
// We return if the config did not get fetched properly, as we can't determine what should and should
|
|
@@ -234,7 +233,6 @@
|
|
|
234
233
|
|
|
235
234
|
{#key linkInjections}
|
|
236
235
|
<UserJourney />
|
|
237
|
-
<PostersScrollReveal />
|
|
238
236
|
{/key}
|
|
239
237
|
|
|
240
238
|
<Consent onchange={afterConsent} />
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// If require_consent has been explicitely turned off we return right away and call `onchange`.
|
|
18
18
|
// We don't need to set consent values as require_consent=false will mean all consent is true.
|
|
19
19
|
if (window.PlayPilotLinkInjections.require_consent === false) {
|
|
20
|
-
|
|
20
|
+
update()
|
|
21
21
|
return
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -66,7 +66,15 @@
|
|
|
66
66
|
affiliate: consent(1) && consent(7),
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
update()
|
|
70
70
|
})
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
function update() {
|
|
74
|
+
window.dispatchEvent(new CustomEvent('updateconsent', {
|
|
75
|
+
bubbles: true,
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
onchange()
|
|
79
|
+
}
|
|
72
80
|
</script>
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
const succesfulInjections = data.evaluated_link_injections?.filter(injection => !injection.failed) || []
|
|
28
28
|
const failedInjections = data.evaluated_link_injections?.filter(injection => injection.failed) || []
|
|
29
29
|
|
|
30
|
+
const visiblePixels = Array.from(document.querySelectorAll<HTMLImageElement>('[data-playpilot-pixel]'))
|
|
31
|
+
|
|
30
32
|
return {
|
|
31
33
|
'Config': [
|
|
32
34
|
{ label: 'Domain', data: data.domain_sid },
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
[`Failed injections (${failedInjections.length})`]: failedInjections.map(injection => ({ label: injection.title, data: `Reason: ${injection.failed_message} | Sentence: ${injection.sentence}` })),
|
|
39
41
|
[`Fetched ads (${data.ads?.length || 0})`]: data.ads?.map(ad => ({ label: ad.campaign_name, data: ad })),
|
|
40
42
|
[`Tracking events (${data.tracked_events?.length || 0})`]: data.tracked_events?.map(event => ({ label: event.event, data: event.payload })),
|
|
43
|
+
[`Visible pixels (${visiblePixels.length})`]: visiblePixels.map((pixel, index) => ({ label: index + 1, data: pixel.src })),
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { heading } from '$lib/actions/heading'
|
|
3
|
+
import { fetchAds } from '$lib/api/ads'
|
|
3
4
|
import { fetchTitles } from '$lib/api/titles'
|
|
4
5
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
5
6
|
import { exploreParentSelector } from '$lib/explore'
|
|
@@ -46,7 +47,11 @@
|
|
|
46
47
|
height = element?.closest<HTMLElement>(exploreParentSelector)?.style.height || null
|
|
47
48
|
})
|
|
48
49
|
|
|
49
|
-
onMount(() =>
|
|
50
|
+
onMount(async () => {
|
|
51
|
+
track(TrackingEvent.ExplorePageView)
|
|
52
|
+
|
|
53
|
+
if (!window.PlayPilotLinkInjections.ads?.length) window.PlayPilotLinkInjections.ads = await fetchAds()
|
|
54
|
+
})
|
|
50
55
|
|
|
51
56
|
async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
|
|
52
57
|
latestRequestId += 1
|
|
@@ -41,12 +41,14 @@
|
|
|
41
41
|
border: theme(explore-search-border, 0);
|
|
42
42
|
border-radius: theme(exlore-search-border-radius, border-radius);
|
|
43
43
|
background: theme(explore-search-background, content);
|
|
44
|
+
transition: none;
|
|
44
45
|
color: theme(text-color-alt);
|
|
45
|
-
font-size: theme(font-size-
|
|
46
|
+
font-size: theme(font-size-large);
|
|
46
47
|
font-family: theme(font-family);
|
|
47
48
|
|
|
48
49
|
&:focus {
|
|
49
|
-
outline: 1px solid white;
|
|
50
|
+
outline: theme(explore-search-focus-outline, 1px solid white);
|
|
51
|
+
outline-offset: 0;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
&::placeholder {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
let windowWidth = $state(0)
|
|
42
42
|
let modalElement: HTMLElement | null = $state(null)
|
|
43
43
|
let dialogElement: HTMLElement | null = $state(null)
|
|
44
|
+
let closeElement: HTMLElement | null = $state(null)
|
|
44
45
|
let dialogOffset: number = $state(0)
|
|
45
46
|
let hasPreviousModal = $state(false)
|
|
46
|
-
let isTransitioningIn = $state(false)
|
|
47
47
|
|
|
48
48
|
const isMobile = $derived(windowWidth < mobileBreakpoint)
|
|
49
49
|
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
scrollableElement?.scrollTo(0, initialScrollPosition)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function scaleOrFly(node: Element
|
|
82
|
+
function scaleOrFly(node: Element): TransitionConfig {
|
|
83
83
|
if (prefersReducedMotion.current) return fade(node, { duration: 0 })
|
|
84
84
|
|
|
85
|
-
if (isMobile) return fly(node, { duration: 250,
|
|
85
|
+
if (isMobile) return fly(node, { duration: 250, y: window.innerHeight, opacity: 1 })
|
|
86
86
|
return scale(node, { duration: 150, start: 0.85 })
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -103,6 +103,14 @@
|
|
|
103
103
|
onhashchange={close}
|
|
104
104
|
bind:innerWidth={windowWidth} />
|
|
105
105
|
|
|
106
|
+
{#snippet closeButton()}
|
|
107
|
+
<div class="close {closeButtonStyle}" transition:scaleOrFly|global bind:this={closeElement}>
|
|
108
|
+
<RoundButton onclick={close} aria-label="Close">
|
|
109
|
+
<IconClose />
|
|
110
|
+
</RoundButton>
|
|
111
|
+
</div>
|
|
112
|
+
{/snippet}
|
|
113
|
+
|
|
106
114
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
107
115
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
108
116
|
<div
|
|
@@ -114,38 +122,38 @@
|
|
|
114
122
|
style:--dialog-offset="{dialogOffset}px"
|
|
115
123
|
onclick={closeOnBackdropClick}
|
|
116
124
|
data-testid="modal"
|
|
117
|
-
|
|
125
|
+
in:fade|global={{ duration: 150 }}
|
|
126
|
+
out:fade|global={{ duration: 150, delay: 100 }}
|
|
118
127
|
bind:this={modalElement}
|
|
119
128
|
use:focustrap>
|
|
120
129
|
{#if prepend}
|
|
121
|
-
<div class="prepend" transition:scaleOrFly|global
|
|
130
|
+
<div class="prepend" transition:scaleOrFly|global data-view-transition-new="playpilot-title-extra">
|
|
122
131
|
{@render prepend()}
|
|
123
132
|
</div>
|
|
124
133
|
{/if}
|
|
125
134
|
|
|
126
135
|
{#if bubble}
|
|
127
|
-
<div class="bubble" transition:scaleOrFly|global
|
|
136
|
+
<div class="bubble" transition:scaleOrFly|global data-view-transition-new="playpilot-title-extra">
|
|
128
137
|
{@render bubble()}
|
|
129
138
|
</div>
|
|
130
139
|
{/if}
|
|
131
140
|
|
|
132
141
|
{#if isMobile}
|
|
133
142
|
<div class="swipe-handle" transition:scaleOrFly|global>
|
|
134
|
-
|
|
143
|
+
{#if dialogElement && closeElement}
|
|
144
|
+
<SwipeHandle targets={[dialogElement, closeElement]} onpassed={close} />
|
|
145
|
+
{/if}
|
|
135
146
|
</div>
|
|
136
147
|
{/if}
|
|
137
148
|
|
|
138
149
|
<div
|
|
139
150
|
class="dialog"
|
|
140
|
-
class:transitioning={isTransitioningIn}
|
|
141
151
|
{onscroll}
|
|
142
152
|
bind:this={dialogElement}
|
|
143
153
|
role="dialog"
|
|
144
154
|
aria-labelledby="title"
|
|
145
155
|
data-view-transition-new="playpilot-title-content"
|
|
146
|
-
|
|
147
|
-
onintroend={() => isTransitioningIn = false}
|
|
148
|
-
transition:scaleOrFly|global={{ y: window.innerHeight }}>
|
|
156
|
+
transition:scaleOrFly|global>
|
|
149
157
|
{#if hasPreviousModal}
|
|
150
158
|
<div class="close back {closeButtonStyle}">
|
|
151
159
|
<RoundButton onclick={() => goBackToPreviousModal()} aria-label="Back">
|
|
@@ -154,14 +162,16 @@
|
|
|
154
162
|
</div>
|
|
155
163
|
{/if}
|
|
156
164
|
|
|
157
|
-
<div class="close {closeButtonStyle}">
|
|
158
|
-
<RoundButton onclick={close} aria-label="Close">
|
|
159
|
-
<IconClose />
|
|
160
|
-
</RoundButton>
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
165
|
{@render children()}
|
|
166
|
+
|
|
167
|
+
{#if !isMobile}
|
|
168
|
+
{@render closeButton()}
|
|
169
|
+
{/if}
|
|
164
170
|
</div>
|
|
171
|
+
|
|
172
|
+
{#if isMobile}
|
|
173
|
+
{@render closeButton()}
|
|
174
|
+
{/if}
|
|
165
175
|
</div>
|
|
166
176
|
|
|
167
177
|
<style lang="scss">
|
|
@@ -174,7 +184,6 @@
|
|
|
174
184
|
flex-direction: column;
|
|
175
185
|
justify-content: flex-start;
|
|
176
186
|
align-items: center;
|
|
177
|
-
overflow: auto;
|
|
178
187
|
top: 0;
|
|
179
188
|
right: 0;
|
|
180
189
|
bottom: 0;
|
|
@@ -182,6 +191,8 @@
|
|
|
182
191
|
background: theme(detail-backdrop, rgba(0, 0, 0, 0.65));
|
|
183
192
|
|
|
184
193
|
@include desktop() {
|
|
194
|
+
overflow: auto;
|
|
195
|
+
overscroll-behavior: contain;
|
|
185
196
|
padding: margin(2) 0;
|
|
186
197
|
}
|
|
187
198
|
|
|
@@ -208,9 +219,11 @@
|
|
|
208
219
|
margin-top: auto;
|
|
209
220
|
padding-bottom: env(safe-area-inset-bottom);
|
|
210
221
|
overflow: auto;
|
|
222
|
+
overscroll-behavior: contain;
|
|
211
223
|
border-radius: theme(detail-border-radius, margin(1) margin(1) 0 0);
|
|
212
224
|
background: theme(detail-background, light);
|
|
213
|
-
|
|
225
|
+
will-change: top;
|
|
226
|
+
transition: top 200ms;
|
|
214
227
|
|
|
215
228
|
@supports (height: 1dvh) {
|
|
216
229
|
height: 80dvh;
|
|
@@ -218,7 +231,7 @@
|
|
|
218
231
|
|
|
219
232
|
@include desktop() {
|
|
220
233
|
height: unset;
|
|
221
|
-
margin
|
|
234
|
+
margin: auto 0;
|
|
222
235
|
padding-bottom: 0;
|
|
223
236
|
overflow: visible;
|
|
224
237
|
border-radius: theme(detail-border-radius, border-radius-large);
|
|
@@ -262,11 +275,14 @@
|
|
|
262
275
|
--playpilot-button-text-color: #{theme(modal-close-button-text-color, text-color-alt)};
|
|
263
276
|
z-index: 5;
|
|
264
277
|
position: fixed;
|
|
265
|
-
top: calc(var(--dialog-offset) + margin(1));
|
|
278
|
+
margin-top: calc(var(--dialog-offset) + margin(1));
|
|
279
|
+
top: 0;
|
|
266
280
|
right: margin(1);
|
|
281
|
+
transition: top 200ms;
|
|
267
282
|
|
|
268
283
|
@include desktop() {
|
|
269
284
|
position: absolute;
|
|
285
|
+
margin-top: 0;
|
|
270
286
|
top: margin(1);
|
|
271
287
|
}
|
|
272
288
|
|
|
@@ -291,10 +307,6 @@
|
|
|
291
307
|
}
|
|
292
308
|
}
|
|
293
309
|
}
|
|
294
|
-
|
|
295
|
-
.transitioning & {
|
|
296
|
-
top: margin(1);
|
|
297
|
-
}
|
|
298
310
|
}
|
|
299
311
|
|
|
300
312
|
.prepend {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Disclaimer from '../Ads/Disclaimer.svelte'
|
|
3
|
+
import TrackingPixels from '../TrackingPixels.svelte'
|
|
3
4
|
import { hasConsentedTo } from '$lib/consent'
|
|
4
5
|
import { removeImageUrlPrefix } from '$lib/image'
|
|
5
6
|
import { t } from '$lib/localization'
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
|
|
15
16
|
const { playlink, disclaimer = '', hideCategory = false, onclick = () => null }: Props = $props()
|
|
16
17
|
|
|
17
|
-
const { name, url, logo_url, highlighted, cta_text, action_text, extra_info: { category } } = $derived(playlink)
|
|
18
|
+
const { name, url, logo_url, highlighted, cta_text, action_text, pixels, extra_info: { category } } = $derived(playlink)
|
|
18
19
|
|
|
19
20
|
const categoryStrings = {
|
|
20
21
|
SVOD: t('Stream'),
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
class="playlink"
|
|
34
35
|
class:highlighted={highlighted && cta_text}
|
|
35
36
|
class:no-category={hideCategory}
|
|
37
|
+
class:no-subtext={!cta_text && hideCategory}
|
|
36
38
|
data-playlink={name}
|
|
37
39
|
rel="sponsored">
|
|
38
40
|
|
|
@@ -57,6 +59,10 @@
|
|
|
57
59
|
<Disclaimer {disclaimer} small />
|
|
58
60
|
{/if}
|
|
59
61
|
</div>
|
|
62
|
+
|
|
63
|
+
{#if pixels?.length}
|
|
64
|
+
<TrackingPixels {pixels} />
|
|
65
|
+
{/if}
|
|
60
66
|
</svelte:element>
|
|
61
67
|
|
|
62
68
|
<style lang="scss">
|
|
@@ -162,6 +168,10 @@
|
|
|
162
168
|
.no-category & {
|
|
163
169
|
grid-template-areas: "image name action" "image cta action";
|
|
164
170
|
}
|
|
171
|
+
|
|
172
|
+
.no-subtext & {
|
|
173
|
+
grid-template-areas: "image name action";
|
|
174
|
+
}
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
.name {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import TrackingPixels from '../TrackingPixels.svelte'
|
|
2
3
|
import { removeImageUrlPrefix } from '$lib/image'
|
|
3
4
|
import type { PlaylinkData } from '$lib/types/playlink'
|
|
4
5
|
|
|
@@ -11,13 +12,18 @@
|
|
|
11
12
|
|
|
12
13
|
const { playlink, size = 30, onclick = () => null }: Props = $props()
|
|
13
14
|
|
|
14
|
-
const { name, url, logo_url } = $derived(playlink)
|
|
15
|
+
const { name, url, logo_url, pixels } = $derived(playlink)
|
|
15
16
|
</script>
|
|
16
17
|
|
|
17
18
|
<a href={url} target="_blank" class="playlink" data-playlink={name} rel="sponsored" {onclick} style:--size="{size}px">
|
|
18
19
|
<img src={removeImageUrlPrefix(logo_url)} alt={name} height={size} width={size} />
|
|
20
|
+
|
|
21
|
+
{#if pixels?.length}
|
|
22
|
+
<TrackingPixels {pixels} />
|
|
23
|
+
{/if}
|
|
19
24
|
</a>
|
|
20
25
|
|
|
26
|
+
|
|
21
27
|
<style lang="scss">
|
|
22
28
|
.playlink {
|
|
23
29
|
display: inline-block;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
interface Props {
|
|
3
|
-
|
|
3
|
+
targets: HTMLElement[]
|
|
4
4
|
threshold?: number
|
|
5
5
|
isDragging?: boolean
|
|
6
6
|
passedThreshold?: boolean
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
let {
|
|
11
|
-
|
|
11
|
+
targets,
|
|
12
12
|
threshold = 100,
|
|
13
13
|
isDragging = $bindable(false),
|
|
14
14
|
passedThreshold = $bindable(false),
|
|
@@ -28,28 +28,33 @@
|
|
|
28
28
|
|
|
29
29
|
export function start(event: TouchEvent): void {
|
|
30
30
|
if (!event) return
|
|
31
|
-
if (!target) return
|
|
32
31
|
|
|
33
32
|
isDragging = true
|
|
34
33
|
hasBeenDragged = true
|
|
35
34
|
|
|
36
35
|
movementStartY = event.touches[0].pageY
|
|
37
|
-
|
|
36
|
+
|
|
37
|
+
for (const target of targets) {
|
|
38
|
+
target.style.transitionDuration = '0ms'
|
|
39
|
+
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export function move(event: TouchEvent): void {
|
|
41
43
|
if (!event) return
|
|
42
|
-
if (!target) return
|
|
43
44
|
if (!isDragging) return
|
|
44
45
|
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
|
|
45
48
|
movementCurrentY = event.touches[0].pageY
|
|
46
49
|
|
|
47
|
-
target.style.transform = `translateY(${difference}px)`
|
|
48
50
|
handleElement!.style.transform = `translateY(${difference}px)`
|
|
51
|
+
|
|
52
|
+
for (const target of targets) {
|
|
53
|
+
target.style.top = `${difference}px`
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export function end(): void {
|
|
52
|
-
if (!target) return
|
|
53
58
|
if (!isDragging) return
|
|
54
59
|
|
|
55
60
|
isDragging = false
|
|
@@ -57,8 +62,12 @@
|
|
|
57
62
|
if (passedThreshold) {
|
|
58
63
|
onpassed()
|
|
59
64
|
} else {
|
|
60
|
-
target.removeAttribute('style')
|
|
61
65
|
handleElement!.removeAttribute('style')
|
|
66
|
+
|
|
67
|
+
for (const target of targets) {
|
|
68
|
+
target.style.removeProperty('transition')
|
|
69
|
+
requestAnimationFrame(() => target.style.top = '0px')
|
|
70
|
+
}
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
movementStartY = 0
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import Title from './Title.svelte'
|
|
9
9
|
import TopScroll from './Ads/TopScroll.svelte'
|
|
10
10
|
import Display from './Ads/Display.svelte'
|
|
11
|
-
import TrackingPixels from './TrackingPixels.svelte'
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
14
13
|
event: MouseEvent
|
|
@@ -68,9 +67,6 @@
|
|
|
68
67
|
<div class="title-popover" bind:this={element} data-playpilot-title-popover role="region" aria-labelledby="title">
|
|
69
68
|
<Popover append={displayAd ? append : null} bubble={topScroll ? bubble : null}>
|
|
70
69
|
<Title {title} small />
|
|
71
|
-
|
|
72
|
-
<!-- Temporary tracking pixel to verify our own tracking works as expected. The random id exists to bypass cache on subsequent requests. -->
|
|
73
|
-
<TrackingPixels pixels={[`https://imp.pxf.io/i/2439446/837174/9358?id=${Math.random()}`]} />
|
|
74
70
|
</Popover>
|
|
75
71
|
</div>
|
|
76
72
|
|
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const { pixels }: Props = $props()
|
|
9
|
+
|
|
10
|
+
let key = $state(Math.random())
|
|
9
11
|
</script>
|
|
10
12
|
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
{
|
|
13
|
+
<svelte:window onupdateconsent={() => key = Math.random()} />
|
|
14
|
+
|
|
15
|
+
{#key key}
|
|
16
|
+
{#if hasConsentedTo('pixels')}
|
|
17
|
+
{#each pixels as src}
|
|
18
|
+
<img {src} alt="" data-playpilot-pixel />
|
|
19
|
+
{/each}
|
|
20
|
+
{/if}
|
|
21
|
+
{/key}
|
|
16
22
|
|
|
17
23
|
<style lang="scss">
|
|
18
24
|
img {
|
|
@@ -15,10 +15,13 @@ describe('Consent.svelte', () => {
|
|
|
15
15
|
expect(onchange).toHaveBeenCalled()
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
it('Should call onchange after user has consented', () => {
|
|
18
|
+
it('Should call onchange and fire window event after user has consented', () => {
|
|
19
19
|
// @ts-ignore
|
|
20
20
|
window.PlayPilotLinkInjections = { require_consent: true }
|
|
21
21
|
|
|
22
|
+
const mock = vi.fn()
|
|
23
|
+
window.addEventListener('updateconsent', mock)
|
|
24
|
+
|
|
22
25
|
// @ts-ignore
|
|
23
26
|
window.__tcfapi = (command, _version, callback) => {
|
|
24
27
|
if (command !== 'addEventListener') return
|
|
@@ -39,6 +42,7 @@ describe('Consent.svelte', () => {
|
|
|
39
42
|
window.__tcfapi()
|
|
40
43
|
|
|
41
44
|
expect(onchange).toHaveBeenCalled()
|
|
45
|
+
expect(mock).toHaveBeenCalled()
|
|
42
46
|
})
|
|
43
47
|
|
|
44
48
|
it('Should not call onchange after user has consented but eventStatus is invalid', () => {
|
|
@@ -6,6 +6,7 @@ import { fetchTitles } from '$lib/api/titles'
|
|
|
6
6
|
import { title } from '$lib/fakeData'
|
|
7
7
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
8
8
|
import { track } from '$lib/tracking'
|
|
9
|
+
import { fetchAds } from '$lib/api/ads'
|
|
9
10
|
|
|
10
11
|
vi.mock('$lib/api/titles', () => ({
|
|
11
12
|
fetchTitles: vi.fn(),
|
|
@@ -19,8 +20,15 @@ vi.mock('$lib/tracking', () => ({
|
|
|
19
20
|
track: vi.fn(),
|
|
20
21
|
}))
|
|
21
22
|
|
|
23
|
+
vi.mock('$lib/api/ads', () => ({
|
|
24
|
+
fetchAds: vi.fn(),
|
|
25
|
+
}))
|
|
26
|
+
|
|
22
27
|
describe('Explore.svelte', () => {
|
|
23
28
|
beforeEach(() => {
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
window.PlayPilotLinkInjections = {}
|
|
31
|
+
|
|
24
32
|
vi.resetAllMocks()
|
|
25
33
|
})
|
|
26
34
|
|
|
@@ -30,10 +38,23 @@ describe('Explore.svelte', () => {
|
|
|
30
38
|
expect(fetchTitles).toHaveBeenCalledWith({ page_size: 24, page: 1 })
|
|
31
39
|
})
|
|
32
40
|
|
|
33
|
-
it('Should call tracking event on mount', () => {
|
|
41
|
+
it('Should call tracking event and fetchAds on mount', () => {
|
|
34
42
|
render(Explore)
|
|
35
43
|
|
|
36
44
|
expect(track).toHaveBeenCalledWith(TrackingEvent.ExplorePageView)
|
|
45
|
+
expect(fetchAds).toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('Should not call fetchAds on mount if ads are already present', () => {
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
window.PlayPilotLinkInjections = {
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
ads: ['a'],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(Explore)
|
|
56
|
+
|
|
57
|
+
expect(fetchAds).not.toHaveBeenCalled()
|
|
37
58
|
})
|
|
38
59
|
|
|
39
60
|
it('Should render all returned titles', async () => {
|
|
@@ -131,4 +131,36 @@ describe('Playlink.svelte', () => {
|
|
|
131
131
|
|
|
132
132
|
expect(getByTestId('disclaimer')).toBeTruthy()
|
|
133
133
|
})
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
it('Should render given tracking pixels', () => {
|
|
137
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => true)
|
|
138
|
+
|
|
139
|
+
const playlink = { name: 'Some playlink', logo_url: 'logo', pixels: ['https://some-pixel.com/a.jpg'], extra_info: { category: 'SVOD' } }
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
const { container } = render(Playlink, { playlink })
|
|
142
|
+
|
|
143
|
+
expect(container.querySelector('img[src*="https://some-pixel.com/a.jpg"]')).toBeTruthy()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('Should include no-subtext class if no cta_text is given and hideCategory is true', () => {
|
|
147
|
+
// @ts-ignore
|
|
148
|
+
const { container } = render(Playlink, { playlink: { ...playlink, cta_text: '' }, hideCategory: true })
|
|
149
|
+
|
|
150
|
+
expect(container.querySelector('.no-subtext')).toBeTruthy()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('Should not include no-subtext class if no cta_text is given but hideCategory is false', () => {
|
|
154
|
+
// @ts-ignore
|
|
155
|
+
const { container } = render(Playlink, { playlink: { ...playlink, cta_text: '' }, hideCategory: false })
|
|
156
|
+
|
|
157
|
+
expect(container.querySelector('.no-subtext')).not.toBeTruthy()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('Should not include no-subtext class if cta_text is given and hideCategory is true', () => {
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
const { container } = render(Playlink, { playlink: { ...playlink, cta_text: 'Some text' }, hideCategory: true })
|
|
163
|
+
|
|
164
|
+
expect(container.querySelector('.no-subtext')).not.toBeTruthy()
|
|
165
|
+
})
|
|
134
166
|
})
|