@playpilot/tpi 5.17.0-beta.1 → 5.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/link-injections.js +9 -9
- package/eslint.config.js +16 -0
- package/events.md +3 -0
- package/package.json +1 -1
- package/src/app.d.ts +13 -13
- package/src/lib/actions/heading.ts +2 -2
- package/src/lib/actions/middlemouse.ts +2 -2
- package/src/lib/{ads.ts → api/ads.ts} +19 -22
- package/src/lib/api/api.ts +21 -0
- package/src/lib/{auth.ts → api/auth.ts} +8 -12
- package/src/lib/api/config.ts +16 -0
- package/src/lib/{api.ts → api/externalPages.ts} +17 -48
- package/src/lib/api/search.ts +14 -0
- package/src/lib/{session.ts → api/session.ts} +3 -3
- package/src/lib/array.ts +2 -2
- package/src/lib/consent.ts +1 -1
- package/src/lib/data/translations.ts +1 -1
- package/src/lib/enums/SplitTest.ts +2 -2
- package/src/lib/enums/TrackingEvent.ts +13 -0
- package/src/lib/event.ts +1 -1
- package/src/lib/fakeData.ts +4 -4
- package/src/lib/hash.ts +1 -1
- package/src/lib/image.ts +2 -2
- package/src/lib/{linkInjection.ts → injection.ts} +25 -18
- package/src/lib/meta.ts +1 -1
- package/src/lib/modal.ts +1 -1
- package/src/lib/playlink.ts +1 -1
- package/src/lib/routes.ts +9 -0
- package/src/lib/splitTest.ts +4 -4
- package/src/lib/text.ts +1 -1
- package/src/lib/token.ts +3 -0
- package/src/lib/tracking.ts +5 -5
- package/src/lib/types/global.d.ts +1 -1
- package/src/lib/types/injection.d.ts +1 -1
- package/src/lib/types/script.d.ts +18 -3
- package/src/lib/types/session.d.ts +1 -1
- package/src/lib/types/title.d.ts +2 -2
- package/src/main.ts +12 -12
- package/src/routes/+page.svelte +5 -4
- package/src/routes/components/Debugger.svelte +26 -1
- package/src/routes/components/Editorial/Editor.svelte +7 -7
- package/src/routes/components/Editorial/EditorItem.svelte +1 -1
- package/src/routes/components/Editorial/ManualInjection.svelte +8 -8
- package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +1 -1
- package/src/routes/components/Editorial/Search/TitleSearch.svelte +1 -1
- package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +3 -2
- package/src/routes/components/Editorial/Session.svelte +1 -1
- package/src/routes/components/ListTitle.svelte +2 -2
- package/src/routes/components/ParticipantModal.svelte +11 -0
- package/src/routes/components/Playlinks.svelte +1 -1
- package/src/routes/components/Rails/SimilarRail.svelte +9 -1
- package/src/routes/components/Rails/TitlesRail.svelte +11 -7
- package/src/routes/components/TitleModal.svelte +3 -3
- package/src/routes/components/TitlePopover.svelte +1 -1
- package/src/tests/lib/{ads.test.js → api/ads.test.js} +17 -14
- package/src/tests/lib/api/api.test.js +49 -0
- package/src/tests/lib/{auth.test.js → api/auth.test.js} +10 -23
- package/src/tests/lib/api/config.test.js +53 -0
- package/src/tests/lib/{api.test.js → api/externalPages.test.js} +71 -101
- package/src/tests/lib/{search.test.js → api/search.test.js} +10 -9
- package/src/tests/lib/{session.test.js → api/session.test.js} +4 -4
- package/src/tests/lib/{linkInjection.test.js → injections.test.js} +26 -2
- package/src/tests/lib/routes.test.js +15 -0
- package/src/tests/routes/+page.test.js +17 -9
- package/src/tests/routes/components/Editorial/Editor.test.js +3 -3
- package/src/tests/routes/components/Editorial/EditorItem.test.js +1 -1
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +2 -2
- package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +2 -2
- package/src/tests/routes/components/Editorial/Session.test.js +2 -2
- package/src/tests/routes/components/ParticipantModal.test.js +35 -0
- package/src/tests/routes/components/Rails/{TitleRail.test.js → TitlesRail.test.js} +10 -1
- package/src/tests/setup.js +2 -0
- package/src/lib/search.ts +0 -23
package/eslint.config.js
CHANGED
|
@@ -65,4 +65,20 @@ export default [
|
|
|
65
65
|
],
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
files: ['**/*.ts'],
|
|
70
|
+
ignores: ['**/*.d.ts'],
|
|
71
|
+
languageOptions: {
|
|
72
|
+
parser: tsParser,
|
|
73
|
+
},
|
|
74
|
+
rules: {
|
|
75
|
+
'@typescript-eslint/explicit-function-return-type': [
|
|
76
|
+
'error',
|
|
77
|
+
{
|
|
78
|
+
allowExpressions: true,
|
|
79
|
+
allowTypedFunctionExpressions: true,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
68
84
|
]
|
package/events.md
CHANGED
|
@@ -34,6 +34,9 @@ Event | Action | Info | Payload
|
|
|
34
34
|
`ali_title_modal_scroll` | _Fires the first time a user scrolls inside of a titel modal._ | | `Title`
|
|
35
35
|
`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)
|
|
36
36
|
`ali_title_modal_save_click` | _Currently unused, there is no save functionality._ | | `Title`
|
|
37
|
+
`ali_participant_modal_view` | _Fires any time a title modal is viewed_ | The title modal opens when viewing a participant modal both on desktop and mobile | `participant` (name of the participant)
|
|
38
|
+
`ali_participant_modal_close` | _Fires any time a title modal is closed_ | | `participant` (name of the participant) `time_spent` (time between modal_view and modal_close milliseconds)
|
|
39
|
+
'ali_similar_title_click' | _Fires any time a similar titles rail item is clicked_ | Title | `title_source` (original name of the title the rail item was clicked in)
|
|
37
40
|
|
|
38
41
|
### Popover
|
|
39
42
|
Event | Action | Info | Payload
|
package/package.json
CHANGED
package/src/app.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// See https://svelte.dev/docs/kit/types#app.d.ts
|
|
2
|
-
// for information about these interfaces
|
|
3
|
-
declare global {
|
|
4
|
-
namespace App {
|
|
5
|
-
// interface Error {}
|
|
6
|
-
// interface Locals {}
|
|
7
|
-
// interface PageData {}
|
|
8
|
-
// interface PageState {}
|
|
9
|
-
// interface Platform {}
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export {}
|
|
1
|
+
// See https://svelte.dev/docs/kit/types#app.d.ts
|
|
2
|
+
// for information about these interfaces
|
|
3
|
+
declare global {
|
|
4
|
+
namespace App {
|
|
5
|
+
// interface Error {}
|
|
6
|
+
// interface Locals {}
|
|
7
|
+
// interface PageData {}
|
|
8
|
+
// interface PageState {}
|
|
9
|
+
// interface Platform {}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* We still want headings to be semantically correct, which is all this action does.
|
|
6
6
|
* @example `use:heading{3}`
|
|
7
7
|
*/
|
|
8
|
-
export function heading(node: HTMLElement, level = 1) {
|
|
9
|
-
node.role =
|
|
8
|
+
export function heading(node: HTMLElement, level = 1): void {
|
|
9
|
+
node.role = 'heading'
|
|
10
10
|
node.ariaLevel = level.toString()
|
|
11
11
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function middlemouse(node: HTMLElement, options: { onclick: (event: MouseEvent) => void }) {
|
|
2
|
-
|
|
1
|
+
export function middlemouse(node: HTMLElement, options: { onclick: (event: MouseEvent) => void }): { destroy(): void } {
|
|
2
|
+
const { onclick } = options
|
|
3
3
|
|
|
4
4
|
function mouseup(event: MouseEvent): MouseEvent | null {
|
|
5
5
|
if (event.button !== 1) return null
|
|
@@ -1,29 +1,26 @@
|
|
|
1
|
-
import { getApiToken } from
|
|
2
|
-
import { hasConsentedTo } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
export async function fetchAds() {
|
|
10
|
-
if (!hasConsentedTo('ads')) return
|
|
11
|
-
|
|
12
|
-
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
1
|
+
import { getApiToken } from '../token'
|
|
2
|
+
import { hasConsentedTo } from '../consent'
|
|
3
|
+
import { TrackingEvent } from '../enums/TrackingEvent'
|
|
4
|
+
import { track } from '../tracking'
|
|
5
|
+
import type { Campaign, CampaignFormat } from '../types/campaign'
|
|
6
|
+
import type { PlaylinkData } from '../types/playlink'
|
|
7
|
+
import { api } from './api'
|
|
8
|
+
|
|
9
|
+
export async function fetchAds(): Promise<Campaign[]> {
|
|
10
|
+
if (!hasConsentedTo('ads')) return []
|
|
11
|
+
|
|
13
12
|
const apiToken = getApiToken()
|
|
14
13
|
|
|
15
14
|
if (!apiToken) throw new Error('No token was provided')
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
try {
|
|
17
|
+
const response = await api<Campaign[]>(`/ads/browse/?region=nl&api-token=${apiToken}`)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
return response
|
|
20
|
+
} catch (error: any) {
|
|
21
|
+
track(TrackingEvent.AdsFetchFailed, null, { status: error.status })
|
|
22
|
+
throw error
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
const parsed = await response.json()
|
|
25
|
-
|
|
26
|
-
return parsed
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
/**
|
|
@@ -52,7 +49,7 @@ export function campaignToPlaylink(campaign: Campaign): PlaylinkData {
|
|
|
52
49
|
cta_text: campaign.content.subheader,
|
|
53
50
|
action_text: campaign.cta.header,
|
|
54
51
|
extra_info: {
|
|
55
|
-
category: 'SVOD'
|
|
56
|
-
}
|
|
52
|
+
category: 'SVOD',
|
|
53
|
+
},
|
|
57
54
|
}
|
|
58
55
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { apiBaseUrl } from '$lib/constants'
|
|
2
|
+
|
|
3
|
+
type Options = {
|
|
4
|
+
headers?: Record<string, string>
|
|
5
|
+
method?: 'POST' | 'GET'
|
|
6
|
+
body?: null | Record<string, any>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function api<T>(path: string, { headers = {}, method = 'GET', body = null }: Options = {}): Promise<T> {
|
|
10
|
+
const baseHeaders = { 'Content-Type': 'application/json' }
|
|
11
|
+
|
|
12
|
+
const response = await fetch(apiBaseUrl + path, {
|
|
13
|
+
method,
|
|
14
|
+
headers: new Headers({ ...baseHeaders, ...headers }),
|
|
15
|
+
body: body ? JSON.stringify(body as BodyInit) : null,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
if (!response?.ok) throw response
|
|
19
|
+
|
|
20
|
+
return await response.json()
|
|
21
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getApiToken } from '
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { getApiToken } from '$lib/token'
|
|
2
|
+
import { TrackingEvent } from '../enums/TrackingEvent'
|
|
3
|
+
import { track } from '../tracking'
|
|
4
|
+
import { api } from './api'
|
|
5
5
|
|
|
6
6
|
const cookieName = 'EncryptedToken'
|
|
7
7
|
const urlParam = 'articleReplacementEditToken'
|
|
@@ -13,8 +13,6 @@ const editorialParam = 'playpilot-editorial-mode'
|
|
|
13
13
|
* @returns Whether the user is authorized or not
|
|
14
14
|
*/
|
|
15
15
|
export async function authorize(href: string = window.location.href): Promise<boolean> {
|
|
16
|
-
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
17
|
-
|
|
18
16
|
try {
|
|
19
17
|
const apiToken = getApiToken()
|
|
20
18
|
if (!apiToken) throw new Error('No token was provided')
|
|
@@ -22,16 +20,14 @@ export async function authorize(href: string = window.location.href): Promise<bo
|
|
|
22
20
|
const authToken = getAuthToken(href)
|
|
23
21
|
if (!authToken) throw new Error('Could not be authenticated')
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// Nothing is done with the response. If it's ok, we're good, if not, it throws and is caught below.
|
|
24
|
+
await api(`/external-pages/edit-authorization?api-token=${apiToken}`, {
|
|
27
25
|
method: 'POST',
|
|
28
|
-
body:
|
|
26
|
+
body: {
|
|
29
27
|
private_token: authToken,
|
|
30
|
-
}
|
|
28
|
+
},
|
|
31
29
|
})
|
|
32
30
|
|
|
33
|
-
if (!response.ok) throw response
|
|
34
|
-
|
|
35
31
|
setAuthCookie(authToken)
|
|
36
32
|
|
|
37
33
|
return true
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getApiToken } from '$lib/token'
|
|
2
|
+
import type { ConfigResponse } from '$lib/types/config'
|
|
3
|
+
import { api } from './api'
|
|
4
|
+
|
|
5
|
+
export async function fetchConfig(): Promise<ConfigResponse | null> {
|
|
6
|
+
const apiToken = getApiToken()
|
|
7
|
+
|
|
8
|
+
if (!apiToken) throw new Error('No token was provided')
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const response = await api(`/domains/config?api-token=${apiToken}`)
|
|
12
|
+
return await response || null
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { authorize, getAuthToken, isEditorialModeEnabled } from './auth'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
2
|
+
import { generateRandomHash, stringToHash } from '../hash'
|
|
3
|
+
import { getLanguage } from '../localization'
|
|
4
|
+
import { getDatetime, getPageMetaData } from '../meta'
|
|
5
|
+
import type { LinkInjectionResponse, LinkInjection } from '../types/injection'
|
|
6
|
+
import { getFullUrlPath } from '../url'
|
|
7
|
+
import { api } from './api'
|
|
8
|
+
import { getApiToken } from '$lib/token'
|
|
9
9
|
|
|
10
10
|
let pollTimeout: ReturnType<typeof setTimeout> | null = null
|
|
11
11
|
|
|
@@ -20,15 +20,15 @@ let pollTimeout: ReturnType<typeof setTimeout> | null = null
|
|
|
20
20
|
export async function fetchLinkInjections(
|
|
21
21
|
pageText: string | null,
|
|
22
22
|
{ url = getFullUrlPath(), hash = stringToHash(pageText || ''), params = {} }:
|
|
23
|
-
{ url?: string, hash?: string, params?: Record<string, any> } = {}
|
|
23
|
+
{ url?: string, hash?: string, params?: Record<string, any> } = {},
|
|
24
24
|
): Promise<LinkInjectionResponse> {
|
|
25
|
-
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
26
25
|
const apiToken = getApiToken()
|
|
27
26
|
const isEditorialMode = isEditorialModeEnabled() ? await authorize() : false
|
|
28
27
|
const language = getLanguage()
|
|
29
28
|
|
|
30
29
|
if (!apiToken) throw new Error('No token was provided')
|
|
31
30
|
|
|
31
|
+
params.url = url
|
|
32
32
|
params.metadata = getPageMetaData()
|
|
33
33
|
|
|
34
34
|
// Add additional parameters if request is made to run the AI. We only include these when saving new data.
|
|
@@ -38,23 +38,15 @@ export async function fetchLinkInjections(
|
|
|
38
38
|
params.page_text = pageText
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const response = await
|
|
42
|
-
headers,
|
|
41
|
+
const response = await api<LinkInjectionResponse>(`/external-pages/?api-token=${apiToken}&include_title_details=true${isEditorialMode ? '&editorial_mode_enabled=true' : ''}&language=${language}`, {
|
|
43
42
|
method: 'POST',
|
|
44
|
-
body:
|
|
45
|
-
url,
|
|
46
|
-
...params
|
|
47
|
-
}),
|
|
43
|
+
body: params,
|
|
48
44
|
})
|
|
49
45
|
|
|
50
|
-
if (!response.ok) throw response
|
|
51
|
-
|
|
52
|
-
const parsed = await response.json()
|
|
53
|
-
|
|
54
46
|
// This is used when debugging (using window.PlayPilotLinkInjections.debug())
|
|
55
|
-
window.PlayPilotLinkInjections.last_successful_fetch =
|
|
47
|
+
window.PlayPilotLinkInjections.last_successful_fetch = response
|
|
56
48
|
|
|
57
|
-
return
|
|
49
|
+
return response
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
/**
|
|
@@ -64,7 +56,7 @@ export async function fetchLinkInjections(
|
|
|
64
56
|
export async function pollLinkInjections(
|
|
65
57
|
pageText: string,
|
|
66
58
|
{ requireCompletedResult = false, runAiWhenRelevant = false, pollInterval = 3000, maxTries = 600, onpoll = () => null }:
|
|
67
|
-
{ requireCompletedResult?: boolean, runAiWhenRelevant?: boolean, pollInterval?: number, maxTries?: number, onpoll?: (_response: LinkInjectionResponse) => void } = {}
|
|
59
|
+
{ requireCompletedResult?: boolean, runAiWhenRelevant?: boolean, pollInterval?: number, maxTries?: number, onpoll?: (_response: LinkInjectionResponse) => void } = {},
|
|
68
60
|
): Promise<LinkInjectionResponse> {
|
|
69
61
|
let currentTry = 0
|
|
70
62
|
|
|
@@ -144,7 +136,7 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], pageTe
|
|
|
144
136
|
after_article_style: linkInjection.after_article_style || null,
|
|
145
137
|
in_text: linkInjection.in_text ?? true,
|
|
146
138
|
inactive: !!linkInjection.inactive,
|
|
147
|
-
removed: !!linkInjection.removed
|
|
139
|
+
removed: !!linkInjection.removed,
|
|
148
140
|
}))
|
|
149
141
|
|
|
150
142
|
const response = await fetchLinkInjections(pageText, {
|
|
@@ -160,25 +152,6 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], pageTe
|
|
|
160
152
|
return linkInjections
|
|
161
153
|
}
|
|
162
154
|
|
|
163
|
-
export async function fetchConfig(): Promise<ConfigResponse | null> {
|
|
164
|
-
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
165
|
-
const apiToken = getApiToken()
|
|
166
|
-
|
|
167
|
-
if (!apiToken) throw new Error('No token was provided')
|
|
168
|
-
|
|
169
|
-
const response = await fetch(apiBaseUrl + `/domains/config?api-token=${apiToken}`, {
|
|
170
|
-
headers,
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
if (!response.ok) throw response
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
return await response.json() || null
|
|
177
|
-
} catch {
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
155
|
/**
|
|
183
156
|
* Insert random keys into link injections. These are used to identify the links on the page.
|
|
184
157
|
* We can't just use SIDs, as a page might include multiple links of the same title
|
|
@@ -186,8 +159,8 @@ export async function fetchConfig(): Promise<ConfigResponse | null> {
|
|
|
186
159
|
function insertRandomKeys(linkInjections: LinkInjection[]): LinkInjection[] {
|
|
187
160
|
return linkInjections.map((linkInjection: LinkInjection) => ({
|
|
188
161
|
...linkInjection,
|
|
189
|
-
key: generateInjectionKey(linkInjection.sid)
|
|
190
|
-
}))
|
|
162
|
+
key: generateInjectionKey(linkInjection.sid),
|
|
163
|
+
}))
|
|
191
164
|
}
|
|
192
165
|
|
|
193
166
|
/**
|
|
@@ -198,7 +171,3 @@ function insertRandomKeys(linkInjections: LinkInjection[]): LinkInjection[] {
|
|
|
198
171
|
export function generateInjectionKey(sid: string): string {
|
|
199
172
|
return sid + '-' + generateRandomHash()
|
|
200
173
|
}
|
|
201
|
-
|
|
202
|
-
export function getApiToken(): string | undefined {
|
|
203
|
-
return window.PlayPilotLinkInjections?.token
|
|
204
|
-
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getApiToken } from '$lib/token'
|
|
2
|
+
import type { TitleData } from '../types/title'
|
|
3
|
+
import { api } from './api'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Search for movies & shows. Requires valid API token.
|
|
7
|
+
*/
|
|
8
|
+
export async function searchTitles(query: string): Promise<TitleData[]> {
|
|
9
|
+
const apiToken = getApiToken()
|
|
10
|
+
|
|
11
|
+
if (!apiToken) throw new Error('No token was provided')
|
|
12
|
+
|
|
13
|
+
return await api<TitleData[]>(`/search/titles/?api-token=${apiToken}&query=${query}`)
|
|
14
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchLinkInjections } from
|
|
2
|
-
import { generateRandomHash } from
|
|
3
|
-
import type { SessionResponse } from
|
|
1
|
+
import { fetchLinkInjections } from './externalPages'
|
|
2
|
+
import { generateRandomHash } from '../hash'
|
|
3
|
+
import type { SessionResponse } from '../types/session'
|
|
4
4
|
|
|
5
5
|
export const sessionKey = 'PlayPilotEditorialSessionId'
|
|
6
6
|
export const sessionPollPeriodMilliseconds = 5000
|
package/src/lib/array.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Range } from
|
|
1
|
+
import type { Range } from './types/range'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns the largest number in an array of numbers. Returns 0 if the array has no entries.
|
|
@@ -24,6 +24,6 @@ export function getNumberOfOccurrencesInArray<T>(array: T[], item: T, keys: stri
|
|
|
24
24
|
return array.filter(i => keys.every(key => i[key] === item[key])).length
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function isIndexInAnyRange(index: number, ranges: Range[]) {
|
|
27
|
+
export function isIndexInAnyRange(index: number, ranges: Range[]): boolean {
|
|
28
28
|
return ranges.some(range => index >= range.start && index < range.end)
|
|
29
29
|
}
|
package/src/lib/consent.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const translations = {
|
|
|
44
44
|
'Commission Disclaimer': {
|
|
45
45
|
[Language.English]: 'We may earn a commission if you make a purchase through these links. In collaboration with',
|
|
46
46
|
[Language.Swedish]: 'Vi kan tjäna en provision om du gör ett köp via dessa länkar. I samarbete med',
|
|
47
|
-
|
|
47
|
+
[Language.Danish]: 'Vi kan tjene en kommission, hvis du foretager et køb via disse links. I samarbejde med',
|
|
48
48
|
},
|
|
49
49
|
'And': {
|
|
50
50
|
[Language.English]: 'and',
|
|
@@ -2,10 +2,10 @@ export const SplitTest = {
|
|
|
2
2
|
TopScrollFormat: {
|
|
3
3
|
key: 'top_scroll_format',
|
|
4
4
|
numberOfVariants: 2,
|
|
5
|
-
variantNames: ['Separated', 'Inline'] as string[]
|
|
5
|
+
variantNames: ['Separated', 'Inline'] as string[],
|
|
6
6
|
},
|
|
7
7
|
ParticipantPlaylinkFormat: {
|
|
8
8
|
key: 'participant_playlink_format',
|
|
9
9
|
numberOfVariants: 2,
|
|
10
|
-
}
|
|
10
|
+
},
|
|
11
11
|
} as const
|
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
/** @see /events.md */
|
|
2
2
|
export const TrackingEvent = Object.freeze({
|
|
3
|
+
// General
|
|
3
4
|
ArticlePageView: 'ali_article_page_view',
|
|
4
5
|
ArticleInjected: 'ali_links_injected',
|
|
5
6
|
InjectionVisible: 'ali_injection_visible',
|
|
6
7
|
|
|
8
|
+
// Modal
|
|
7
9
|
TitleModalView: 'ali_title_modal_view',
|
|
8
10
|
TitleModalClose: 'ali_title_modal_close',
|
|
9
11
|
TitleModalScroll: 'ali_title_modal_scroll',
|
|
10
12
|
TitleModalPlaylinkClick: 'ali_title_modal_playlink_click',
|
|
11
13
|
TitleModalSaveClick: 'ali_title_modal_save_click',
|
|
14
|
+
ParticipantModalView: 'ali_participant_modal_view',
|
|
15
|
+
ParticipantModalClose: 'ali_participant_modal_close',
|
|
12
16
|
|
|
17
|
+
// Popover
|
|
13
18
|
TitlePopoverView: 'ali_title_popover_view',
|
|
14
19
|
TitlePopoverClose: 'ali_title_popover_close',
|
|
15
20
|
TitlePopoverSaveClick: 'ali_title_popover_save_click',
|
|
16
21
|
TitlePopoverPlaylinkClick: 'ali_title_popover_playlink_click',
|
|
17
22
|
|
|
23
|
+
// Rails
|
|
24
|
+
SimilarTitleClick: 'ali_similar_title_click',
|
|
25
|
+
|
|
26
|
+
// After article
|
|
18
27
|
AfterArticlePlaylinkClick: 'ali_after_article_playlink_click',
|
|
19
28
|
AfterArticleModalButtonClick: 'ali_after_article_modal_button_click',
|
|
20
29
|
|
|
30
|
+
// Fails
|
|
21
31
|
InjectionFailed: 'ali_injection_failed',
|
|
22
32
|
TotalInjectionsCount: 'ali_injection_count',
|
|
23
33
|
FetchingConfigFailed: 'ali_fetch_config_failed',
|
|
24
34
|
AuthFailed: 'ali_auth_failed',
|
|
25
35
|
|
|
36
|
+
// Errors
|
|
26
37
|
ManualReport: 'ali_manual_report',
|
|
27
38
|
EditorError: 'ali_editor_error',
|
|
28
39
|
InjectionError: 'ali_injection_error',
|
|
29
40
|
|
|
41
|
+
// Ads
|
|
30
42
|
TopScrollView: 'ali_top_scroll_view',
|
|
31
43
|
TopScrollClick: 'ali_top_scroll_click',
|
|
32
44
|
DisplayAdView: 'ali_display_ad_view',
|
|
@@ -34,6 +46,7 @@ export const TrackingEvent = Object.freeze({
|
|
|
34
46
|
DisplayedAdPlaylickClick: 'ali_display_ad_playlink_click',
|
|
35
47
|
AdsFetchFailed: 'ali_ads_fetch_failed',
|
|
36
48
|
|
|
49
|
+
// Split tests
|
|
37
50
|
SplitTestView: 'ali_split_test_view',
|
|
38
51
|
SplitTestAction: 'ali_split_test_action',
|
|
39
52
|
})
|
package/src/lib/event.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns true when user performs event that should open a link in a new tab
|
|
3
3
|
*/
|
|
4
|
-
export function isHoldingSpecialKey(event: MouseEvent) {
|
|
4
|
+
export function isHoldingSpecialKey(event: MouseEvent): boolean {
|
|
5
5
|
return event.ctrlKey || event.metaKey || event.button !== 0
|
|
6
6
|
}
|
package/src/lib/fakeData.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Campaign } from
|
|
2
|
-
import type { LinkInjection } from
|
|
3
|
-
import type { ParticipantData } from
|
|
4
|
-
import type { TitleData } from
|
|
1
|
+
import type { Campaign } from './types/campaign'
|
|
2
|
+
import type { LinkInjection } from './types/injection'
|
|
3
|
+
import type { ParticipantData } from './types/participant'
|
|
4
|
+
import type { TitleData } from './types/title'
|
|
5
5
|
|
|
6
6
|
export const title: TitleData = {
|
|
7
7
|
sid: 'tig9r9F',
|
package/src/lib/hash.ts
CHANGED
package/src/lib/image.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { imageBaseUrl } from
|
|
2
|
-
import type { ImageDimensions } from
|
|
1
|
+
import { imageBaseUrl } from './constants'
|
|
2
|
+
import type { ImageDimensions } from './enums/ImageDimensions'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* NOTE: This is a temporary measure. Images url from the API use a previous format which is to be replaced,
|
|
@@ -8,8 +8,8 @@ import { playFallbackViewTransition } from './viewTransition'
|
|
|
8
8
|
import { prefersReducedMotion } from 'svelte/motion'
|
|
9
9
|
import { getNumberOfOccurrencesInArray } from './array'
|
|
10
10
|
import { mobileBreakpoint } from './constants'
|
|
11
|
+
import { isEditorialModeEnabled } from './api/auth'
|
|
11
12
|
import { destroyAllModals, openModal } from './modal'
|
|
12
|
-
import { isEditorialModeEnabled } from './auth'
|
|
13
13
|
import { track } from './tracking'
|
|
14
14
|
import { TrackingEvent } from './enums/TrackingEvent'
|
|
15
15
|
|
|
@@ -238,7 +238,10 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
238
238
|
|
|
239
239
|
return mergedInjections.filter(i => i.title_details).map((injection, index) => {
|
|
240
240
|
// Favour manual injections over AI injections
|
|
241
|
-
const
|
|
241
|
+
const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
|
|
242
|
+
const duplicate = injection.duplicate ?? hasManualEquivalent
|
|
243
|
+
|
|
244
|
+
if (duplicate) failedMessages[injection.key] = hasManualEquivalent ? 'Injection was manually removed.' : 'Injection was marked as duplicate.'
|
|
242
245
|
|
|
243
246
|
const matchingElement = document.querySelector(`[${keyDataAttribute}="${injection.key}"]`)
|
|
244
247
|
const failed = isValidPlaylinkType(injection) && !injection.inactive && !injection.after_article && !matchingElement
|
|
@@ -253,7 +256,7 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
253
256
|
inactive: injection.inactive ?? false,
|
|
254
257
|
duplicate,
|
|
255
258
|
failed,
|
|
256
|
-
failed_message: failedMessage
|
|
259
|
+
failed_message: failedMessage,
|
|
257
260
|
}
|
|
258
261
|
})
|
|
259
262
|
}
|
|
@@ -353,7 +356,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
353
356
|
playFallbackViewTransition(() => {
|
|
354
357
|
destroyLinkPopover(false)
|
|
355
358
|
openModal({ event, injection, data: injection.title_details })
|
|
356
|
-
}, !prefersReducedMotion.current && window.innerWidth >= mobileBreakpoint && !window.matchMedia(
|
|
359
|
+
}, !prefersReducedMotion.current && window.innerWidth >= mobileBreakpoint && !window.matchMedia('(pointer: coarse)').matches)
|
|
357
360
|
})
|
|
358
361
|
|
|
359
362
|
window.addEventListener('mousemove', (event) => {
|
|
@@ -426,20 +429,24 @@ function openLinkPopover(event: MouseEvent, injection: LinkInjection): void {
|
|
|
426
429
|
/**
|
|
427
430
|
* Unmount the popover, removing it from the dom
|
|
428
431
|
*/
|
|
429
|
-
function destroyLinkPopover(outro: boolean = true) {
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
// HMR during development. In that case we remove the element straight from the dom.
|
|
433
|
-
// Doing this will prevent the outro animation from playing, but this being a fallback, that's ok.
|
|
434
|
-
document.querySelectorAll<HTMLElement>('[data-playpilot-title-popover]').forEach(element => element.remove())
|
|
435
|
-
|
|
436
|
-
return
|
|
437
|
-
}
|
|
432
|
+
async function destroyLinkPopover(outro: boolean = true) {
|
|
433
|
+
if (activePopoverInsertedComponent) {
|
|
434
|
+
const promise = unmount(activePopoverInsertedComponent, { outro })
|
|
438
435
|
|
|
439
|
-
|
|
436
|
+
currentlyHoveredInjection = null
|
|
437
|
+
activePopoverInsertedComponent = null
|
|
440
438
|
|
|
441
|
-
|
|
442
|
-
|
|
439
|
+
// Await the unmount promise after setting the variables above to prevent race conditions when
|
|
440
|
+
// mounting a new popover. The promise resolves after the element has transitioned fully out.
|
|
441
|
+
await promise
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// In some cases a popover lingers even if it should have been removed. This happens sometimes during
|
|
445
|
+
// HMR during development, but I've seen it happen on production too.
|
|
446
|
+
// In that case we remove the element straight from the dom.
|
|
447
|
+
// Doing this will prevent the outro animation from playing, but this being a fallback, that's ok.
|
|
448
|
+
// TODO: Find the actual cause of this bug.
|
|
449
|
+
document.querySelectorAll<HTMLElement>('[data-playpilot-title-popover]').forEach(element => element.remove())
|
|
443
450
|
}
|
|
444
451
|
|
|
445
452
|
/**
|
|
@@ -463,8 +470,8 @@ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections:
|
|
|
463
470
|
target,
|
|
464
471
|
props: {
|
|
465
472
|
linkInjections: injections,
|
|
466
|
-
onclickmodal: (event, injection) => openModal({ event, injection, data: injection.title_details })
|
|
467
|
-
}
|
|
473
|
+
onclickmodal: (event, injection) => openModal({ event, injection, data: injection.title_details }),
|
|
474
|
+
},
|
|
468
475
|
})
|
|
469
476
|
}
|
|
470
477
|
|
package/src/lib/meta.ts
CHANGED
package/src/lib/modal.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { mount, unmount } from "svelte"
|
|
|
2
2
|
import { isHoldingSpecialKey } from "./event"
|
|
3
3
|
import TitleModal from "../routes/components/TitleModal.svelte"
|
|
4
4
|
import type { LinkInjection } from "./types/injection"
|
|
5
|
-
import { getPlayPilotWrapperElement } from "./linkInjection"
|
|
6
5
|
import ParticipantModal from "../routes/components/ParticipantModal.svelte"
|
|
7
6
|
import type { TitleData } from "./types/title"
|
|
8
7
|
import type { ParticipantData } from "./types/participant"
|
|
9
8
|
import { mobileBreakpoint } from "./constants"
|
|
9
|
+
import { getPlayPilotWrapperElement } from "./injection"
|
|
10
10
|
|
|
11
11
|
type ModalType = 'title' | 'participant'
|
|
12
12
|
|
package/src/lib/playlink.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { playPilotBaseUrl } from "./constants"
|
|
2
|
+
import type { TitleData } from "./types/title"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @returns Full url to PlayPilot page for the given title
|
|
6
|
+
*/
|
|
7
|
+
export function titleUrl(title: TitleData): string {
|
|
8
|
+
return `${playPilotBaseUrl}/${title.type}/${title.slug}/`
|
|
9
|
+
}
|