@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.
Files changed (38) hide show
  1. package/dist/link-injections.js +9 -9
  2. package/events.md +8 -2
  3. package/package.json +1 -1
  4. package/src/lib/ads.ts +38 -0
  5. package/src/lib/constants.ts +2 -1
  6. package/src/lib/enums/ImageDimensions.ts +7 -0
  7. package/src/lib/enums/SplitTest.ts +23 -0
  8. package/src/lib/enums/TrackingEvent.ts +3 -0
  9. package/src/lib/image.ts +12 -0
  10. package/src/lib/linkInjection.ts +36 -2
  11. package/src/lib/splitTest.ts +19 -14
  12. package/src/lib/types/campaign.d.ts +45 -0
  13. package/src/lib/types/playlink.d.ts +2 -0
  14. package/src/lib/types/script.d.ts +5 -0
  15. package/src/main.ts +6 -0
  16. package/src/routes/+layout.svelte +4 -0
  17. package/src/routes/+page.svelte +13 -2
  18. package/src/routes/components/Ads/Display.svelte +140 -0
  19. package/src/routes/components/Ads/TopScroll.svelte +240 -0
  20. package/src/routes/components/DragHandle.svelte +105 -0
  21. package/src/routes/components/Modal.svelte +125 -30
  22. package/src/routes/components/Playlink.svelte +185 -0
  23. package/src/routes/components/Playlinks.svelte +34 -168
  24. package/src/routes/components/Popover.svelte +30 -2
  25. package/src/routes/components/Title.svelte +13 -43
  26. package/src/routes/components/TitleModal.svelte +24 -10
  27. package/src/routes/components/TitlePopover.svelte +12 -6
  28. package/src/tests/lib/ads.test.js +55 -0
  29. package/src/tests/lib/splitTest.test.js +97 -18
  30. package/src/tests/routes/+page.test.js +33 -0
  31. package/src/tests/routes/components/Ads/Display.test.js +109 -0
  32. package/src/tests/routes/components/Ads/TopScroll.test.js +104 -0
  33. package/src/tests/routes/components/Playlink.test.js +95 -0
  34. package/src/tests/routes/components/Playlinks.test.js +0 -30
  35. package/src/tests/routes/components/TitleModal.test.js +36 -0
  36. package/src/tests/routes/components/TitlePopover.test.js +20 -0
  37. package/src/routes/components/Ads/FloatingPlaylink.svelte +0 -62
  38. package/src/routes/components/Ads/VideoBackground.svelte +0 -28
package/events.md CHANGED
@@ -64,5 +64,11 @@ Event | Action | Info | Payload
64
64
  ### Split Testing
65
65
  Event | Action | Info | Payload
66
66
  --- | --- | --- | ---
67
- `ali_split_test_view` | _Should be fired for any active split test_ | | `key`, `variant` (0 or 1)
68
- `ali_split_test_action` | _Should be fired for any assertion in split tests_ | | `key` (matches the key of `ali_split_test_view`), `variant` (0 or 1), `action`
67
+ `ali_split_test_view` | _Should be fired for any active split test_ | | `key`, `variant` (a whole number starting at 0)
68
+ `ali_split_test_action` | _Should be fired for any assertion in split tests_ | | `key` (matches the key of `ali_split_test_view`), `variant` (a whole number starting at 0), `action`
69
+
70
+ ### Ads
71
+ Event | Action | Info | Payload
72
+ --- | --- | --- | ---
73
+ `ali_top_scroll_click` | _Fired any time the top scroll ad is called_ | | `campaign_name`
74
+ `ali_display_ad_click` | _Fired any time the top scroll ad is clicked_ | | `campaign_name`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.6.0-beta.video.7",
3
+ "version": "5.7.0-beta.display-2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
package/src/lib/ads.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { getApiToken } from "./api"
2
+ import { apiBaseUrl } from "./constants"
3
+ import type { Campaign, CampaignFormat } from "./types/campaign"
4
+ import type { PlaylinkData } from "./types/playlink"
5
+
6
+ export async function fetchAds() {
7
+ const headers = new Headers({ 'Content-Type': 'application/json' })
8
+ const apiToken = getApiToken()
9
+
10
+ if (!apiToken) throw new Error('No token was provided')
11
+
12
+ const response = await fetch(apiBaseUrl + `/ads/browse/?region=nl&api-token=${apiToken}`, { headers })
13
+
14
+ if (!response.ok) throw response
15
+
16
+ const parsed = await response.json()
17
+
18
+ return parsed
19
+ }
20
+
21
+ export function getFirstAdOfType(format: CampaignFormat): Campaign | null {
22
+ return (window.PlayPilotLinkInjections?.ads || []).find(i => i.campaign_format === format) || null
23
+ }
24
+
25
+ export function campaignToPlaylink(campaign: Campaign): PlaylinkData {
26
+ return {
27
+ sid: '',
28
+ name: campaign.content.header || '',
29
+ url: campaign.cta.url || '',
30
+ logo_url: campaign.cta.image || '',
31
+ highlighted: true,
32
+ cta_text: campaign.content.subheader,
33
+ action_text: campaign.cta.header,
34
+ extra_info: {
35
+ category: 'SVOD'
36
+ }
37
+ }
38
+ }
@@ -1,4 +1,5 @@
1
1
  export const playPilotBaseUrl = 'https://www.playpilot.com'
2
- export const apiBaseUrl = 'https://partner-api.playpilot.tech/1.0.0'
2
+ export const apiBaseUrl = 'https://partner-api-staging.playpilot.tech/1.0.0'
3
+ export const imageBaseUrl = 'https://img-staging.playpilot.tech'
3
4
 
4
5
  export const imagePlaceholderDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFQAAAB+CAMAAACNgsajAAAAb1BMVEU1Q2fI1N5YZoNVYoFndI9qd5JBT3FFU3S8yNS3xNC4xNFBTnDG0t2lscJJV3e9ytaptcWToLOOm6/BzdiZprh3hJ1aaIWJlatte5VPXHx7iJ9zgZlfbYk3RWmNmq5jcY1XZIO2wtCjsMB+i6I9S25IprTeAAABqUlEQVRo3u3W2ZKCMBCF4T5ExRmRVXBfZnn/Z5wKiU5pz1AJ7ZXV/x03HxVCB0jTNE3TNE3TNO0l2rbPNxcFdk8334AINTUD5eSaWbPoQs0qw0CVN98BzNNgE4NVv+ZHsJliuNVt7UgotATAafp/5mZiG4waAB0N5k0kUeg0wMykKDfLvRTl5pxyCFFupjQVo9ykiRTlphzl5nNQNu8C9Hv2lylDT0W2NMyUoeXdLC68KUNLuM7O9HskQ0uLLAEUR2aOQjfORE5LzHP2PMehxpl2k6otM8eh2aQGkBlieyRBYbs3y5Rk6IPZn8mT65XJR6LcROGErwaoxqLm4XvkiTVsy1FoYe5n06zcjazp1Wk0umHz3k9BT6+bXjXR6PnRtKpr755PfRjx4WPz7tXW/26gGYnOvOmrM7xtiK4qYtDOrpGZtnR7JFcSi+Z1XZt7k5d4NCZmcrWxqMzkbRgqN+nAULHpx1RiylFvftJwlxjUz2bWdpPB7NnSxZih5RFrD20Vai4izGOgeenPukMSUE6hte5denI7NMyU1xrSNE3TNE3TNE17hX4ADHsS0j0OCOoAAAAASUVORK5CYII='
@@ -0,0 +1,7 @@
1
+ // Image dimensions to be used to generate images from the supplied image UUIDs.
2
+ // The number given is a multiplier of the base 32 pixels.
3
+ // https://docs.google.com/spreadsheets/d/1Nx73ux1fgGKJfuaUeQRwSXUJdSQonPG5DxjW2zxnyz0
4
+ export const ImageDimensions = {
5
+ TopScrollBackground: 'topscrollBackgroundMobile', // 600x140
6
+ TopScrollContent: '8by1x23',
7
+ } as const
@@ -0,0 +1,23 @@
1
+ export const SplitTest = {
2
+ InitialTest: {
3
+ key: 'initial_test',
4
+ numberOfVariants: 2,
5
+ },
6
+ MultipleVariants: {
7
+ key: 'multiple_variants',
8
+ numberOfVariants: 4,
9
+ },
10
+ TopScrollFormat: {
11
+ key: 'top_scroll_format',
12
+ numberOfVariants: 2,
13
+ // Variant 0 is separate bubble
14
+ // Variant 1 is inline bubble
15
+ },
16
+ DisplayAdPosition: {
17
+ key: 'display_ad_position',
18
+ numberOfVariants: 3,
19
+ // Variant 0 is display ad in playlinks
20
+ // Variant 1 is display ad above card with highlighted playlink
21
+ // Variant 2 is display ad above card without highlighted playlink
22
+ },
23
+ } as const
@@ -26,6 +26,9 @@ export const TrackingEvent = Object.freeze({
26
26
  EditorError: 'ali_editor_error',
27
27
  InjectionError: 'ali_injection_error',
28
28
 
29
+ TopScrollClick: 'ali_top_scroll_click',
30
+ DisplayAdClick: 'ali_display_ad_click',
31
+
29
32
  SplitTestView: 'ali_split_test_view',
30
33
  SplitTestAction: 'ali_split_test_action'
31
34
  })
package/src/lib/image.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { imageBaseUrl } from "./constants"
2
+ import type { ImageDimensions } from "./enums/ImageDimensions"
3
+
1
4
  /**
2
5
  * NOTE: This is a temporary measure. Images url from the API use a previous format which is to be replaced,
3
6
  * but that isn't live yet and requires some extra work. In the meantime we remove part of the URL ourselves.
@@ -6,3 +9,12 @@ export function removeImageUrlPrefix(url: string | null): string | null {
6
9
  if (!url) return null
7
10
  return url.replace('/src/img', '')
8
11
  }
12
+
13
+ /**
14
+ * Creates a CDN image URL from a UUID.
15
+ */
16
+ export function imageFromUUID(uuid: string | null, dimensions: (typeof ImageDimensions)[keyof typeof ImageDimensions] | null = null): string {
17
+ if (!uuid?.length) return ''
18
+
19
+ return `${imageBaseUrl}/${uuid}?${dimensions ? `class=${dimensions}&` : ''}optimizer=image`
20
+ }
@@ -219,7 +219,9 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
219
219
  if (replacementIndex === -1) continue
220
220
  if (hasBeenReplaced) continue
221
221
 
222
- element.innerHTML = replaceStartingFrom(element.innerHTML, injection.title, injectionElement.outerHTML, replacementIndex)
222
+ if (!replaceIfSafeInjection(element.innerHTML, injection.title, element, injectionElement, replacementIndex)) {
223
+ failedMessages[injection.key] = 'Injection would have lead to broken HTML.'
224
+ }
223
225
  }
224
226
 
225
227
  addLinkInjectionEventListeners(validInjections)
@@ -267,6 +269,31 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
267
269
  return { injectionElement, linkElement }
268
270
  }
269
271
 
272
+ /**
273
+ * In some coses injections lead to broken HTML. The reason for this varies and is hard to figure out. In any case, we
274
+ * should never serve broken HTML.
275
+ * Before replacing the HTML, we apply the the replacement to a dummy element. From here we check if the number of links
276
+ * in the HTML increased by one. If it didn't, something went wrong. What exactly is hard to say, we just know something
277
+ * didn't go as intended and we should not inject.
278
+ *
279
+ * No tests exists for this function because writing tests would require finding a scenario in which things break. If we
280
+ * knew when things break we'd fix it instead.
281
+ */
282
+ function replaceIfSafeInjection(originalHtml: string, phrase: string, sentenceElement: HTMLElement, injectionElement: HTMLElement, replacementIndex: number): boolean {
283
+ const dummyElement = document.createElement('div')
284
+ dummyElement.innerHTML = originalHtml
285
+
286
+ const startingNumberOfLinks = Array.from(dummyElement.querySelectorAll<HTMLAnchorElement>('a')).filter(a => a.innerText).length
287
+ dummyElement.innerHTML = replaceStartingFrom(originalHtml, phrase, injectionElement.outerHTML, replacementIndex)
288
+ const finalNumberOfLinks = Array.from(dummyElement.querySelectorAll<HTMLAnchorElement>('a')).filter(a => a.innerText).length
289
+
290
+ if (finalNumberOfLinks != startingNumberOfLinks + 1) return false
291
+
292
+ sentenceElement.innerHTML = dummyElement.innerHTML
293
+
294
+ return true
295
+ }
296
+
270
297
  /**
271
298
  * Add all used CSS variables to a data attribute. This data attribute is then used for selectors that for each
272
299
  * individual CSS variable. This is done this way so that CSS variables are only set when they are used.
@@ -399,7 +426,14 @@ function openLinkPopover(event: MouseEvent, injection: LinkInjection): void {
399
426
  * Unmount the popover, removing it from the dom
400
427
  */
401
428
  function destroyLinkPopover(outro: boolean = true) {
402
- if (!activePopoverInsertedComponent) return
429
+ if (!activePopoverInsertedComponent) {
430
+ // In some cases a popover lingers even if it should have been removed. This happens sometimes during
431
+ // HMR during development. In that case we remove the element straight from the dom.
432
+ // Doing this will prevent the outro animation from playing, but this being a fallback, that's ok.
433
+ document.querySelectorAll<HTMLElement>('[data-playpilot-title-popover]').forEach(element => element.remove())
434
+
435
+ return
436
+ }
403
437
 
404
438
  unmount(activePopoverInsertedComponent, { outro })
405
439
 
@@ -1,11 +1,16 @@
1
1
  import { TrackingEvent } from "./enums/TrackingEvent"
2
2
  import { track } from "./tracking"
3
3
 
4
+ type SplitTest = {
5
+ key: string
6
+ numberOfVariants: number
7
+ }
8
+
4
9
  /**
5
10
  * Each split test uses a different split test identifier so a user can be part of one, multiple, or none, tests.
6
11
  * The identifier is saved on the window object so that it is consistent across this session.
7
12
  */
8
- export function getSplitTestIdentifier(key: string): number {
13
+ export function getSplitTestIdentifier({ key }: SplitTest): number {
9
14
  const windowIdentifier = window.PlayPilotLinkInjections?.split_test_identifiers?.[key]
10
15
  if (windowIdentifier) return windowIdentifier
11
16
 
@@ -17,24 +22,24 @@ export function getSplitTestIdentifier(key: string): number {
17
22
  return randomIdentifier
18
23
  }
19
24
 
20
- /**
21
- * Split tests are either on or off with a 50/50 split. That's all we need for now.
22
- */
23
- export function isInSplitTest(key: string): boolean {
24
- const identifier = getSplitTestIdentifier(key)
25
- const isInSplitTest = identifier > 0.5
25
+ export function getSplitTestVariantIndex(test: SplitTest): number {
26
+ const identifier = getSplitTestIdentifier(test)
27
+
28
+ return Math.floor(identifier * test.numberOfVariants)
29
+ }
26
30
 
27
- return isInSplitTest
31
+ export function isInSplitTestVariant(test: SplitTest, variant = 1): boolean {
32
+ return getSplitTestVariantIndex(test) === variant
28
33
  }
29
34
 
30
- export function trackSplitTestView(key: string): void {
31
- const variant = isInSplitTest(key) ? 1 : 0
35
+ export function trackSplitTestView(test: SplitTest): void {
36
+ const variant = getSplitTestVariantIndex(test)
32
37
 
33
- track(TrackingEvent.SplitTestView, null, { key, variant })
38
+ track(TrackingEvent.SplitTestView, null, { key: test.key, variant })
34
39
  }
35
40
 
36
- export function trackSplitTestAction(key: string, action: string): void {
37
- const variant = isInSplitTest(key) ? 1 : 0
41
+ export function trackSplitTestAction(test: SplitTest, action: string): void {
42
+ const variant = getSplitTestVariantIndex(test) ? 1 : 0
38
43
 
39
- track(TrackingEvent.SplitTestAction, null, { key, variant, action })
44
+ track(TrackingEvent.SplitTestAction, null, { key: test.key, variant, action })
40
45
  }
@@ -0,0 +1,45 @@
1
+ export type Campaign = {
2
+ campaign_format: CampaignFormat
3
+ campaign_type: 'playlist' | 'video' | 'image'
4
+ campaign_platforms: CampaignPlatform[]
5
+ campaign_name: string
6
+ campaign_start: string
7
+ campaign_end: string
8
+ campaign_region: string
9
+ content: CampaignContent
10
+ cta: CampaignCTA
11
+ content_playlist: object
12
+ provider: object | null
13
+ impression_trackers: string[]
14
+ target_title_uuid: string
15
+ target_title_sid: string
16
+ backfill_providers: string[]
17
+ autogenerated: boolean
18
+ enabled: boolean
19
+ hide_imdb_score: boolean
20
+ hide_sponsored_message: boolean
21
+ disclaimer: string | null
22
+ }
23
+
24
+ export type CampaignContent = {
25
+ header: string | null
26
+ header_logo: string | null
27
+ header_logo_uuid: string | null
28
+ subheader: string | null
29
+ image: string | null
30
+ image_uuid: string | null
31
+ video: string | null
32
+ format: string | null
33
+ }
34
+
35
+ export type CampaignCTA = {
36
+ header: string | null
37
+ subheader: string | null
38
+ image: string | null
39
+ image_uuid: string | null
40
+ url: string | null
41
+ }
42
+
43
+ export type CampaignPlatform = 'web' | 'android_app' | 'ios_app'
44
+
45
+ export type CampaignFormat = 'takeover' | 'horizontal' | 'hero' | 'top_scroll' | 'card'
@@ -5,6 +5,8 @@ export type PlaylinkData = {
5
5
  logo_url: string
6
6
  highlighted?: boolean
7
7
  cta_text?: string | null
8
+ // This doesn't actually exists on the API objects, it's only present from ads
9
+ action_text?: string | null,
8
10
  extra_info: {
9
11
  category: PlaylinkCategory
10
12
  }
@@ -1,3 +1,6 @@
1
+ import type { Campaign } from "./campaign"
2
+ import type { LinkInjection } from "./injection"
3
+
1
4
  export type ScriptConfig = {
2
5
  token: string
3
6
  editorial_token?: string
@@ -10,4 +13,6 @@ export type ScriptConfig = {
10
13
  last_successful_fetch?: LinkInjectionResponse | null
11
14
  tracked_events?: { event: string, payload: Record<string, any> }[]
12
15
  split_test_identifiers?: Record<string, number>
16
+ evaluated_link_injections?: LinkInjection[]
17
+ ads?: Campaign[]
13
18
  }
package/src/main.ts CHANGED
@@ -15,6 +15,8 @@ window.PlayPilotLinkInjections = {
15
15
  last_successful_fetch: null,
16
16
  tracked_events: [],
17
17
  split_test_identifiers: {},
18
+ evaluated_link_injections: [],
19
+ ads: [],
18
20
  app: null,
19
21
 
20
22
  initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }): void {
@@ -79,6 +81,10 @@ window.PlayPilotLinkInjections = {
79
81
  console.log(getPageText(elements))
80
82
  console.groupEnd()
81
83
 
84
+ console.groupCollapsed('Evaluated injections')
85
+ console.log(this.evaluated_link_injections)
86
+ console.groupEnd()
87
+
82
88
  console.groupCollapsed('Tracked events')
83
89
  console.log(this.tracked_events)
84
90
  console.groupEnd()
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { browser } from '$app/environment'
3
3
  import { page } from '$app/state'
4
+ import { SplitTest } from '$lib/enums/SplitTest'
5
+ import { getSplitTestVariantIndex } from '$lib/splitTest'
4
6
 
5
7
  /**
6
8
  * !! NOTE: This layout file is for development purposes only and will not be compiled with the final script.
@@ -75,6 +77,8 @@
75
77
 
76
78
  {#if browser}
77
79
  {@render children()}
80
+
81
+ Viewing with split test index: {getSplitTestVariantIndex(SplitTest.MultipleVariants)}
78
82
  {/if}
79
83
  {/key}
80
84
  {/if}
@@ -8,11 +8,13 @@
8
8
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
9
  import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/auth'
10
10
  import { trackSplitTestView } from '$lib/splitTest'
11
+ import { SplitTest } from '$lib/enums/SplitTest'
11
12
  import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
12
13
  import Editor from './components/Editorial/Editor.svelte'
13
14
  import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
14
15
  import Alert from './components/Editorial/Alert.svelte'
15
16
  import TrackingPixels from './components/TrackingPixels.svelte'
17
+ import { fetchAds } from '$lib/ads'
16
18
 
17
19
  let parentElement: HTMLElement | null = $state(null)
18
20
  let elements: HTMLElement[] = $state([])
@@ -42,7 +44,9 @@
42
44
  await initialize()
43
45
  track(TrackingEvent.ArticlePageView)
44
46
 
45
- trackSplitTestView('initial_test')
47
+ if (aiInjections.length || manualInjections.length) window.PlayPilotLinkInjections.ads = await fetchAds()
48
+
49
+ trackSplitTestView(SplitTest.MultipleVariants)
46
50
  })()
47
51
 
48
52
  return () => clearLinkInjections()
@@ -120,7 +124,14 @@
120
124
  // Get filtered injections as they are shown on the page.
121
125
  // Only update state if it they are different from current injections.
122
126
  const filteredInjections = injectLinksInDocument(elements, injections)
123
- if (JSON.stringify(filteredInjections) !== JSON.stringify(linkInjections)) linkInjections = filteredInjections
127
+ if (JSON.stringify(filteredInjections) !== JSON.stringify(linkInjections)) {
128
+ linkInjections = filteredInjections
129
+
130
+ // This is added to the window both for debugging purposes and for the benchmarking tool.
131
+ // Just to have some way to evaluate injections outside of editorial mode.
132
+ // This shouldn't be used anywhere else.
133
+ window.PlayPilotLinkInjections.evaluated_link_injections = filteredInjections
134
+ }
124
135
 
125
136
  const successfulInjections = filteredInjections.filter(i => !i.failed)
126
137
 
@@ -0,0 +1,140 @@
1
+ <script lang="ts">
2
+ import { SplitTest } from '$lib/enums/SplitTest'
3
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
4
+ import { trackSplitTestAction, trackSplitTestView } from '$lib/splitTest'
5
+ import { track } from '$lib/tracking'
6
+ import type { Campaign } from '$lib/types/campaign'
7
+
8
+ interface Props {
9
+ campaign: Campaign
10
+ }
11
+
12
+ const { campaign }: Props = $props()
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
+
20
+ function onclick(): void {
21
+ track(TrackingEvent.DisplayAdClick, null, { campaign_name: campaign.campaign_name })
22
+ trackSplitTestAction(SplitTest.DisplayAdPosition, 'click')
23
+ }
24
+ </script>
25
+
26
+ <a {href} target="_blank" class="display" rel="sponsored" {onclick}>
27
+ {#if disclaimer}
28
+ <div class="disclaimer">
29
+ {disclaimer}
30
+ </div>
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}
52
+ </div>
53
+ {/if}
54
+
55
+ {#if image}
56
+ <img class="background" src={image} alt="" />
57
+ {/if}
58
+ </a>
59
+
60
+ <style lang="scss">
61
+ .display {
62
+ display: block;
63
+ position: relative;
64
+ border-radius: var(--playpilot-display-ad-border-radius, margin(0.5));
65
+ overflow: hidden;
66
+ background: black;
67
+ aspect-ratio: 16 / 9;
68
+ color: white !important;
69
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.35);
70
+ font-family: var(--playpilot-detail-font-family, var(--playpilot-font-family));
71
+ line-height: 1.2;
72
+ font-style: normal !important;
73
+ }
74
+
75
+ .disclaimer {
76
+ position: absolute;
77
+ top: 0;
78
+ left: 0;
79
+ padding: margin(0.5);
80
+ font-size: margin(0.75);
81
+ text-decoration: underline;
82
+
83
+ @media (min-width: 600px) {
84
+ padding: margin(1);
85
+ }
86
+ }
87
+
88
+ .cta {
89
+ z-index: 1;
90
+ display: flex;
91
+ align-items: center;
92
+ gap: margin(0.5);
93
+ position: absolute;
94
+ right: 0;
95
+ bottom: 0;
96
+ left: 0;
97
+ padding: margin(0.5);
98
+ background: linear-gradient(to right, rgba(110, 110, 110, 1), rgba(85, 85, 85, 0.3));
99
+ font-size: margin(0.875);
100
+ text-decoration: none;
101
+
102
+ @media (min-width: 600px) {
103
+ padding: margin(1);
104
+ }
105
+ }
106
+
107
+ .logo {
108
+ border-radius: var(--playpilot-display-ad-border-radius, margin(0.5));
109
+ }
110
+
111
+ .name {
112
+ font-weight: 500;
113
+ }
114
+
115
+ .subheader {
116
+ font-size: margin(0.75);
117
+ }
118
+
119
+ .action {
120
+ margin-left: auto;
121
+ padding: margin(0.5) margin(0.75);
122
+ background: var(--playpilot-display-ad-action-background, white);
123
+ border-radius: var(--playpilot-display-action-border-radius, margin(2));
124
+ transition: transform 50ms;
125
+ text-shadow: none;
126
+ font-weight: var(--playpilot-display-action-font-weight, 500);
127
+ color: var(--playpilot-display-action-text-color, black);
128
+ white-space: nowrap;
129
+
130
+ &:hover {
131
+ transform: scale(1.05);
132
+ }
133
+ }
134
+
135
+ .background {
136
+ display: block;
137
+ width: 100%;
138
+ height: auto;
139
+ }
140
+ </style>