@playpilot/tpi 5.7.0-beta.display.1 → 5.7.0-beta.video-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 +7 -7
- package/package.json +1 -1
- package/src/lib/ads.ts +4 -41
- package/src/lib/types/campaign.d.ts +1 -1
- package/src/lib/types/playlink.d.ts +2 -0
- package/src/routes/components/Ads/Display.svelte +36 -17
- package/src/routes/components/Modal.svelte +7 -4
- package/src/routes/components/Playlink.svelte +3 -3
- package/src/routes/components/Playlinks.svelte +1 -1
- package/src/routes/components/TitleModal.svelte +1 -1
- package/src/tests/routes/components/Ads/Display.test.js +50 -0
- package/src/tests/routes/components/Playlink.test.js +7 -0
- package/src/tests/routes/components/TitleModal.test.js +1 -1
package/package.json
CHANGED
package/src/lib/ads.ts
CHANGED
|
@@ -9,45 +9,7 @@ export async function fetchAds() {
|
|
|
9
9
|
|
|
10
10
|
if (!apiToken) throw new Error('No token was provided')
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
campaign_format: 'display',
|
|
14
|
-
campaign_type: 'video',
|
|
15
|
-
campaign_platforms: ['web', 'android_app', 'ios_app'],
|
|
16
|
-
campaign_name: 'some_campaign_NL',
|
|
17
|
-
campaign_start: '2025-04-24T00:00:00Z',
|
|
18
|
-
campaign_end: '2025-04-30T23:59:00Z',
|
|
19
|
-
campaign_region: 'nl',
|
|
20
|
-
content: {
|
|
21
|
-
header: 'Some top scroll header',
|
|
22
|
-
header_logo: 'https://example.com/',
|
|
23
|
-
header_logo_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
24
|
-
subheader: null,
|
|
25
|
-
image: 'https://example.com/',
|
|
26
|
-
image_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
27
|
-
video: null,
|
|
28
|
-
format: null,
|
|
29
|
-
},
|
|
30
|
-
cta: {
|
|
31
|
-
header: 'Button Label',
|
|
32
|
-
subheader: null,
|
|
33
|
-
image: null,
|
|
34
|
-
image_uuid: null,
|
|
35
|
-
url: 'https://google.com/',
|
|
36
|
-
},
|
|
37
|
-
content_playlist: null,
|
|
38
|
-
provider: null,
|
|
39
|
-
impression_trackers: [],
|
|
40
|
-
target_title_uuid: null,
|
|
41
|
-
target_title_sid: null,
|
|
42
|
-
backfill_providers: [],
|
|
43
|
-
autogenerated: false,
|
|
44
|
-
enabled: true,
|
|
45
|
-
hide_imdb_score: false,
|
|
46
|
-
hide_sponsored_message: false,
|
|
47
|
-
disclaimer: 'Some disclaimer',
|
|
48
|
-
}]
|
|
49
|
-
|
|
50
|
-
const response = await fetch(apiBaseUrl + `/ads/browse/?api-token=${apiToken}`, { headers })
|
|
12
|
+
const response = await fetch(apiBaseUrl + `/ads/browse/?region=nl&api-token=${apiToken}`, { headers })
|
|
51
13
|
|
|
52
14
|
if (!response.ok) throw response
|
|
53
15
|
|
|
@@ -63,11 +25,12 @@ export function getFirstAdOfType(format: CampaignFormat): Campaign | null {
|
|
|
63
25
|
export function campaignToPlaylink(campaign: Campaign): PlaylinkData {
|
|
64
26
|
return {
|
|
65
27
|
sid: '',
|
|
66
|
-
name: campaign.
|
|
28
|
+
name: campaign.content.header || '',
|
|
67
29
|
url: campaign.cta.url || '',
|
|
68
30
|
logo_url: campaign.cta.image || '',
|
|
69
31
|
highlighted: true,
|
|
70
|
-
cta_text: campaign.
|
|
32
|
+
cta_text: campaign.content.subheader,
|
|
33
|
+
action_text: campaign.cta.header,
|
|
71
34
|
extra_info: {
|
|
72
35
|
category: 'SVOD'
|
|
73
36
|
}
|
|
@@ -42,4 +42,4 @@ export type CampaignCTA = {
|
|
|
42
42
|
|
|
43
43
|
export type CampaignPlatform = 'web' | 'android_app' | 'ios_app'
|
|
44
44
|
|
|
45
|
-
export type CampaignFormat = 'takeover' | 'horizontal' | 'hero' | 'top_scroll' | '
|
|
45
|
+
export type CampaignFormat = 'takeover' | 'horizontal' | 'hero' | 'top_scroll' | 'card'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
3
3
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
4
|
-
import { trackSplitTestAction } from '$lib/splitTest'
|
|
4
|
+
import { trackSplitTestAction, trackSplitTestView } from '$lib/splitTest'
|
|
5
5
|
import { track } from '$lib/tracking'
|
|
6
6
|
import type { Campaign } from '$lib/types/campaign'
|
|
7
7
|
|
|
@@ -11,31 +11,50 @@
|
|
|
11
11
|
|
|
12
12
|
const { campaign }: Props = $props()
|
|
13
13
|
|
|
14
|
+
const { disclaimer, content, cta } = $derived(campaign)
|
|
15
|
+
const { header, subheader, header_logo: logo, image } = $derived(content)
|
|
16
|
+
const { header: buttonLabel, url: href } = $derived(cta)
|
|
17
|
+
|
|
18
|
+
trackSplitTestView(SplitTest.DisplayAdPosition)
|
|
19
|
+
|
|
14
20
|
function onclick(): void {
|
|
15
21
|
track(TrackingEvent.DisplayAdClick, null, { campaign_name: campaign.campaign_name })
|
|
16
22
|
trackSplitTestAction(SplitTest.DisplayAdPosition, 'click')
|
|
17
23
|
}
|
|
18
24
|
</script>
|
|
19
25
|
|
|
20
|
-
<a href
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<div class="cta">
|
|
26
|
-
<img class="logo" src="https://picsum.photos/seed/a/40/40" alt="" height="40" width="40" />
|
|
27
|
-
|
|
28
|
-
<div>
|
|
29
|
-
<span class="name">Apple TV+</span>
|
|
30
|
-
<div class="subheader">Get Apple TV+ free for 1 week</div>
|
|
26
|
+
<a {href} target="_blank" class="display" rel="sponsored" {onclick}>
|
|
27
|
+
{#if disclaimer}
|
|
28
|
+
<div class="disclaimer">
|
|
29
|
+
{disclaimer}
|
|
31
30
|
</div>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
{/if}
|
|
32
|
+
|
|
33
|
+
{#if logo || header || subheader || buttonLabel}
|
|
34
|
+
<div class="cta">
|
|
35
|
+
{#if logo}
|
|
36
|
+
<img class="logo" src={logo} alt="" height="40" width="40" />
|
|
37
|
+
{/if}
|
|
38
|
+
|
|
39
|
+
<div>
|
|
40
|
+
{#if header}
|
|
41
|
+
<span class="name">{header}</span>
|
|
42
|
+
{/if}
|
|
43
|
+
|
|
44
|
+
{#if subheader}
|
|
45
|
+
<div class="subheader">{subheader}</div>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{#if buttonLabel}
|
|
50
|
+
<div class="action">{buttonLabel}</div>
|
|
51
|
+
{/if}
|
|
35
52
|
</div>
|
|
36
|
-
|
|
53
|
+
{/if}
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
{#if image}
|
|
56
|
+
<img class="background" src={image} alt="" />
|
|
57
|
+
{/if}
|
|
39
58
|
</a>
|
|
40
59
|
|
|
41
60
|
<style lang="scss">
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
import DragHandle from './DragHandle.svelte'
|
|
6
6
|
import { onMount, setContext, type Snippet } from 'svelte'
|
|
7
7
|
import { prefersReducedMotion } from 'svelte/motion'
|
|
8
|
-
import { isInSplitTestVariant
|
|
8
|
+
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
9
9
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
10
|
-
import { getFirstAdOfType } from '$lib/ads'
|
|
11
10
|
|
|
12
11
|
interface Props {
|
|
13
12
|
children: Snippet
|
|
@@ -30,8 +29,6 @@
|
|
|
30
29
|
$effect(() => { if (windowWidth) dialogOffset = dialogElement?.offsetTop || 0 })
|
|
31
30
|
$effect(() => { setTimeout(() => dialogOffset = dialogElement?.offsetTop || 0) }) // Set after the dialog has shown to get the proper height
|
|
32
31
|
|
|
33
|
-
if (getFirstAdOfType('display')) trackSplitTestView(SplitTest.DisplayAdPosition)
|
|
34
|
-
|
|
35
32
|
setContext('scope', 'modal')
|
|
36
33
|
|
|
37
34
|
onMount(() => {
|
|
@@ -216,5 +213,11 @@
|
|
|
216
213
|
margin-top: 0;
|
|
217
214
|
}
|
|
218
215
|
}
|
|
216
|
+
|
|
217
|
+
.prepend + & {
|
|
218
|
+
&:not(.inline) {
|
|
219
|
+
margin-top: 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
219
222
|
}
|
|
220
223
|
</style>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const { playlink, hideCategory = false, onclick = () => null }: Props = $props()
|
|
13
13
|
|
|
14
|
-
const { name, url, logo_url, highlighted, cta_text, extra_info: { category } } = $derived(playlink)
|
|
14
|
+
const { name, url, logo_url, highlighted, cta_text, action_text, extra_info: { category } } = $derived(playlink)
|
|
15
15
|
|
|
16
16
|
const categoryStrings = {
|
|
17
17
|
SVOD: t('Stream'),
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
{/if}
|
|
37
37
|
|
|
38
38
|
<div class="action">
|
|
39
|
-
{t('Watch')}
|
|
39
|
+
{action_text || t('Watch')}
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
42
|
</a>
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
z-index: 5;
|
|
131
131
|
display: grid;
|
|
132
132
|
grid-template-areas: "image name action" "image category action";
|
|
133
|
-
grid-template-columns: $image-size auto margin(
|
|
133
|
+
grid-template-columns: $image-size auto margin(6);
|
|
134
134
|
align-items: center;
|
|
135
135
|
gap: 0 margin(0.75);
|
|
136
136
|
padding: margin(0.75);
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
const { playlinks, title }: Props = $props()
|
|
22
22
|
|
|
23
23
|
const isModal = getContext('scope') === 'modal'
|
|
24
|
-
const displayAd = getFirstAdOfType('
|
|
24
|
+
const displayAd = getFirstAdOfType('card')
|
|
25
25
|
const showDisplayAd = displayAd && isModal && isInSplitTestVariant(SplitTest.DisplayAdPosition, 0)
|
|
26
26
|
const showPlaylinkAd = displayAd && isModal && isInSplitTestVariant(SplitTest.DisplayAdPosition, 1)
|
|
27
27
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
const { onclose, title }: Props = $props()
|
|
20
20
|
|
|
21
21
|
const topScrollAd = getFirstAdOfType('top_scroll')
|
|
22
|
-
const displayAd = getFirstAdOfType('
|
|
22
|
+
const displayAd = getFirstAdOfType('card')
|
|
23
23
|
|
|
24
24
|
const isInDisplayAdFormatSplitTest = isInSplitTestVariant(SplitTest.DisplayAdPosition, 1) || isInSplitTestVariant(SplitTest.DisplayAdPosition, 2)
|
|
25
25
|
|
|
@@ -56,4 +56,54 @@ describe('Display.svelte', () => {
|
|
|
56
56
|
|
|
57
57
|
expect(track).toHaveBeenCalledWith(TrackingEvent.DisplayAdClick, null, { campaign_name: campaign.campaign_name })
|
|
58
58
|
})
|
|
59
|
+
|
|
60
|
+
it('Should render link without any content by default', () => {
|
|
61
|
+
/** @ts-ignore */
|
|
62
|
+
const { container } = render(Display, { campaign: { cta: {}, content: {} } })
|
|
63
|
+
|
|
64
|
+
expect(/** @type {HTMLAnchorElement} */ (container.querySelector('a'))).toBeTruthy()
|
|
65
|
+
expect(/** @type {HTMLAnchorElement} */ (container.querySelector('a')).children).toHaveLength(0)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('Should render disclaimer when given', () => {
|
|
69
|
+
/** @ts-ignore */
|
|
70
|
+
const { getByText } = render(Display, { campaign: { disclaimer: 'Some disclaimer', cta: {}, content: {} } })
|
|
71
|
+
|
|
72
|
+
expect(getByText('Some disclaimer')).toBeTruthy()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('Should render logo when given', () => {
|
|
76
|
+
/** @ts-ignore */
|
|
77
|
+
const { getByRole } = render(Display, { campaign: { cta: {}, content: { header_logo: 'test' } } })
|
|
78
|
+
|
|
79
|
+
expect(getByRole('presentation').getAttribute('src')).toBe('test')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('Should render background image when given', () => {
|
|
83
|
+
/** @ts-ignore */
|
|
84
|
+
const { getByRole } = render(Display, { campaign: { cta: {}, content: { image: 'test' } } })
|
|
85
|
+
|
|
86
|
+
expect(getByRole('presentation').getAttribute('src')).toBe('test')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('Should render header when given', () => {
|
|
90
|
+
/** @ts-ignore */
|
|
91
|
+
const { getByText } = render(Display, { campaign: { cta: {}, content: { header: 'Some header' } } })
|
|
92
|
+
|
|
93
|
+
expect(getByText('Some header')).toBeTruthy()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('Should render subheader when given', () => {
|
|
97
|
+
/** @ts-ignore */
|
|
98
|
+
const { getByText } = render(Display, { campaign: { cta: {}, content: { header: 'Some subheader' } } })
|
|
99
|
+
|
|
100
|
+
expect(getByText('Some subheader')).toBeTruthy()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('Should render button when cta header is given', () => {
|
|
104
|
+
/** @ts-ignore */
|
|
105
|
+
const { getByText } = render(Display, { campaign: { cta: { header: 'Some button label' }, content: {} } })
|
|
106
|
+
|
|
107
|
+
expect(getByText('Some button label')).toBeTruthy()
|
|
108
|
+
})
|
|
59
109
|
})
|
|
@@ -85,4 +85,11 @@ describe('Playlink.svelte', () => {
|
|
|
85
85
|
expect(getByText('Some playlink').closest('.playlink')?.classList).not.toContain('highlighted')
|
|
86
86
|
expect(queryByText('Some CTA')).not.toBeTruthy()
|
|
87
87
|
})
|
|
88
|
+
|
|
89
|
+
it('Should use action_text in button label when given', () => {
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
const { getByText } = render(Playlink, { playlink: { name: 'Some highlighted playlink', action_text: 'Some action text', extra_info: { category: 'BUY' } } })
|
|
92
|
+
|
|
93
|
+
expect(getByText('Some action text')).toBeTruthy()
|
|
94
|
+
})
|
|
88
95
|
})
|
|
@@ -66,7 +66,7 @@ describe('TitleModal.svelte', () => {
|
|
|
66
66
|
|
|
67
67
|
it('Should render display ad when given', () => {
|
|
68
68
|
// @ts-ignore
|
|
69
|
-
window.PlayPilotLinkInjections = { ads: [{ campaign_format: '
|
|
69
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'card', content: {}, cta: {} }] }
|
|
70
70
|
|
|
71
71
|
const { container } = render(TitleModal, { onclose, title })
|
|
72
72
|
|