@playpilot/tpi 5.2.1 → 5.4.0-beta.topscroll-1
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 +60 -0
- package/package.json +1 -1
- package/src/lib/api.ts +3 -0
- package/src/lib/enums/TrackingEvent.ts +5 -0
- package/src/lib/linkInjection.ts +4 -0
- package/src/lib/tracking.ts +8 -0
- package/src/lib/types/global.d.ts +1 -0
- package/src/lib/types/script.d.ts +2 -0
- package/src/lib/types/title.d.ts +1 -1
- package/src/main.ts +36 -1
- package/src/routes/+page.svelte +2 -2
- package/src/routes/components/Ads/TopScroll.svelte +251 -0
- package/src/routes/components/Modal.svelte +33 -11
- package/src/routes/components/Popover.svelte +12 -1
- package/src/routes/components/TitleModal.svelte +11 -0
- package/src/routes/components/TitlePopover.svelte +11 -1
- package/src/tests/routes/components/Ads/TopScroll.test.js +99 -0
- package/src/tests/routes/components/TitleModal.test.js +16 -1
- package/src/tests/routes/components/TitlePopover.test.js +17 -1
package/events.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
This document highlights the purpose of each tracking event.
|
|
2
|
+
|
|
3
|
+
## Payload
|
|
4
|
+
|
|
5
|
+
All events share a common payload:
|
|
6
|
+
|
|
7
|
+
- `url`: The URL of the related article. This is a URL with only the protocol, base url, and pathname. No parameters are included
|
|
8
|
+
- `organization_sid`: The sid for the related organization
|
|
9
|
+
- `domain_sid`: The sid for the related domain
|
|
10
|
+
|
|
11
|
+
Events related to titles share an additional set of data (referred to below as `Title`):
|
|
12
|
+
|
|
13
|
+
- `original_title`
|
|
14
|
+
- `title_sid`
|
|
15
|
+
- `title_type`: "movie" or "series"
|
|
16
|
+
- `providers`: An array of provider names
|
|
17
|
+
|
|
18
|
+
Events may have additional data in their payload.
|
|
19
|
+
|
|
20
|
+
### General
|
|
21
|
+
Event | Action | Info | Payload
|
|
22
|
+
--- | --- | --- | ---
|
|
23
|
+
`ali_article_page_view` | _Fires any time an article is visited_ | This event will fire right after all data is fetched and will fire regardless of if there are injections or not | It will fire even on pages where injections are disabled. | -
|
|
24
|
+
`ali_links_injected` | _Fires as long as any injections are injected into the article_ | Includes an object with the number of injections for this article with `manual` and `ai` as two separate numbers. | `manual` (number of manual injection), `ai` (number of ai injections)
|
|
25
|
+
|
|
26
|
+
### Modal
|
|
27
|
+
Event | Action | Info | Payload
|
|
28
|
+
--- | --- | --- | ---
|
|
29
|
+
`ali_title_modal_view` | _Fires any time a title modal is viewed_ | The title modal opens when clicking an injection both on desktop and mobile | `Title`
|
|
30
|
+
`ali_title_modal_scroll` | _Fires the first time a user scrolls inside of a titel modal._ | | `Title`
|
|
31
|
+
`ali_title_modal_playlink_click` | _Fires any time a playlink is clicked inside of a title modal_ | Includes data on which playlink was clicked. | `Title`, `playlink` (name of the clicked playlink)
|
|
32
|
+
`ali_title_modal_save_click` | _Currently unused, there is no save functionality._ | | `Title`
|
|
33
|
+
|
|
34
|
+
### Popover
|
|
35
|
+
Event | Action | Info | Payload
|
|
36
|
+
--- | --- | --- | ---
|
|
37
|
+
`ali_title_popover_view` | _Fires any time a title popover is viewed_ | The title popover opens when hovering an injection. This will only occur on desktop. | `Title`
|
|
38
|
+
`ali_title_popover_save_click` | _Currently unused, there is no save functionality._ | | `Title`
|
|
39
|
+
`ali_title_popover_playlink_click` | _Fires any time a playlink is clicked inside of a title popover_ | Includes data on which playlink was clicked. | `Title`, `playlink` (name of the clicked playlink)
|
|
40
|
+
|
|
41
|
+
### After Article
|
|
42
|
+
Event | Action | Info | Payload
|
|
43
|
+
--- | --- | --- | ---
|
|
44
|
+
`ali_after_article_playlink_click` | _Fires any time a playlink is clicked inside of an after article block_ | This block will only appear injections are configured for after article, which no one is currently using. Includes data on which playlink was clicked. | `Title`, `playlink` (name of the clicked playlink)
|
|
45
|
+
`ali_after_article_modal_button_click` | _Fires any time an after article modal button is clicked_ | This button will only appear in after article blocks when configured, which, which no one is currently using | `Title`
|
|
46
|
+
|
|
47
|
+
### Data
|
|
48
|
+
Event | Action | Info | Payload
|
|
49
|
+
--- | --- | --- | ---
|
|
50
|
+
`ali_injection_failed` | _Fires only inside of the Editor for each injection that failed_ | Only includes visible failures, for instance, it will ignore failures because of already existing links. If a user is shown a message about a failed injection, this event will fire. Includes data on the phrase and sentence for the injection. | `Title`, `phrase`, `sentence`
|
|
51
|
+
`ali_injection_count` | _Fires the first time the Editor is shown_ | This logs the total amount of injections, the total amount of failed and manual injections, and the total amount of successful injections. | `total` (number of failed + successsful injections), `failed_automatic`, `failed_manual`, `final_injected` (number of successful injections)
|
|
52
|
+
`ali_fetch_config_failed` | _Fires whenever the config object failed to fetch_ | When this happens, injections are aborted.
|
|
53
|
+
`ali_auth_failed` | _Fires whenever authentication for the Editor fails._
|
|
54
|
+
|
|
55
|
+
### Reporting
|
|
56
|
+
Event | Action | Info | Payload
|
|
57
|
+
--- | --- | --- | ---
|
|
58
|
+
`ali_manual_report` | _Fires only through manual action when reporting issues with an injection via the Editor._ | | `Title`, `report_reason`, `sid` (of injection), `title` (of injection), `sentence`, `failed` (true or false), `failed_message` (reason for failure as given in the editor), `manual` (true or false)
|
|
59
|
+
`ali_editor_error` | _Fires whenever an error occurs within the Editor._ | | `Title`, `phrase`, `sentence`
|
|
60
|
+
`ali_injection_error` | _Fires whenever an error occurs during injection_ | This includes fetching the injections as well as actually injecting itself. Does not include fetching of the config object. | `message` (error message as given by the browser)
|
package/package.json
CHANGED
package/src/lib/api.ts
CHANGED
|
@@ -51,6 +51,9 @@ export async function fetchLinkInjections(
|
|
|
51
51
|
|
|
52
52
|
const parsed = await response.json()
|
|
53
53
|
|
|
54
|
+
// This is used when debugging (using window.PlayPilotLinkInjections.debug())
|
|
55
|
+
window.PlayPilotLinkInjections.last_successful_fetch = parsed
|
|
56
|
+
|
|
54
57
|
return parsed
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
/** @see /events.md */
|
|
1
2
|
export const TrackingEvent = Object.freeze({
|
|
2
3
|
ArticlePageView: 'ali_article_page_view',
|
|
3
4
|
ArticleInjected: 'ali_links_injected',
|
|
4
5
|
|
|
5
6
|
TitleModalView: 'ali_title_modal_view',
|
|
7
|
+
TitleModalClose: 'ali_title_modal_close',
|
|
6
8
|
TitleModalScroll: 'ali_title_modal_scroll',
|
|
7
9
|
TitleModalPlaylinkClick: 'ali_title_modal_playlink_click',
|
|
8
10
|
TitleModalSaveClick: 'ali_title_modal_save_click',
|
|
9
11
|
|
|
10
12
|
TitlePopoverView: 'ali_title_popover_view',
|
|
13
|
+
TitlePopoverClose: 'ali_title_popover_close',
|
|
11
14
|
TitlePopoverSaveClick: 'ali_title_popover_save_click',
|
|
12
15
|
TitlePopoverPlaylinkClick: 'ali_title_popover_playlink_click',
|
|
13
16
|
|
|
@@ -22,4 +25,6 @@ export const TrackingEvent = Object.freeze({
|
|
|
22
25
|
ManualReport: 'ali_manual_report',
|
|
23
26
|
EditorError: 'ali_editor_error',
|
|
24
27
|
InjectionError: 'ali_injection_error',
|
|
28
|
+
|
|
29
|
+
TopScrollClick: 'ali_top_scroll_click',
|
|
25
30
|
})
|
package/src/lib/linkInjection.ts
CHANGED
|
@@ -103,6 +103,10 @@ export function getLinkInjectionsParentElement(): HTMLElement {
|
|
|
103
103
|
return document.querySelector('article') || document.querySelector('main') || document.body
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export function getPageText(elements: HTMLElement[]): string {
|
|
107
|
+
return elements.map(element => element.innerText).join('\n\n')
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
/**
|
|
107
111
|
* Replace all found injections within all given elements on the page
|
|
108
112
|
* @returns Returns an array of injections with injections that failed to be inserted marked as `failed`.
|
package/src/lib/tracking.ts
CHANGED
|
@@ -36,6 +36,14 @@ export async function track(event: string, title: TitleData | null = null, paylo
|
|
|
36
36
|
source: 'ali',
|
|
37
37
|
})),
|
|
38
38
|
})
|
|
39
|
+
|
|
40
|
+
pushEventToWindow({ event, payload })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Save this event to the window object. Used when calling .debug() */
|
|
44
|
+
function pushEventToWindow(data: { event: string, payload: Record<string, any> }) {
|
|
45
|
+
if (!window.PlayPilotLinkInjections.tracked_events) window.PlayPilotLinkInjections.tracked_events = []
|
|
46
|
+
window.PlayPilotLinkInjections.tracked_events?.push(data)
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/**
|
|
@@ -7,4 +7,6 @@ export type ScriptConfig = {
|
|
|
7
7
|
after_article_selector?: string
|
|
8
8
|
after_article_insert_position?: InsertPosition | ''
|
|
9
9
|
language?: string | null
|
|
10
|
+
last_successful_fetch?: LinkInjectionResponse | null
|
|
11
|
+
tracked_events?: { event: string, payload: Record<string, any> }[]
|
|
10
12
|
}
|
package/src/lib/types/title.d.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mount } from 'svelte'
|
|
2
2
|
import App from './routes/+page.svelte'
|
|
3
|
-
import { clearLinkInjections } from '$lib/linkInjection'
|
|
3
|
+
import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/linkInjection'
|
|
4
|
+
import { getPageMetaData } from '$lib/meta'
|
|
4
5
|
|
|
5
6
|
window.PlayPilotLinkInjections = {
|
|
6
7
|
token: '',
|
|
@@ -11,6 +12,8 @@ window.PlayPilotLinkInjections = {
|
|
|
11
12
|
language: null,
|
|
12
13
|
organization_sid: null,
|
|
13
14
|
domain_sid: null,
|
|
15
|
+
last_successful_fetch: null,
|
|
16
|
+
tracked_events: [],
|
|
14
17
|
app: null,
|
|
15
18
|
|
|
16
19
|
initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }): void {
|
|
@@ -48,6 +51,38 @@ window.PlayPilotLinkInjections = {
|
|
|
48
51
|
|
|
49
52
|
clearLinkInjections()
|
|
50
53
|
},
|
|
54
|
+
|
|
55
|
+
debug(): void {
|
|
56
|
+
const parentElement = getLinkInjectionsParentElement()
|
|
57
|
+
const elements = getLinkInjectionElements(parentElement)
|
|
58
|
+
|
|
59
|
+
console.group('PlayPilot Link Injection Debug')
|
|
60
|
+
console.groupCollapsed('Config')
|
|
61
|
+
console.table(Object.entries(this))
|
|
62
|
+
console.groupEnd()
|
|
63
|
+
|
|
64
|
+
console.groupCollapsed('Elements')
|
|
65
|
+
console.log('Parent element', parentElement)
|
|
66
|
+
console.log('Valid elements', elements)
|
|
67
|
+
console.groupEnd()
|
|
68
|
+
|
|
69
|
+
console.groupCollapsed('Last fetch')
|
|
70
|
+
console.log(this.last_successful_fetch)
|
|
71
|
+
console.groupEnd()
|
|
72
|
+
|
|
73
|
+
console.groupCollapsed('Meta')
|
|
74
|
+
console.log(getPageMetaData())
|
|
75
|
+
console.groupEnd()
|
|
76
|
+
|
|
77
|
+
console.groupCollapsed('Page text')
|
|
78
|
+
console.log(getPageText(elements))
|
|
79
|
+
console.groupEnd()
|
|
80
|
+
|
|
81
|
+
console.groupCollapsed('Tracked events')
|
|
82
|
+
console.log(this.tracked_events)
|
|
83
|
+
console.groupEnd()
|
|
84
|
+
console.groupEnd()
|
|
85
|
+
}
|
|
51
86
|
}
|
|
52
87
|
|
|
53
88
|
export default window.PlayPilotLinkInjections
|
package/src/routes/+page.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte'
|
|
3
3
|
import { fetchConfig, pollLinkInjections } from '$lib/api'
|
|
4
|
-
import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
|
|
4
|
+
import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
|
|
5
5
|
import { setTrackingSids, track } from '$lib/tracking'
|
|
6
6
|
import { getFullUrlPath } from '$lib/url'
|
|
7
7
|
import { isCrawler } from '$lib/crawler'
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// @ts-ignore It's ok if the response is empty
|
|
27
27
|
const { ai_injections: aiInjections = [], manual_injections: manualInjections = [] } = $derived(response || {})
|
|
28
28
|
|
|
29
|
-
const pageText = $derived(elements
|
|
29
|
+
const pageText = $derived(getPageText(elements))
|
|
30
30
|
|
|
31
31
|
// Rerender link injections when linkInjections change. This is only relevant for editiorial mode.
|
|
32
32
|
$effect(() => {
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
|
+
import { track } from '$lib/tracking'
|
|
4
|
+
import { fade } from 'svelte/transition'
|
|
5
|
+
|
|
6
|
+
// Placeholder data, of course
|
|
7
|
+
const { campaign = {
|
|
8
|
+
campaign_format: 'top_scroll',
|
|
9
|
+
campaign_type: 'video',
|
|
10
|
+
campaign_platforms: ['web', 'android_app', 'ios_app'],
|
|
11
|
+
campaign_name: 'some_campaign_NL',
|
|
12
|
+
campaign_start: '2025-04-24T00:00:00Z',
|
|
13
|
+
campaign_end: '2025-04-30T23:59:00Z',
|
|
14
|
+
campaign_region: 'nl',
|
|
15
|
+
content: {
|
|
16
|
+
header: 'Some top scroll header',
|
|
17
|
+
header_logo: 'https://example.com/',
|
|
18
|
+
header_logo_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
19
|
+
subheader: null,
|
|
20
|
+
image: 'https://example.com/',
|
|
21
|
+
image_uuid: '4fb7d6ea152111f08dd20a58a9feac02',
|
|
22
|
+
video: null,
|
|
23
|
+
format: null,
|
|
24
|
+
},
|
|
25
|
+
cta: {
|
|
26
|
+
header: 'Button',
|
|
27
|
+
subheader: null,
|
|
28
|
+
image: null,
|
|
29
|
+
image_uuid: null,
|
|
30
|
+
url: 'https://google.com/',
|
|
31
|
+
},
|
|
32
|
+
content_playlist: null,
|
|
33
|
+
provider: null,
|
|
34
|
+
impression_trackers: [],
|
|
35
|
+
target_title_uuid: null,
|
|
36
|
+
target_title_sid: null,
|
|
37
|
+
backfill_providers: [],
|
|
38
|
+
autogenerated: false,
|
|
39
|
+
enabled: true,
|
|
40
|
+
hide_imdb_score: false,
|
|
41
|
+
hide_sponsored_message: false,
|
|
42
|
+
disclaimer: 'Some disclaimer',
|
|
43
|
+
} } = $props()
|
|
44
|
+
|
|
45
|
+
const { disclaimer, content, cta } = $derived(campaign)
|
|
46
|
+
const { format, header, header_logo: logo, image_uuid: backgroundImageUUID } = $derived(content)
|
|
47
|
+
const { header: buttonLabel, url: href } = $derived(cta)
|
|
48
|
+
|
|
49
|
+
const simple = $derived(format === 'large')
|
|
50
|
+
|
|
51
|
+
let clientWidth = $state(0)
|
|
52
|
+
let tooltipVisible = $state(false)
|
|
53
|
+
|
|
54
|
+
function trackClick(): void {
|
|
55
|
+
track(TrackingEvent.TopScrollClick)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toggleTooltip(event: MouseEvent): void {
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
event.stopPropagation()
|
|
61
|
+
|
|
62
|
+
tooltipVisible = !tooltipVisible
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<a
|
|
67
|
+
{href}
|
|
68
|
+
target="_blank"
|
|
69
|
+
class="top-scroll"
|
|
70
|
+
class:simple
|
|
71
|
+
tabindex="-1"
|
|
72
|
+
onclick={trackClick}
|
|
73
|
+
rel="sponsored"
|
|
74
|
+
style:--width="{clientWidth}px"
|
|
75
|
+
bind:clientWidth>
|
|
76
|
+
<div class="content">
|
|
77
|
+
{#if simple}
|
|
78
|
+
<img class="content-image" src="https://picsum.photos/seed/movie/728/90" alt={header} width="728" height="90" />
|
|
79
|
+
{:else}
|
|
80
|
+
{#if logo}
|
|
81
|
+
<img class="logo" src="https://picsum.photos/seed/disney/50/50" alt="" width="50" height="50" />
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
<p class="tagline">{header}</p>
|
|
85
|
+
|
|
86
|
+
{#if buttonLabel}
|
|
87
|
+
<button class="button">{buttonLabel}</button>
|
|
88
|
+
{/if}
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{#if !simple}
|
|
93
|
+
{#if backgroundImageUUID}
|
|
94
|
+
<div class="background" style:--background="url('https://picsum.photos/seed/movie/728/90')"></div>
|
|
95
|
+
{/if}
|
|
96
|
+
|
|
97
|
+
{#if disclaimer}
|
|
98
|
+
<button class="tooltip" onclick={toggleTooltip}>
|
|
99
|
+
<div class="disclaimer">i</div>
|
|
100
|
+
|
|
101
|
+
{#if tooltipVisible}
|
|
102
|
+
<div class="tooltip-content" transition:fade={{ duration: 50 }}>
|
|
103
|
+
{disclaimer}
|
|
104
|
+
</div>
|
|
105
|
+
{/if}
|
|
106
|
+
</button>
|
|
107
|
+
{/if}
|
|
108
|
+
{/if}
|
|
109
|
+
</a>
|
|
110
|
+
|
|
111
|
+
<style lang="scss">
|
|
112
|
+
.top-scroll {
|
|
113
|
+
position: relative;
|
|
114
|
+
display: block;
|
|
115
|
+
width: 100%;
|
|
116
|
+
border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
|
|
117
|
+
background: black;
|
|
118
|
+
color: var(--playpilot-top-scroll-text-color, white);
|
|
119
|
+
font-family: var(--playpilot-top-scroll-font-family, var(--playpilot-detail-font-family, var(--playpilot-font-family)));
|
|
120
|
+
font-size: var(--playpilot-top-scroll-font-size, var(--playpilot-detail-font-size, 14px));
|
|
121
|
+
text-decoration: none;
|
|
122
|
+
line-height: 1.35;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.content {
|
|
126
|
+
display: flex;
|
|
127
|
+
z-index: 1;
|
|
128
|
+
position: relative;
|
|
129
|
+
align-items: center;
|
|
130
|
+
height: 100%;
|
|
131
|
+
min-height: margin(4.5);
|
|
132
|
+
padding: margin(0.25) margin(0.5);
|
|
133
|
+
gap: margin(0.5);
|
|
134
|
+
color: var(--playpilot-top-scroll-text-color, white);
|
|
135
|
+
|
|
136
|
+
.simple & {
|
|
137
|
+
justify-content: center;
|
|
138
|
+
min-height: 0;
|
|
139
|
+
padding: 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.logo {
|
|
144
|
+
border-radius: var(--playpilot-top-scroll-logo-border-radius, margin(0.5));
|
|
145
|
+
background: black;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.tagline {
|
|
149
|
+
margin: 0;
|
|
150
|
+
text-shadow: var(--playpilot-shadow);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.button {
|
|
154
|
+
margin-left: auto;
|
|
155
|
+
margin-right: margin(1);
|
|
156
|
+
padding: margin(0.25) margin(0.5);
|
|
157
|
+
background: var(--playpilot-top-scroll-button-background, white);
|
|
158
|
+
border: 0;
|
|
159
|
+
border-radius: var(--playpilot-top-scroll-button-border-radius, margin(0.25));
|
|
160
|
+
box-shadow: var(--playpilot-shadow);
|
|
161
|
+
color: var(--playpilot-top-scroll-button-text-color, black);
|
|
162
|
+
font-family: inherit;
|
|
163
|
+
font-size: inherit;
|
|
164
|
+
line-height: inherit;
|
|
165
|
+
|
|
166
|
+
&:hover {
|
|
167
|
+
outline: margin(0.25) solid var(--playpilot-top-scroll-button-background, rgba(white, 0.35));
|
|
168
|
+
background: var(--playpilot-top-scroll-button-background, white);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&:active {
|
|
172
|
+
background: var(--playpilot-top-scroll-button-background, white);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.background {
|
|
177
|
+
z-index: 0;
|
|
178
|
+
position: absolute;
|
|
179
|
+
top: 0;
|
|
180
|
+
right: 0;
|
|
181
|
+
bottom: 0;
|
|
182
|
+
left: 0;
|
|
183
|
+
border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
|
|
184
|
+
background-image: var(--background);
|
|
185
|
+
background-position: center;
|
|
186
|
+
background-size: cover;
|
|
187
|
+
opacity: 0.4;
|
|
188
|
+
|
|
189
|
+
.top-scroll:hover & {
|
|
190
|
+
filter: brightness(1.15);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.content-image {
|
|
195
|
+
display: block;
|
|
196
|
+
max-width: 100%;
|
|
197
|
+
height: auto;
|
|
198
|
+
background: black;
|
|
199
|
+
border-radius: var(--playpilot-top-scroll-border-radius, var(--playpilot-popover-border-radius, margin(1)));
|
|
200
|
+
|
|
201
|
+
.top-scroll:hover & {
|
|
202
|
+
filter: brightness(1.15);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.disclaimer {
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
border: var(--playpilot-top-scroll-disclaimer-border, 2px solid rgba(255, 255, 255, 0.5));
|
|
211
|
+
padding: margin(0.25);
|
|
212
|
+
height: margin(1.25);
|
|
213
|
+
width: margin(1.25);
|
|
214
|
+
border-radius: 99px;
|
|
215
|
+
color: var(--playpilot-top-scroll-disclaimer-text-color, rgba(255, 255, 255, 0.75));
|
|
216
|
+
font-weight: bold;
|
|
217
|
+
line-height: 1;
|
|
218
|
+
|
|
219
|
+
&:hover {
|
|
220
|
+
border-color: var(--playpilot-top-scroll-disclaimer-hover-border-color, white);
|
|
221
|
+
color: var(--playpilot-top-scroll-disclaimer-hover-text-color, white);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.tooltip {
|
|
226
|
+
z-index: 2;
|
|
227
|
+
position: absolute;
|
|
228
|
+
right: 0;
|
|
229
|
+
bottom: 0;
|
|
230
|
+
padding: margin(0.25);
|
|
231
|
+
background: transparent;
|
|
232
|
+
border: 0;
|
|
233
|
+
color: var(--playpilot-top-scroll-text-color, white);
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.tooltip-content {
|
|
238
|
+
display: block;
|
|
239
|
+
z-index: 20;
|
|
240
|
+
position: absolute;
|
|
241
|
+
right: 100%;
|
|
242
|
+
bottom: margin(0.25);
|
|
243
|
+
width: calc(var(--width) * 0.8);
|
|
244
|
+
padding: margin(0.5);
|
|
245
|
+
border-radius: margin(1);
|
|
246
|
+
background: var(--playpilot-content);
|
|
247
|
+
box-shadow: var(--playpilot-shadow);
|
|
248
|
+
line-height: 1.1;
|
|
249
|
+
text-align: right;
|
|
250
|
+
}
|
|
251
|
+
</style>
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
children: Snippet
|
|
10
|
+
bubble?: Snippet
|
|
10
11
|
onclose?: () => void
|
|
11
12
|
onscroll?: () => void
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const { children, onclose = () => null, onscroll = () => null }: Props = $props()
|
|
15
|
+
const { children, bubble, onclose = () => null, onscroll = () => null }: Props = $props()
|
|
15
16
|
|
|
16
17
|
setContext('scope', 'modal')
|
|
17
18
|
|
|
@@ -22,12 +23,12 @@
|
|
|
22
23
|
return () => document.body.style.overflowY = baseOverflowStyle || ''
|
|
23
24
|
})
|
|
24
25
|
|
|
25
|
-
function scaleOrFly(node: Element): TransitionConfig {
|
|
26
|
+
function scaleOrFly(node: Element, options: { y: number } = { y: 0 }): TransitionConfig {
|
|
26
27
|
if (prefersReducedMotion.current) return fade(node, { duration: 0 })
|
|
27
28
|
|
|
28
29
|
const shouldFly = window.innerWidth < 600
|
|
29
30
|
|
|
30
|
-
if (shouldFly) return fly(node, { duration: 250,
|
|
31
|
+
if (shouldFly) return fly(node, { duration: 250, ...options })
|
|
31
32
|
return scale(node, { duration: 150, start: 0.85 })
|
|
32
33
|
}
|
|
33
34
|
</script>
|
|
@@ -35,7 +36,13 @@
|
|
|
35
36
|
<svelte:window on:keydown={({ key }) => { if (key === 'Escape') onclose() }} />
|
|
36
37
|
|
|
37
38
|
<div class="modal" transition:fade|global={{ duration: 150 }}>
|
|
38
|
-
|
|
39
|
+
{#if bubble}
|
|
40
|
+
<div class="bubble" transition:scaleOrFly|global={{ y: -10 }}>
|
|
41
|
+
{@render bubble()}
|
|
42
|
+
</div>
|
|
43
|
+
{/if}
|
|
44
|
+
|
|
45
|
+
<div class="dialog" {onscroll} role="dialog" aria-labelledby="title" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new>
|
|
39
46
|
<div class="close">
|
|
40
47
|
<RoundButton onclick={() => onclose()}>
|
|
41
48
|
<IconClose />
|
|
@@ -51,44 +58,46 @@
|
|
|
51
58
|
</div>
|
|
52
59
|
|
|
53
60
|
<style lang="scss">
|
|
61
|
+
$max-width: 600px;
|
|
62
|
+
|
|
54
63
|
.modal {
|
|
55
64
|
z-index: 2147483647; // As high as she goes
|
|
56
65
|
box-sizing: border-box;
|
|
57
66
|
position: fixed;
|
|
58
67
|
display: flex;
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
justify-content: flex-start;
|
|
70
|
+
align-items: center;
|
|
61
71
|
top: 0;
|
|
62
72
|
left: 0;
|
|
63
73
|
width: 100%;
|
|
64
74
|
height: 100%;
|
|
65
75
|
background: var(--playpilot-detail-backdrop, rgba(0, 0, 0, 0.65));
|
|
66
76
|
|
|
67
|
-
@media (min-width:
|
|
77
|
+
@media (min-width: $max-width) {
|
|
68
78
|
padding: margin(2);
|
|
79
|
+
gap: margin(1);
|
|
69
80
|
}
|
|
70
81
|
}
|
|
71
82
|
|
|
72
|
-
|
|
73
83
|
.dialog {
|
|
74
84
|
z-index: 1;
|
|
75
85
|
position: relative;
|
|
76
86
|
width: 100%;
|
|
77
|
-
max-width:
|
|
87
|
+
max-width: $max-width;
|
|
78
88
|
max-height: 80vh;
|
|
79
89
|
overflow: auto;
|
|
80
90
|
margin-top: auto;
|
|
81
91
|
border-radius: var(--playpilot-detail-border-radius, margin(1) margin(1) 0 0);
|
|
82
92
|
background: var(--playpilot-detail-background, var(--playpilot-light));
|
|
83
93
|
|
|
84
|
-
@media (min-width:
|
|
94
|
+
@media (min-width: $max-width) {
|
|
85
95
|
margin-top: 0;
|
|
86
96
|
border-radius: var(--playpilot-detail-border-radius, margin(1));
|
|
87
97
|
max-height: 100%;
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
|
|
92
101
|
.backdrop {
|
|
93
102
|
z-index: 0;
|
|
94
103
|
position: absolute;
|
|
@@ -112,4 +121,17 @@
|
|
|
112
121
|
filter: brightness(1.1);
|
|
113
122
|
}
|
|
114
123
|
}
|
|
124
|
+
|
|
125
|
+
.bubble {
|
|
126
|
+
z-index: 1;
|
|
127
|
+
position: relative;
|
|
128
|
+
width: calc(100% - margin(1));
|
|
129
|
+
max-width: $max-width;
|
|
130
|
+
margin: margin(0.5);
|
|
131
|
+
|
|
132
|
+
@media (min-width: $max-width) {
|
|
133
|
+
width: 100%;
|
|
134
|
+
margin: 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
115
137
|
</style>
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
children: Snippet
|
|
8
|
+
bubble?: Snippet
|
|
8
9
|
maxHeight?: number
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
let { children, maxHeight = $bindable() }: Props = $props()
|
|
12
|
+
let { children, bubble, maxHeight = $bindable() }: Props = $props()
|
|
12
13
|
|
|
13
14
|
setContext('scope', 'popover')
|
|
14
15
|
|
|
@@ -63,11 +64,20 @@
|
|
|
63
64
|
<div class="dialog" transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }} data-view-transition-old>
|
|
64
65
|
{@render children()}
|
|
65
66
|
</div>
|
|
67
|
+
|
|
68
|
+
{#if bubble}
|
|
69
|
+
<div transition:fly|global={{ duration: prefersReducedMotion.current ? 0 : 100, y: 10 }}>
|
|
70
|
+
{@render bubble()}
|
|
71
|
+
</div>
|
|
72
|
+
{/if}
|
|
66
73
|
</div>
|
|
67
74
|
|
|
68
75
|
<style lang="scss">
|
|
69
76
|
.popover {
|
|
70
77
|
--offset: #{margin(0.5)};
|
|
78
|
+
display: flex;
|
|
79
|
+
gap: margin(0.5);
|
|
80
|
+
flex-direction: column-reverse;
|
|
71
81
|
position: absolute;
|
|
72
82
|
top: calc((var(--offset) - 1px) * -1); /* Add 1 pixel to account for rounding errors */
|
|
73
83
|
left: 0;
|
|
@@ -78,6 +88,7 @@
|
|
|
78
88
|
z-index: 2147483647; // As high as she goes
|
|
79
89
|
|
|
80
90
|
&.flip {
|
|
91
|
+
flex-direction: column;
|
|
81
92
|
top: auto;
|
|
82
93
|
bottom: calc(var(--offset) + 1px); /* Add 1 pixel to account for rounding errors */
|
|
83
94
|
transform: translateY(calc(100% + var(--offset)));
|
|
@@ -2,8 +2,10 @@
|
|
|
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 { onMount } from 'svelte'
|
|
5
6
|
import Modal from './Modal.svelte'
|
|
6
7
|
import Title from './Title.svelte'
|
|
8
|
+
import TopScroll from './Ads/TopScroll.svelte'
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
11
|
onclose: () => void,
|
|
@@ -16,6 +18,11 @@
|
|
|
16
18
|
|
|
17
19
|
let hasTrackedScrolling = false
|
|
18
20
|
|
|
21
|
+
onMount(() => {
|
|
22
|
+
const openTimestamp = Date.now()
|
|
23
|
+
return () => track(TrackingEvent.TitleModalClose, title, { time_spent: Date.now() - openTimestamp })
|
|
24
|
+
})
|
|
25
|
+
|
|
19
26
|
function onscroll(): void {
|
|
20
27
|
if (hasTrackedScrolling) return
|
|
21
28
|
|
|
@@ -26,4 +33,8 @@
|
|
|
26
33
|
|
|
27
34
|
<Modal {onclose} {onscroll}>
|
|
28
35
|
<Title {title} />
|
|
36
|
+
|
|
37
|
+
{#snippet bubble()}
|
|
38
|
+
<TopScroll />
|
|
39
|
+
{/snippet}
|
|
29
40
|
</Modal>
|