@playpilot/tpi 5.6.0-beta.video.7 → 5.7.0-beta.display-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/dist/link-injections.js +9 -9
- package/events.md +8 -2
- package/package.json +1 -1
- package/src/lib/ads.ts +38 -0
- package/src/lib/constants.ts +2 -1
- package/src/lib/enums/ImageDimensions.ts +7 -0
- package/src/lib/enums/SplitTest.ts +23 -0
- package/src/lib/enums/TrackingEvent.ts +3 -0
- package/src/lib/image.ts +12 -0
- package/src/lib/linkInjection.ts +36 -2
- package/src/lib/splitTest.ts +19 -14
- package/src/lib/types/campaign.d.ts +45 -0
- package/src/lib/types/playlink.d.ts +2 -0
- package/src/lib/types/script.d.ts +5 -0
- package/src/main.ts +6 -0
- package/src/routes/+layout.svelte +4 -0
- package/src/routes/+page.svelte +13 -2
- package/src/routes/components/Ads/Display.svelte +140 -0
- package/src/routes/components/Ads/TopScroll.svelte +240 -0
- package/src/routes/components/DragHandle.svelte +105 -0
- package/src/routes/components/Modal.svelte +125 -30
- package/src/routes/components/Playlink.svelte +185 -0
- package/src/routes/components/Playlinks.svelte +34 -168
- package/src/routes/components/Popover.svelte +30 -2
- package/src/routes/components/Title.svelte +13 -43
- package/src/routes/components/TitleModal.svelte +24 -10
- package/src/routes/components/TitlePopover.svelte +12 -6
- package/src/tests/lib/ads.test.js +55 -0
- package/src/tests/lib/splitTest.test.js +97 -18
- package/src/tests/routes/+page.test.js +33 -0
- package/src/tests/routes/components/Ads/Display.test.js +109 -0
- package/src/tests/routes/components/Ads/TopScroll.test.js +104 -0
- package/src/tests/routes/components/Playlink.test.js +95 -0
- package/src/tests/routes/components/Playlinks.test.js +0 -30
- package/src/tests/routes/components/TitleModal.test.js +36 -0
- package/src/tests/routes/components/TitlePopover.test.js +20 -0
- package/src/routes/components/Ads/FloatingPlaylink.svelte +0 -62
- package/src/routes/components/Ads/VideoBackground.svelte +0 -28
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { removeImageUrlPrefix } from '$lib/image'
|
|
3
|
+
import { t } from '$lib/localization'
|
|
4
|
+
import type { PlaylinkData } from '$lib/types/playlink'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
playlink: PlaylinkData
|
|
8
|
+
hideCategory?: boolean
|
|
9
|
+
onclick?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { playlink, hideCategory = false, onclick = () => null }: Props = $props()
|
|
13
|
+
|
|
14
|
+
const { name, url, logo_url, highlighted, cta_text, action_text, extra_info: { category } } = $derived(playlink)
|
|
15
|
+
|
|
16
|
+
const categoryStrings = {
|
|
17
|
+
SVOD: t('Stream'),
|
|
18
|
+
BUY: t('Buy'),
|
|
19
|
+
RENT: t('Rent'),
|
|
20
|
+
TVOD: t('Rent Or Buy'),
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<a href={url} target="_blank" class="playlink" class:highlighted={highlighted && cta_text} class:no-category={hideCategory} {onclick} data-playlink={name} rel="sponsored">
|
|
25
|
+
<div class="playlink-content">
|
|
26
|
+
<img src={removeImageUrlPrefix(logo_url)} alt="" height="36" width="36" />
|
|
27
|
+
|
|
28
|
+
<span class="name">{name}</span>
|
|
29
|
+
|
|
30
|
+
{#if !hideCategory}
|
|
31
|
+
<span class="category" data-testid="category">{categoryStrings[category] || t('Stream')}</span>
|
|
32
|
+
{/if}
|
|
33
|
+
|
|
34
|
+
{#if cta_text}
|
|
35
|
+
<span class="cta">{cta_text}</span>
|
|
36
|
+
{/if}
|
|
37
|
+
|
|
38
|
+
<div class="action">
|
|
39
|
+
{action_text || t('Watch')}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</a>
|
|
43
|
+
|
|
44
|
+
<style lang="scss">
|
|
45
|
+
$image-size: margin(2.25);
|
|
46
|
+
|
|
47
|
+
@keyframes sheen {
|
|
48
|
+
0%, 20%, 60%, 80.01%, 100% {
|
|
49
|
+
background-position: -100% 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
80% {
|
|
53
|
+
background-position: 100% 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
img {
|
|
58
|
+
height: $image-size;
|
|
59
|
+
width: $image-size;
|
|
60
|
+
border-radius: margin(0.5);
|
|
61
|
+
background: rgba(0, 0, 0, 0.25);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.playlink {
|
|
65
|
+
position: relative;
|
|
66
|
+
background: var(--playpilot-playlink-background, var(--playpilot-lighter));
|
|
67
|
+
box-shadow: var(--playpilot-playlink-shadow, var(--playpilot-shadow));
|
|
68
|
+
border-radius: var(--playpilot-playlink-border-radius, margin(0.5));
|
|
69
|
+
color: var(--playpilot-playlink-text-color, var(--playpilot-text-color)) !important;
|
|
70
|
+
font-weight: var(--playpilot-playlink-font-weight, inherit);
|
|
71
|
+
font-style: var(--playpilot-playlink-font-style, normal) !important;
|
|
72
|
+
text-decoration: none !important;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
font-size: var(--playpilot-playlinks-font-size, margin(0.75));
|
|
75
|
+
line-height: 1;
|
|
76
|
+
|
|
77
|
+
&:hover,
|
|
78
|
+
&:active {
|
|
79
|
+
filter: var(--playpilot-playlink-hover-filter, brightness(1.1));
|
|
80
|
+
background: var(--playpilot-playlink-hover-background, var(--playpilot-playlink-background, var(--playpilot-lighter))) !important;
|
|
81
|
+
text-decoration: none !important;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&.highlighted {
|
|
85
|
+
grid-column: span 2;
|
|
86
|
+
|
|
87
|
+
&::before {
|
|
88
|
+
content: "";
|
|
89
|
+
z-index: 1;
|
|
90
|
+
display: block;
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 0;
|
|
93
|
+
right: 0;
|
|
94
|
+
bottom: 0;
|
|
95
|
+
left: 0;
|
|
96
|
+
border-radius: inherit;
|
|
97
|
+
background: linear-gradient(to left, currentColor 20%, transparent 50%, currentColor 80%);
|
|
98
|
+
background-size: 200% 100%;
|
|
99
|
+
background-position: -100% 0;
|
|
100
|
+
opacity: 0.35;
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
|
|
103
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
104
|
+
animation: sheen 4000ms ease-in-out infinite;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&::after {
|
|
109
|
+
content: "";
|
|
110
|
+
z-index: 2;
|
|
111
|
+
display: block;
|
|
112
|
+
position: absolute;
|
|
113
|
+
top: 2px;
|
|
114
|
+
right: 2px;
|
|
115
|
+
bottom: 2px;
|
|
116
|
+
left: 2px;
|
|
117
|
+
border-radius: inherit;
|
|
118
|
+
background: var(--playpilot-playlink-background, var(--playpilot-lighter));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
img {
|
|
123
|
+
grid-area: image;
|
|
124
|
+
margin: 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.playlink-content {
|
|
129
|
+
position: relative;
|
|
130
|
+
z-index: 5;
|
|
131
|
+
display: grid;
|
|
132
|
+
grid-template-areas: "image name action" "image category action";
|
|
133
|
+
grid-template-columns: $image-size auto margin(6);
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 0 margin(0.75);
|
|
136
|
+
padding: margin(0.75);
|
|
137
|
+
|
|
138
|
+
.highlighted & {
|
|
139
|
+
grid-template-areas: "image name action" "image category action" "image cta action";
|
|
140
|
+
gap: margin(0.25) margin(0.75);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.no-category & {
|
|
144
|
+
grid-template-areas: "image name action" "image cta action";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.name {
|
|
149
|
+
grid-area: name;
|
|
150
|
+
width: 100%;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
font-weight: var(--playpilot-playlink-font-weight, 500);
|
|
153
|
+
font-family: var(--playpilot-playlink-font-family, inherit);
|
|
154
|
+
text-overflow: ellipsis;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.category {
|
|
159
|
+
grid-area: category;
|
|
160
|
+
width: 100%;
|
|
161
|
+
font-size: var(--playpilot-playlinks-category-font-size, margin(0.625));
|
|
162
|
+
color: var(--playpilot-playlink-category-text-color, var(--playpilot-text-color-alt));
|
|
163
|
+
font-weight: var(--playpilot-playlink-category-font-weight, inherit);
|
|
164
|
+
font-family: var(--playpilot-playlink-category-font-family, inherit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.cta {
|
|
168
|
+
grid-area: cta;
|
|
169
|
+
width: 100%;
|
|
170
|
+
font-size: var(--playpilot-playlinks-cta-font-size, var(--playpilot-playlinks-category-font-size, margin(0.625)));
|
|
171
|
+
color: var(--playpilot-playlink-cta-text-color, var(--playpilot-playlink-category-text-color, var(--playpilot-text-color)));
|
|
172
|
+
font-weight: var(--playpilot-playlink-cta-font-weight, var(--playpilot-playlink-category-font-weight, inherit));
|
|
173
|
+
font-family: var(--playpilot-playlink-cta-font-family, var(--playpilot-playlink-category-font-family, inherit));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.action {
|
|
177
|
+
grid-area: action;
|
|
178
|
+
margin-left: auto;
|
|
179
|
+
padding: margin(0.5);
|
|
180
|
+
border: var(--playpilot-playlinks-action-border, 1px solid currentColor);
|
|
181
|
+
border-radius: var(--playpilot-playlinks-action-border-radius, margin(2));
|
|
182
|
+
font-weight: var(--playpilot-playlinks-action-font-weight, 500);
|
|
183
|
+
color: var(--playpilot-playlinks-action-text-color, var(--playpilot-text-color));
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
import type { PlaylinkData } from '$lib/types/playlink'
|
|
7
7
|
import type { TitleData } from '$lib/types/title'
|
|
8
8
|
import { heading } from '$lib/actions/heading'
|
|
9
|
+
import { isInSplitTestVariant, trackSplitTestAction } from '$lib/splitTest'
|
|
10
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
9
11
|
import { getContext } from 'svelte'
|
|
10
|
-
import
|
|
12
|
+
import Playlink from './Playlink.svelte'
|
|
13
|
+
import Display from './Ads/Display.svelte'
|
|
14
|
+
import { campaignToPlaylink, getFirstAdOfType } from '$lib/ads'
|
|
11
15
|
|
|
12
16
|
interface Props {
|
|
13
17
|
playlinks: PlaylinkData[]
|
|
@@ -17,22 +21,21 @@
|
|
|
17
21
|
const { playlinks, title }: Props = $props()
|
|
18
22
|
|
|
19
23
|
const isModal = getContext('scope') === 'modal'
|
|
24
|
+
const displayAd = getFirstAdOfType('card')
|
|
25
|
+
const showDisplayAd = displayAd && isModal && isInSplitTestVariant(SplitTest.DisplayAdPosition, 0)
|
|
26
|
+
const showPlaylinkAd = displayAd && isModal && isInSplitTestVariant(SplitTest.DisplayAdPosition, 1)
|
|
20
27
|
|
|
21
28
|
let outerWidth = $state(0)
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
// Grid turns into a list when the playlinks container is small enough
|
|
31
|
+
// It is also a list by default if a display ad is present, as that would
|
|
32
|
+
// otherwise break the layout in ways that don't make sense to fix.
|
|
33
|
+
const list = $derived(outerWidth < 500 || showDisplayAd || showPlaylinkAd)
|
|
24
34
|
|
|
25
|
-
// Remove any playlinks without logos,
|
|
35
|
+
// Remove any playlinks without logos, these are likely sub providers.
|
|
26
36
|
const filteredPlaylinks = $derived(playlinks.filter(playlink => !!playlink.logo_url))
|
|
27
37
|
const mergedPlaylink = $derived(mergePlaylinks(filteredPlaylinks))
|
|
28
38
|
|
|
29
|
-
const categoryStrings = {
|
|
30
|
-
SVOD: t('Stream'),
|
|
31
|
-
BUY: t('Buy'),
|
|
32
|
-
RENT: t('Rent'),
|
|
33
|
-
TVOD: t('Rent Or Buy'),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
39
|
function onclick(playlink: string): void {
|
|
37
40
|
track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.TitlePopoverPlaylinkClick, title, { playlink })
|
|
38
41
|
}
|
|
@@ -48,42 +51,25 @@
|
|
|
48
51
|
{/if}
|
|
49
52
|
|
|
50
53
|
<div class="playlinks" class:list bind:clientWidth={outerWidth}>
|
|
51
|
-
{#each mergedPlaylink as
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<span class="cta">{cta_text}</span>
|
|
61
|
-
{/if}
|
|
62
|
-
|
|
63
|
-
<div class="action">
|
|
64
|
-
{t('Watch')}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</a>
|
|
54
|
+
{#each mergedPlaylink as playlink, index}
|
|
55
|
+
<Playlink {playlink} onclick={() => onclick(playlink.name)} />
|
|
56
|
+
|
|
57
|
+
<!-- Insert display ad after the first playlink -->
|
|
58
|
+
{#if showDisplayAd && (index === 0)}
|
|
59
|
+
<Display campaign={displayAd} />
|
|
60
|
+
{:else if showPlaylinkAd && (index === 0)}
|
|
61
|
+
<Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => trackSplitTestAction(SplitTest.DisplayAdPosition, 'click_playlink_ad')} hideCategory />
|
|
62
|
+
{/if}
|
|
68
63
|
{/each}
|
|
69
64
|
|
|
70
|
-
{#if !
|
|
71
|
-
<div class="
|
|
65
|
+
{#if !mergedPlaylink.length}
|
|
66
|
+
<div class="empty" data-testid="playlinks-empty">
|
|
72
67
|
{t('Title Unavailable')}
|
|
73
68
|
</div>
|
|
74
69
|
{/if}
|
|
75
70
|
</div>
|
|
76
71
|
|
|
77
72
|
<style lang="scss">
|
|
78
|
-
$image-size: margin(2.25);
|
|
79
|
-
|
|
80
|
-
img {
|
|
81
|
-
height: $image-size;
|
|
82
|
-
width: $image-size;
|
|
83
|
-
border-radius: margin(0.5);
|
|
84
|
-
background: rgba(0, 0, 0, 0.25);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
73
|
.heading {
|
|
88
74
|
margin: 0;
|
|
89
75
|
color: var(--playpilot-playlinks-title-color, var(--playpilot-text-color-alt));
|
|
@@ -104,130 +90,11 @@
|
|
|
104
90
|
|
|
105
91
|
&.list {
|
|
106
92
|
grid-template-columns: 1fr;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@keyframes sheen {
|
|
112
|
-
0%, 20%, 60%, 80.01%, 100% {
|
|
113
|
-
background-position: -100% 0;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
80% {
|
|
117
|
-
background-position: 100% 0;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.playlink {
|
|
122
|
-
position: relative;
|
|
123
|
-
background: var(--playpilot-playlink-background, var(--playpilot-lighter));
|
|
124
|
-
box-shadow: var(--playpilot-playlink-shadow, var(--playpilot-shadow));
|
|
125
|
-
border-radius: var(--playpilot-playlink-border-radius, margin(0.5));
|
|
126
|
-
color: var(--playpilot-playlink-text-color, var(--playpilot-text-color)) !important;
|
|
127
|
-
font-weight: var(--playpilot-playlink-font-weight, inherit);
|
|
128
|
-
font-style: var(--playpilot-playlink-font-style, normal) !important;
|
|
129
|
-
text-decoration: none !important;
|
|
130
|
-
white-space: nowrap;
|
|
131
|
-
font-size: var(--playpilot-playlinks-font-size, margin(0.75));
|
|
132
|
-
line-height: 1;
|
|
133
|
-
|
|
134
|
-
&:hover,
|
|
135
|
-
&:active {
|
|
136
|
-
filter: var(--playpilot-playlink-hover-filter, brightness(1.1));
|
|
137
|
-
background: var(--playpilot-playlink-hover-background, var(--playpilot-playlink-background, var(--playpilot-lighter))) !important;
|
|
138
|
-
text-decoration: none !important;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
&.highlighted {
|
|
142
|
-
grid-column: span 2;
|
|
143
|
-
|
|
144
|
-
.list & {
|
|
145
|
-
grid-column: 1;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
&::before {
|
|
149
|
-
content: "";
|
|
150
|
-
z-index: 1;
|
|
151
|
-
display: block;
|
|
152
|
-
position: absolute;
|
|
153
|
-
top: 0;
|
|
154
|
-
right: 0;
|
|
155
|
-
bottom: 0;
|
|
156
|
-
left: 0;
|
|
157
|
-
border-radius: inherit;
|
|
158
|
-
background: linear-gradient(to left, currentColor 20%, transparent 50%, currentColor 80%);
|
|
159
|
-
background-size: 200% 100%;
|
|
160
|
-
background-position: -100% 0;
|
|
161
|
-
opacity: 0.35;
|
|
162
|
-
pointer-events: none;
|
|
163
|
-
|
|
164
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
165
|
-
animation: sheen 4000ms ease-in-out infinite;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
93
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
z-index: 2;
|
|
172
|
-
display: block;
|
|
173
|
-
position: absolute;
|
|
174
|
-
top: 2px;
|
|
175
|
-
right: 2px;
|
|
176
|
-
bottom: 2px;
|
|
177
|
-
left: 2px;
|
|
178
|
-
border-radius: inherit;
|
|
179
|
-
background: var(--playpilot-playlink-background, var(--playpilot-lighter));
|
|
94
|
+
:global(.playlink.highlighted) {
|
|
95
|
+
grid-column: span 1;
|
|
180
96
|
}
|
|
181
97
|
}
|
|
182
|
-
|
|
183
|
-
img {
|
|
184
|
-
grid-area: image;
|
|
185
|
-
margin: 0;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.playlink-content {
|
|
190
|
-
position: relative;
|
|
191
|
-
z-index: 5;
|
|
192
|
-
display: grid;
|
|
193
|
-
grid-template-areas: "image name action" "image category action";
|
|
194
|
-
grid-template-columns: $image-size auto margin(4);
|
|
195
|
-
align-items: center;
|
|
196
|
-
gap: 0 margin(0.75);
|
|
197
|
-
padding: margin(0.75);
|
|
198
|
-
|
|
199
|
-
.highlighted & {
|
|
200
|
-
grid-template-areas: "image name action" "image category action" "image cta action";
|
|
201
|
-
gap: margin(0.25) margin(0.75);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.name {
|
|
206
|
-
grid-area: name;
|
|
207
|
-
width: 100%;
|
|
208
|
-
overflow: hidden;
|
|
209
|
-
font-weight: var(--playpilot-playlink-font-weight, 500);
|
|
210
|
-
font-family: var(--playpilot-playlink-font-family, inherit);
|
|
211
|
-
text-overflow: ellipsis;
|
|
212
|
-
white-space: nowrap;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.category {
|
|
216
|
-
grid-area: category;
|
|
217
|
-
width: 100%;
|
|
218
|
-
font-size: var(--playpilot-playlinks-category-font-size, margin(0.625));
|
|
219
|
-
color: var(--playpilot-playlink-category-text-color, var(--playpilot-text-color-alt));
|
|
220
|
-
font-weight: var(--playpilot-playlink-category-font-weight, inherit);
|
|
221
|
-
font-family: var(--playpilot-playlink-category-font-family, inherit);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.cta {
|
|
225
|
-
grid-area: cta;
|
|
226
|
-
width: 100%;
|
|
227
|
-
font-size: var(--playpilot-playlinks-cta-font-size, var(--playpilot-playlinks-category-font-size, margin(0.625)));
|
|
228
|
-
color: var(--playpilot-playlink-cta-text-color, var(--playpilot-playlink-category-text-color, var(--playpilot-text-color)));
|
|
229
|
-
font-weight: var(--playpilot-playlink-cta-font-weight, var(--playpilot-playlink-category-font-weight, inherit));
|
|
230
|
-
font-family: var(--playpilot-playlink-cta-font-family, var(--playpilot-playlink-category-font-family, inherit));
|
|
231
98
|
}
|
|
232
99
|
|
|
233
100
|
.disclaimer {
|
|
@@ -249,17 +116,16 @@
|
|
|
249
116
|
}
|
|
250
117
|
|
|
251
118
|
.empty {
|
|
119
|
+
grid-column: span 2;
|
|
120
|
+
padding: margin(0.75);
|
|
121
|
+
background: var(--playpilot-playlink-background, var(--playpilot-lighter));
|
|
122
|
+
box-shadow: var(--playpilot-playlink-shadow, var(--playpilot-shadow));
|
|
123
|
+
border-radius: var(--playpilot-playlink-border-radius, margin(0.5));
|
|
252
124
|
white-space: initial;
|
|
253
125
|
line-height: 1.35;
|
|
254
|
-
}
|
|
255
126
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
padding: margin(0.5);
|
|
260
|
-
font-weight: var(--playpilot-playlinks-action-font-weight, 500);
|
|
261
|
-
color: var(--playpilot-playlinks-action-text-color, var(--playpilot-text-color));
|
|
262
|
-
border: var(--playpilot-playlinks-action-border, 1px solid currentColor);
|
|
263
|
-
border-radius: var(--playpilot-playlinks-action-border-radius, margin(2));
|
|
127
|
+
.list & {
|
|
128
|
+
grid-column: 1;
|
|
129
|
+
}
|
|
264
130
|
}
|
|
265
131
|
</style>
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
3
|
+
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
2
4
|
import { onMount, setContext, tick, type Snippet } from 'svelte'
|
|
3
5
|
import { prefersReducedMotion } from 'svelte/motion'
|
|
4
6
|
import { fly } from 'svelte/transition'
|
|
5
7
|
|
|
6
8
|
interface Props {
|
|
7
9
|
children: Snippet
|
|
10
|
+
bubble?: Snippet | null
|
|
8
11
|
maxHeight?: number
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
let { children, maxHeight = $bindable() }: Props = $props()
|
|
14
|
+
let { children, bubble, maxHeight = $bindable() }: Props = $props()
|
|
12
15
|
|
|
13
16
|
setContext('scope', 'popover')
|
|
14
17
|
|
|
18
|
+
const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
19
|
+
|
|
15
20
|
let element: HTMLElement | null = $state(null)
|
|
16
21
|
let flip = $state(false)
|
|
17
22
|
|
|
@@ -59,15 +64,24 @@
|
|
|
59
64
|
}
|
|
60
65
|
</script>
|
|
61
66
|
|
|
62
|
-
<div class="popover" class:flip bind:this={element} style:--max-height={maxHeight ? maxHeight + 'px' : null} tabindex="-1" aria-hidden="true">
|
|
67
|
+
<div class="popover" class:flip class:inline={inlineBubble} class:has-bubble={!!bubble && inlineBubble} bind:this={element} style:--max-height={maxHeight ? maxHeight + 'px' : null} tabindex="-1" aria-hidden="true">
|
|
63
68
|
<div class="dialog" transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }} data-view-transition-old>
|
|
64
69
|
{@render children()}
|
|
65
70
|
</div>
|
|
71
|
+
|
|
72
|
+
{#if bubble}
|
|
73
|
+
<div transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }}>
|
|
74
|
+
{@render bubble()}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
66
77
|
</div>
|
|
67
78
|
|
|
68
79
|
<style lang="scss">
|
|
69
80
|
.popover {
|
|
70
81
|
--offset: #{margin(0.5)};
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column-reverse;
|
|
84
|
+
gap: margin(0.5);
|
|
71
85
|
position: absolute;
|
|
72
86
|
top: calc((var(--offset) - 1px) * -1); /* Add 1 pixel to account for rounding errors */
|
|
73
87
|
left: 0;
|
|
@@ -78,9 +92,18 @@
|
|
|
78
92
|
z-index: 2147483647; // As high as she goes
|
|
79
93
|
|
|
80
94
|
&.flip {
|
|
95
|
+
flex-direction: column;
|
|
81
96
|
top: auto;
|
|
82
97
|
bottom: calc(var(--offset) + 1px); /* Add 1 pixel to account for rounding errors */
|
|
83
98
|
transform: translateY(calc(100% + var(--offset)));
|
|
99
|
+
|
|
100
|
+
&.inline {
|
|
101
|
+
flex-direction: column-reverse;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&.inline {
|
|
106
|
+
gap: 0;
|
|
84
107
|
}
|
|
85
108
|
}
|
|
86
109
|
|
|
@@ -96,5 +119,10 @@
|
|
|
96
119
|
overflow-y: overlay;
|
|
97
120
|
overflow-x: hidden;
|
|
98
121
|
view-transition-name: playpilot-title-content;
|
|
122
|
+
|
|
123
|
+
.has-bubble & {
|
|
124
|
+
border-top-left-radius: 0;
|
|
125
|
+
border-top-right-radius: 0;
|
|
126
|
+
}
|
|
99
127
|
}
|
|
100
128
|
</style>
|
|
@@ -7,28 +7,21 @@
|
|
|
7
7
|
import type { TitleData } from '$lib/types/title'
|
|
8
8
|
import { heading } from '$lib/actions/heading'
|
|
9
9
|
import { removeImageUrlPrefix } from '$lib/image'
|
|
10
|
-
import type { Snippet } from 'svelte'
|
|
11
|
-
import FloatingPlaylink from './Ads/FloatingPlaylink.svelte'
|
|
12
|
-
import VideoBackground from './Ads/VideoBackground.svelte'
|
|
13
10
|
|
|
14
11
|
interface Props {
|
|
15
12
|
title: TitleData
|
|
16
13
|
small?: boolean
|
|
17
14
|
compact?: boolean
|
|
18
|
-
useVideoBackground?: boolean
|
|
19
|
-
backgroundFormat?: '' | 'landscape' | 'portrait'
|
|
20
|
-
children?: Snippet
|
|
21
|
-
background?: Snippet | null
|
|
22
15
|
}
|
|
23
16
|
|
|
24
|
-
const { title, small = false, compact = false
|
|
17
|
+
const { title, small = false, compact = false }: Props = $props()
|
|
25
18
|
|
|
26
19
|
let posterLoaded = $state(false)
|
|
27
20
|
let backgroundLoaded = $state(false)
|
|
28
21
|
let useBackgroundFallback = $state(false)
|
|
29
22
|
</script>
|
|
30
23
|
|
|
31
|
-
<div class="content
|
|
24
|
+
<div class="content" class:small class:compact data-playpilot-link-injections-title>
|
|
32
25
|
<div class="header">
|
|
33
26
|
{#if !compact}
|
|
34
27
|
<div class="top">
|
|
@@ -67,26 +60,18 @@
|
|
|
67
60
|
<Description text={title.description} blurb={title.blurb} />
|
|
68
61
|
{/if}
|
|
69
62
|
</div>
|
|
70
|
-
|
|
71
|
-
{#if useVideoBackground}
|
|
72
|
-
<FloatingPlaylink playlink={title.providers.find(p => p.name === 'Apple TV+')!} />
|
|
73
|
-
{/if}
|
|
74
63
|
</div>
|
|
75
64
|
|
|
76
|
-
<div class="background
|
|
77
|
-
{#
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
onload={() => backgroundLoaded = true}
|
|
87
|
-
onerror={() => useBackgroundFallback = true} />
|
|
88
|
-
{/key}
|
|
89
|
-
{/if}
|
|
65
|
+
<div class="background" class:faded={compact}>
|
|
66
|
+
{#key useBackgroundFallback}
|
|
67
|
+
<img
|
|
68
|
+
src={removeImageUrlPrefix(useBackgroundFallback ? title.standing_poster : title.medium_poster)}
|
|
69
|
+
alt=""
|
|
70
|
+
class:loaded={backgroundLoaded}
|
|
71
|
+
class:blur={useBackgroundFallback}
|
|
72
|
+
onload={() => backgroundLoaded = true}
|
|
73
|
+
onerror={() => useBackgroundFallback = true} />
|
|
74
|
+
{/key}
|
|
90
75
|
</div>
|
|
91
76
|
|
|
92
77
|
<style lang="scss">
|
|
@@ -142,14 +127,6 @@
|
|
|
142
127
|
.compact & {
|
|
143
128
|
padding: 0 0 margin(1);
|
|
144
129
|
}
|
|
145
|
-
|
|
146
|
-
.landscape & {
|
|
147
|
-
padding-top: margin(6);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.portrait & {
|
|
151
|
-
padding-top: margin(12);
|
|
152
|
-
}
|
|
153
130
|
}
|
|
154
131
|
|
|
155
132
|
.top {
|
|
@@ -206,6 +183,7 @@
|
|
|
206
183
|
left: 0;
|
|
207
184
|
width: 100%;
|
|
208
185
|
height: margin(12);
|
|
186
|
+
border-radius: var(--playpilot-detail-background-border-radius);
|
|
209
187
|
overflow: hidden;
|
|
210
188
|
background: var(--playpilot-detail-background, var(--playpilot-lighter));
|
|
211
189
|
z-index: 0;
|
|
@@ -222,14 +200,6 @@
|
|
|
222
200
|
z-index: 1;
|
|
223
201
|
}
|
|
224
202
|
|
|
225
|
-
&.landscape {
|
|
226
|
-
height: margin(18);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
&.portrait {
|
|
230
|
-
height: margin(24);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
203
|
img {
|
|
234
204
|
width: 100%;
|
|
235
205
|
height: 100%;
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
3
|
import { track } from '$lib/tracking'
|
|
4
4
|
import type { TitleData } from '$lib/types/title'
|
|
5
|
-
import { trackSplitTestAction } from '$lib/splitTest'
|
|
6
5
|
import { onMount } from 'svelte'
|
|
7
6
|
import Modal from './Modal.svelte'
|
|
8
7
|
import Title from './Title.svelte'
|
|
8
|
+
import TopScroll from './Ads/TopScroll.svelte'
|
|
9
|
+
import Display from './Ads/Display.svelte'
|
|
10
|
+
import { getFirstAdOfType } from '$lib/ads'
|
|
11
|
+
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
12
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
9
13
|
|
|
10
14
|
interface Props {
|
|
11
15
|
onclose: () => void,
|
|
@@ -14,20 +18,18 @@
|
|
|
14
18
|
|
|
15
19
|
const { onclose, title }: Props = $props()
|
|
16
20
|
|
|
21
|
+
const topScrollAd = getFirstAdOfType('top_scroll')
|
|
22
|
+
const displayAd = getFirstAdOfType('card')
|
|
23
|
+
|
|
24
|
+
const isInDisplayAdFormatSplitTest = isInSplitTestVariant(SplitTest.DisplayAdPosition, 1) || isInSplitTestVariant(SplitTest.DisplayAdPosition, 2)
|
|
25
|
+
|
|
17
26
|
track(TrackingEvent.TitleModalView, title)
|
|
18
27
|
|
|
19
28
|
let hasTrackedScrolling = false
|
|
20
29
|
|
|
21
|
-
// This is all placeholder stuff that hardcodes a video ad for Apple TV+.
|
|
22
|
-
const useVideoBackground = title.providers.some(p => p.name === 'Apple TV+')
|
|
23
|
-
const backgroundFormat = useVideoBackground ? (Math.random() > 0.5 ? 'landscape' : 'portrait') : ''
|
|
24
|
-
const mobileSize = useVideoBackground ? (backgroundFormat === 'landscape' ? 'tall' : 'taller') : ''
|
|
25
|
-
|
|
26
30
|
onMount(() => {
|
|
27
31
|
const openTimestamp = Date.now()
|
|
28
32
|
|
|
29
|
-
trackSplitTestAction('initial_test', 'modal')
|
|
30
|
-
|
|
31
33
|
return () => track(TrackingEvent.TitleModalClose, title, { time_spent: Date.now() - openTimestamp })
|
|
32
34
|
})
|
|
33
35
|
|
|
@@ -39,6 +41,18 @@
|
|
|
39
41
|
}
|
|
40
42
|
</script>
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
{#snippet bubble()}
|
|
45
|
+
{#if topScrollAd}
|
|
46
|
+
<TopScroll campaign={topScrollAd} />
|
|
47
|
+
{/if}
|
|
48
|
+
{/snippet}
|
|
49
|
+
|
|
50
|
+
{#snippet prepend()}
|
|
51
|
+
{#if displayAd}
|
|
52
|
+
<Display campaign={displayAd} />
|
|
53
|
+
{/if}
|
|
54
|
+
{/snippet}
|
|
55
|
+
|
|
56
|
+
<Modal {onclose} {onscroll} prepend={isInDisplayAdFormatSplitTest ? prepend : null} bubble={topScrollAd ? bubble : null}>
|
|
57
|
+
<Title {title} />
|
|
44
58
|
</Modal>
|