@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.
Files changed (73) hide show
  1. package/dist/link-injections.js +9 -9
  2. package/eslint.config.js +16 -0
  3. package/events.md +3 -0
  4. package/package.json +1 -1
  5. package/src/app.d.ts +13 -13
  6. package/src/lib/actions/heading.ts +2 -2
  7. package/src/lib/actions/middlemouse.ts +2 -2
  8. package/src/lib/{ads.ts → api/ads.ts} +19 -22
  9. package/src/lib/api/api.ts +21 -0
  10. package/src/lib/{auth.ts → api/auth.ts} +8 -12
  11. package/src/lib/api/config.ts +16 -0
  12. package/src/lib/{api.ts → api/externalPages.ts} +17 -48
  13. package/src/lib/api/search.ts +14 -0
  14. package/src/lib/{session.ts → api/session.ts} +3 -3
  15. package/src/lib/array.ts +2 -2
  16. package/src/lib/consent.ts +1 -1
  17. package/src/lib/data/translations.ts +1 -1
  18. package/src/lib/enums/SplitTest.ts +2 -2
  19. package/src/lib/enums/TrackingEvent.ts +13 -0
  20. package/src/lib/event.ts +1 -1
  21. package/src/lib/fakeData.ts +4 -4
  22. package/src/lib/hash.ts +1 -1
  23. package/src/lib/image.ts +2 -2
  24. package/src/lib/{linkInjection.ts → injection.ts} +25 -18
  25. package/src/lib/meta.ts +1 -1
  26. package/src/lib/modal.ts +1 -1
  27. package/src/lib/playlink.ts +1 -1
  28. package/src/lib/routes.ts +9 -0
  29. package/src/lib/splitTest.ts +4 -4
  30. package/src/lib/text.ts +1 -1
  31. package/src/lib/token.ts +3 -0
  32. package/src/lib/tracking.ts +5 -5
  33. package/src/lib/types/global.d.ts +1 -1
  34. package/src/lib/types/injection.d.ts +1 -1
  35. package/src/lib/types/script.d.ts +18 -3
  36. package/src/lib/types/session.d.ts +1 -1
  37. package/src/lib/types/title.d.ts +2 -2
  38. package/src/main.ts +12 -12
  39. package/src/routes/+page.svelte +5 -4
  40. package/src/routes/components/Debugger.svelte +26 -1
  41. package/src/routes/components/Editorial/Editor.svelte +7 -7
  42. package/src/routes/components/Editorial/EditorItem.svelte +1 -1
  43. package/src/routes/components/Editorial/ManualInjection.svelte +8 -8
  44. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +1 -1
  45. package/src/routes/components/Editorial/Search/TitleSearch.svelte +1 -1
  46. package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +3 -2
  47. package/src/routes/components/Editorial/Session.svelte +1 -1
  48. package/src/routes/components/ListTitle.svelte +2 -2
  49. package/src/routes/components/ParticipantModal.svelte +11 -0
  50. package/src/routes/components/Playlinks.svelte +1 -1
  51. package/src/routes/components/Rails/SimilarRail.svelte +9 -1
  52. package/src/routes/components/Rails/TitlesRail.svelte +11 -7
  53. package/src/routes/components/TitleModal.svelte +3 -3
  54. package/src/routes/components/TitlePopover.svelte +1 -1
  55. package/src/tests/lib/{ads.test.js → api/ads.test.js} +17 -14
  56. package/src/tests/lib/api/api.test.js +49 -0
  57. package/src/tests/lib/{auth.test.js → api/auth.test.js} +10 -23
  58. package/src/tests/lib/api/config.test.js +53 -0
  59. package/src/tests/lib/{api.test.js → api/externalPages.test.js} +71 -101
  60. package/src/tests/lib/{search.test.js → api/search.test.js} +10 -9
  61. package/src/tests/lib/{session.test.js → api/session.test.js} +4 -4
  62. package/src/tests/lib/{linkInjection.test.js → injections.test.js} +26 -2
  63. package/src/tests/lib/routes.test.js +15 -0
  64. package/src/tests/routes/+page.test.js +17 -9
  65. package/src/tests/routes/components/Editorial/Editor.test.js +3 -3
  66. package/src/tests/routes/components/Editorial/EditorItem.test.js +1 -1
  67. package/src/tests/routes/components/Editorial/ManualInjection.test.js +2 -2
  68. package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +2 -2
  69. package/src/tests/routes/components/Editorial/Session.test.js +2 -2
  70. package/src/tests/routes/components/ParticipantModal.test.js +35 -0
  71. package/src/tests/routes/components/Rails/{TitleRail.test.js → TitlesRail.test.js} +10 -1
  72. package/src/tests/setup.js +2 -0
  73. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "5.17.0-beta.1",
3
+ "version": "5.18.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
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 = "heading"
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
- let { onclick } = options
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 "./api"
2
- import { hasConsentedTo } from "./consent"
3
- import { apiBaseUrl } from "./constants"
4
- import { TrackingEvent } from "./enums/TrackingEvent"
5
- import { track } from "./tracking"
6
- import type { Campaign, CampaignFormat } from "./types/campaign"
7
- import type { PlaylinkData } from "./types/playlink"
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
- const response = await fetch(apiBaseUrl + `/ads/browse/?region=nl&api-token=${apiToken}`, { headers })
16
+ try {
17
+ const response = await api<Campaign[]>(`/ads/browse/?region=nl&api-token=${apiToken}`)
18
18
 
19
- if (!response.ok) {
20
- track(TrackingEvent.AdsFetchFailed, null, { status: response.status })
21
- throw response
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 './api'
2
- import { apiBaseUrl } from './constants'
3
- import { TrackingEvent } from './enums/TrackingEvent'
4
- import { track } from './tracking'
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
- const response = await fetch(apiBaseUrl + `/external-pages/edit-authorization?api-token=${apiToken}`, {
26
- headers,
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: JSON.stringify(({
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 { apiBaseUrl } from './constants'
3
- import { generateRandomHash, stringToHash } from './hash'
4
- import { getLanguage } from './localization'
5
- import { getDatetime, getPageMetaData } from './meta'
6
- import type { ConfigResponse } from './types/config'
7
- import type { LinkInjectionResponse, LinkInjection } from './types/injection'
8
- import { getFullUrlPath } from './url'
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 fetch(apiBaseUrl + `/external-pages/?api-token=${apiToken}&include_title_details=true${isEditorialMode ? '&editorial_mode_enabled=true' : ''}&language=${language}`, {
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: JSON.stringify({
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 = parsed
47
+ window.PlayPilotLinkInjections.last_successful_fetch = response
56
48
 
57
- return parsed
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 "./api"
2
- import { generateRandomHash } from "./hash"
3
- import type { SessionResponse } from "./types/session"
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 "./types/range"
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
  }
@@ -1,4 +1,4 @@
1
- import type { ConsentKeys, ConsentOptions } from "./types/consent"
1
+ import type { ConsentKeys, ConsentOptions } from './types/consent'
2
2
 
3
3
  export function setConsent(options: ConsentOptions): void {
4
4
  if (!window.PlayPilotLinkInjections) return
@@ -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
- [Language.Danish]: 'Vi kan tjene en kommission, hvis du foretager et køb via disse links. I samarbejde med',
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
  }
@@ -1,7 +1,7 @@
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"
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
@@ -13,6 +13,6 @@ export function stringToHash(string: string): string {
13
13
  return hash.toString(16)
14
14
  }
15
15
 
16
- export function generateRandomHash() {
16
+ export function generateRandomHash(): string {
17
17
  return (Math.random() + 1).toString(36).substring(7)
18
18
  }
package/src/lib/image.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { imageBaseUrl } from "./constants"
2
- import type { ImageDimensions } from "./enums/ImageDimensions"
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 duplicate = injection.duplicate ?? (!injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections))
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("(pointer: coarse)").matches)
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 (!activePopoverInsertedComponent) {
431
- // In some cases a popover lingers even if it should have been removed. This happens sometimes during
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
- unmount(activePopoverInsertedComponent, { outro })
436
+ currentlyHoveredInjection = null
437
+ activePopoverInsertedComponent = null
440
438
 
441
- currentlyHoveredInjection = null
442
- activePopoverInsertedComponent = null
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
@@ -1,4 +1,4 @@
1
- import { getLinkInjectionsParentElement } from './linkInjection'
1
+ import { getLinkInjectionsParentElement } from './injection'
2
2
  import type { ArticleMetaData } from './types/injection'
3
3
 
4
4
  /**
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
 
@@ -1,4 +1,4 @@
1
- import type { PlaylinkData } from "./types/playlink"
1
+ import type { PlaylinkData } from './types/playlink'
2
2
 
3
3
  /**
4
4
  * Merge playlinks of the same provider of BUY and RENT categories into a shared TVOD category.
@@ -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
+ }