@playpilot/tpi 8.14.0-beta.4 → 8.14.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/editorial.mount.js +9 -9
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +9 -9
- package/events.md +5 -0
- package/package.json +2 -2
- package/src/lib/api/titles.ts +0 -10
- package/src/lib/data/translations.ts +9 -4
- package/src/lib/enums/TrackingEvent.ts +3 -0
- package/src/lib/fakeData.ts +3 -3
- package/src/lib/inTextWidgets.ts +43 -0
- package/src/lib/injection.ts +7 -13
- package/src/lib/routes.ts +0 -8
- package/src/lib/types/config.d.ts +0 -5
- package/src/routes/+layout.svelte +2 -0
- package/src/routes/components/Debugger.svelte +0 -5
- package/src/routes/components/Description.svelte +0 -1
- package/src/routes/components/Explore/ExploreLayout.svelte +19 -2
- package/src/routes/components/Explore/ExploreRouter.svelte +2 -55
- package/src/routes/components/Explore/Routes/ExploreResults.svelte +2 -12
- package/src/routes/components/Modals/RailModal.svelte +3 -4
- package/src/routes/components/Playlinks/Playlinks.svelte +0 -1
- package/src/routes/components/Rails/Rail.svelte +4 -4
- package/src/routes/components/Title.svelte +4 -12
- package/src/routes/components/Widgets/InjectionsWidgetRail.svelte +51 -0
- package/src/tests/lib/api/titles.test.js +1 -23
- package/src/tests/lib/inTextWidgets.test.js +160 -0
- package/src/tests/lib/injection.test.js +3 -44
- package/src/tests/lib/routes.test.js +2 -14
- package/src/tests/routes/components/Widgets/InjectionsWidgetRail.test.js +28 -0
- package/src/routes/components/Explore/Routes/ExploreTitle.svelte +0 -94
- package/src/tests/routes/components/Explore/Routes/ExploreTitle.test.js +0 -87
package/events.md
CHANGED
|
@@ -100,6 +100,11 @@ Event | Action | Info | Payload
|
|
|
100
100
|
`ali_display_ad_playlink_click` | _Fires any time the playlink linked to the display ad is clicked_ | | `campaign_name`
|
|
101
101
|
`ali_ads_fetch_failed` | _Fires whenever ads tried to but failed to fetch_ | | `status` (Response status code)
|
|
102
102
|
|
|
103
|
+
## Widgets
|
|
104
|
+
Event | Action | Info | Payload
|
|
105
|
+
--- | --- | --- | ---
|
|
106
|
+
`ali_widget_article_rail_title_click` | _Fires when a title in an tpi rail widget is clicked_ | | `Title`
|
|
107
|
+
|
|
103
108
|
### Various
|
|
104
109
|
Event | Action | Info | Payload
|
|
105
110
|
--- | --- | --- | ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playpilot/tpi",
|
|
3
|
-
"version": "8.14.0
|
|
3
|
+
"version": "8.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"svelte": "5.44.1",
|
|
37
37
|
"svelte-check": "^4.0.0",
|
|
38
38
|
"svelte-preprocess": "^6.0.3",
|
|
39
|
-
"svelte-tiny-slider": "^2.7.
|
|
39
|
+
"svelte-tiny-slider": "^2.7.2",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
41
|
"typescript-eslint": "^8.59.2",
|
|
42
42
|
"vite": "^5.4.21",
|
package/src/lib/api/titles.ts
CHANGED
|
@@ -27,13 +27,3 @@ export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]>
|
|
|
27
27
|
|
|
28
28
|
return response.results
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
export async function fetchTitleBySid(sid: string): Promise<TitleData> {
|
|
32
|
-
const data = await fetchTitles({ sids: sid, no_region_filter: true })
|
|
33
|
-
|
|
34
|
-
const title = data.results[0]
|
|
35
|
-
|
|
36
|
-
if (!title) throw new Error('No title was returned for sid: ' + sid)
|
|
37
|
-
|
|
38
|
-
return title
|
|
39
|
-
}
|
|
@@ -171,6 +171,11 @@ export const translations = {
|
|
|
171
171
|
[Language.Swedish]: 'Upptäck och sök bland alla filmer och tv-serier',
|
|
172
172
|
[Language.Danish]: 'Opdag og søg i alle film og tv-serier',
|
|
173
173
|
},
|
|
174
|
+
'Streaming Guide Disclaimer': {
|
|
175
|
+
[Language.English]: 'In collaboration with <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
|
|
176
|
+
[Language.Swedish]: 'I samarbete med <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
|
|
177
|
+
[Language.Danish]: 'I samarbejde med <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
|
|
178
|
+
},
|
|
174
179
|
'Streaming Guide Description': {
|
|
175
180
|
[Language.English]: 'Find where to watch movies online - the ultimate guide that helps you find the best movies and shows across streaming services.',
|
|
176
181
|
[Language.Swedish]: 'Sök bland alla filmer och serier för att ta reda på var du kan streama dem',
|
|
@@ -281,10 +286,10 @@ export const translations = {
|
|
|
281
286
|
[Language.Swedish]: 'Utforska',
|
|
282
287
|
[Language.Danish]: 'Udforsk',
|
|
283
288
|
},
|
|
284
|
-
'
|
|
285
|
-
[Language.English]: '
|
|
286
|
-
[Language.Swedish]: '
|
|
287
|
-
[Language.Danish]: '
|
|
289
|
+
'Mentioned In This Article': {
|
|
290
|
+
[Language.English]: 'Mentioned in this article',
|
|
291
|
+
[Language.Swedish]: 'Nämnda i den här artikeln',
|
|
292
|
+
[Language.Danish]: 'Nævnt i denne artikel',
|
|
288
293
|
},
|
|
289
294
|
|
|
290
295
|
// List titles
|
|
@@ -55,6 +55,9 @@ export const TrackingEvent = {
|
|
|
55
55
|
SplitTestView: 'ali_split_test_view',
|
|
56
56
|
SplitTestAction: 'ali_split_test_action',
|
|
57
57
|
|
|
58
|
+
// Widgets
|
|
59
|
+
WidgetArticleRailTitleClick: 'ali_widget_article_rail_title_click',
|
|
60
|
+
|
|
58
61
|
// Various
|
|
59
62
|
ShareTitle: 'ali_share_title',
|
|
60
63
|
SaveTitle: 'ali_save_title',
|
package/src/lib/fakeData.ts
CHANGED
|
@@ -49,7 +49,7 @@ export const linkInjections: LinkInjection[] = [{
|
|
|
49
49
|
sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
|
|
50
50
|
playpilot_url: 'https://playpilot.com/movie/example/',
|
|
51
51
|
key: 'some-key-1',
|
|
52
|
-
title_details: title,
|
|
52
|
+
title_details: { ...title, sid: '1' },
|
|
53
53
|
}, {
|
|
54
54
|
sid: '2',
|
|
55
55
|
title: 'The Long Kiss Goodnight',
|
|
@@ -57,7 +57,7 @@ export const linkInjections: LinkInjection[] = [{
|
|
|
57
57
|
playpilot_url: 'https://playpilot.com/movie/example-2/',
|
|
58
58
|
key: 'some-key-2',
|
|
59
59
|
after_article: false,
|
|
60
|
-
title_details: title,
|
|
60
|
+
title_details: { ...title, sid: '2' },
|
|
61
61
|
}, {
|
|
62
62
|
sid: '3',
|
|
63
63
|
title: 'Nobody',
|
|
@@ -65,7 +65,7 @@ export const linkInjections: LinkInjection[] = [{
|
|
|
65
65
|
playpilot_url: 'https://playpilot.com/movie/example-3/',
|
|
66
66
|
key: 'some-key-3',
|
|
67
67
|
after_article: true,
|
|
68
|
-
title_details: title,
|
|
68
|
+
title_details: { ...title, sid: '3' },
|
|
69
69
|
manual: false,
|
|
70
70
|
}, {
|
|
71
71
|
sid: '4',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mount, unmount } from 'svelte'
|
|
2
|
+
import type { LinkInjection } from './types/injection'
|
|
3
|
+
import InjectionsWidgetRail from '../routes/components/Widgets/InjectionsWidgetRail.svelte'
|
|
4
|
+
|
|
5
|
+
const widgets: Record<string, any> = {
|
|
6
|
+
'tpi-rail': InjectionsWidgetRail,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const inTextWidgetSelector = '[data-playpilot-widget]'
|
|
10
|
+
|
|
11
|
+
export let inTextWidgetInsertedComponents: any[] = []
|
|
12
|
+
|
|
13
|
+
export function insertInTextWidgets(linkInjections: LinkInjection[]): void {
|
|
14
|
+
clearInTextWidgets()
|
|
15
|
+
|
|
16
|
+
if (!linkInjections.length) return
|
|
17
|
+
|
|
18
|
+
const targets = document.querySelectorAll<HTMLElement>(inTextWidgetSelector)
|
|
19
|
+
|
|
20
|
+
targets.forEach(target => {
|
|
21
|
+
const widget = target.dataset.playpilotWidget || ''
|
|
22
|
+
|
|
23
|
+
const component = widgets[widget]
|
|
24
|
+
|
|
25
|
+
if (!component) return
|
|
26
|
+
|
|
27
|
+
const insertedComponent = mount(component, {
|
|
28
|
+
target,
|
|
29
|
+
props: {
|
|
30
|
+
linkInjections,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
inTextWidgetInsertedComponents.push(insertedComponent)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function clearInTextWidgets(): void {
|
|
39
|
+
inTextWidgetInsertedComponents.forEach(component => unmount(component))
|
|
40
|
+
document.querySelectorAll('[data-playpilot-widget]').forEach(element => element.innerHTML = '')
|
|
41
|
+
|
|
42
|
+
inTextWidgetInsertedComponents = []
|
|
43
|
+
}
|
package/src/lib/injection.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { destroyAllModals, openModalForInjectedLink } from './modal'
|
|
|
5
5
|
import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverOnMouseleave, isPopoverActive, openPopoverForInjectedLink } from './popover'
|
|
6
6
|
import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
|
|
7
7
|
import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
|
|
8
|
-
import {
|
|
8
|
+
import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
|
|
9
9
|
|
|
10
10
|
export const keyDataAttribute = 'data-playpilot-injection-key'
|
|
11
11
|
export const keySelector = `[${keyDataAttribute}]`
|
|
@@ -169,6 +169,8 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
169
169
|
// The function itself will decide whether or not it should actually insert the component based on the config.
|
|
170
170
|
if (document.querySelector(keySelector)) insertInTextDisclaimer(elements)
|
|
171
171
|
|
|
172
|
+
insertInTextWidgets(foundInjections)
|
|
173
|
+
|
|
172
174
|
return mergedInjections.filter(i => i.title_details).map((injection, index) => {
|
|
173
175
|
const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
|
|
174
176
|
const duplicate = injection.duplicate ?? hasManualEquivalent
|
|
@@ -200,15 +202,11 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
200
202
|
const injectionElement = document.createElement('span')
|
|
201
203
|
injectionElement.dataset.playpilotInjectionKey = injection.key
|
|
202
204
|
|
|
203
|
-
const openInExplore = !!window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore
|
|
204
|
-
|
|
205
|
-
const href = openInExplore ? exploreTitleUrl(injection.title_details!) : titleUrl(injection.title_details!)
|
|
206
|
-
|
|
207
205
|
const linkElement = document.createElement('a')
|
|
208
206
|
linkElement.dataset.playpilotPosterUrl = injection.title_details?.standing_poster
|
|
209
207
|
linkElement.innerText = injection.title
|
|
210
|
-
linkElement.href =
|
|
211
|
-
linkElement.target =
|
|
208
|
+
linkElement.href = injection.playpilot_url
|
|
209
|
+
linkElement.target = '_blank'
|
|
212
210
|
linkElement.rel = 'noopener nofollow noreferrer'
|
|
213
211
|
|
|
214
212
|
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
@@ -313,12 +311,7 @@ function addCSSVariablesToLinks(): void {
|
|
|
313
311
|
|
|
314
312
|
function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
315
313
|
window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
|
|
316
|
-
|
|
317
|
-
window.addEventListener('click', (event) => {
|
|
318
|
-
if (window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore) return
|
|
319
|
-
|
|
320
|
-
openModalForInjectedLink(event, injections)
|
|
321
|
-
})
|
|
314
|
+
window.addEventListener('click', (event) => openModalForInjectedLink(event, injections))
|
|
322
315
|
|
|
323
316
|
const createdInjectionElements = document.querySelectorAll<HTMLElement>(keySelector)
|
|
324
317
|
|
|
@@ -343,6 +336,7 @@ export function clearLinkInjections(): void {
|
|
|
343
336
|
|
|
344
337
|
clearAfterArticlePlaylinks()
|
|
345
338
|
clearInTextDisclaimer()
|
|
339
|
+
clearInTextWidgets()
|
|
346
340
|
destroyAllModals(false)
|
|
347
341
|
destroyLinkPopover(false)
|
|
348
342
|
}
|
package/src/lib/routes.ts
CHANGED
|
@@ -4,11 +4,3 @@ import type { TitleData } from './types/title'
|
|
|
4
4
|
export function titleUrl(title: TitleData): string {
|
|
5
5
|
return `${playPilotBaseUrl}/${title.type}/${title.slug}/`
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
export function exploreTitleUrl(title: TitleData): string {
|
|
9
|
-
if (localStorage.getItem('tpi-open-explore-as-modal') === 'true') {
|
|
10
|
-
return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=modal&sid=${title.sid}`
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=title&sid=${title.sid}`
|
|
14
|
-
}
|
|
@@ -97,11 +97,6 @@ export type ConfigResponse = {
|
|
|
97
97
|
in_text_disclaimer_selector?: string
|
|
98
98
|
in_text_disclaimer_insert_position?: InsertPosition
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
* Open TPI links in explore rather than in modals in the article
|
|
102
|
-
*/
|
|
103
|
-
open_tpi_links_in_explore?: boolean
|
|
104
|
-
|
|
105
100
|
/**
|
|
106
101
|
* These options are all relevant for the Explore component, which can be inserted as a widget on any page or as a modal.
|
|
107
102
|
* `explore_navigation_selector` is used to select the navigation element that should be copied and inserted _after_.
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
<p>De tre ’Jurassic World’-film kunne have givet indtryk af, at der ikke skal meget til, før dinosaurer igen ville kunne dominere kloden. Men det har vist sig ikke at holde stik.</p>
|
|
71
71
|
<p>Het komt elk jaar voor dat films met torenhoge budgetten flink floppen aan de box-office, ongeacht de kwaliteit van de film. Dit is het geval bij een aantal films die dit jaar zijn uitgekomen. Denk aan Mickey 17, Black Bag en de onlangs uitgebrachte animatiefilm Elio. In 2015 was de superheldenfilm Fantastic Four een van de grootste flops.</p>
|
|
72
72
|
|
|
73
|
+
<div data-playpilot-widget="tpi-rail"></div>
|
|
74
|
+
|
|
73
75
|
<h2>A matching link is already present</h2>
|
|
74
76
|
<p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
|
|
75
77
|
|
|
@@ -226,11 +226,6 @@
|
|
|
226
226
|
|
|
227
227
|
<hr />
|
|
228
228
|
|
|
229
|
-
<button onclick={() => { localStorage.setItem('tpi-open-explore-as-modal', 'true'); onrerender() }}>Open links as modal in explore</button>
|
|
230
|
-
<button onclick={() => { localStorage.setItem('tpi-open-explore-as-modal', 'false'); onrerender() }}>Open links as separate page in explore</button>
|
|
231
|
-
|
|
232
|
-
<hr />
|
|
233
|
-
|
|
234
229
|
<button onclick={() => shown = false}>Close</button>
|
|
235
230
|
</div>
|
|
236
231
|
{/if}
|
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
{t('Streaming Guide Subheading')}
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
|
+
<p class="disclaimer">
|
|
56
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
57
|
+
{@html t('Streaming Guide Disclaimer')}
|
|
58
|
+
</p>
|
|
59
|
+
|
|
55
60
|
{#if !useExploreRouter()}
|
|
56
61
|
<p class="description">
|
|
57
62
|
{t('Streaming Guide Description')}
|
|
@@ -117,7 +122,7 @@
|
|
|
117
122
|
.heading,
|
|
118
123
|
.subheading {
|
|
119
124
|
color: theme(text-color);
|
|
120
|
-
font-size: theme(explore-heading-size, clamp(margin(1.5), 5vw, margin(2)));
|
|
125
|
+
font-size: theme(explore-heading-font-size, clamp(margin(1.5), 5vw, margin(2)));
|
|
121
126
|
font-weight: theme(explore-heading-font-weight, font-bold);
|
|
122
127
|
text-transform: theme(explore-heading-text-transform, normal);
|
|
123
128
|
line-height: theme(explore-heading-line-height, 1.5);
|
|
@@ -126,13 +131,25 @@
|
|
|
126
131
|
.subheading {
|
|
127
132
|
margin-top: margin(0.5);
|
|
128
133
|
max-width: margin(15);
|
|
129
|
-
font-size: theme(explore-subheading-size, clamp(margin(1), 2.5vw, margin(1.25)));
|
|
134
|
+
font-size: theme(explore-subheading-font-size, clamp(margin(1), 2.5vw, margin(1.25)));
|
|
130
135
|
|
|
131
136
|
@include desktop {
|
|
132
137
|
max-width: 100%;
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
.disclaimer {
|
|
142
|
+
margin: theme(explore-disclaimer-margin, 0);
|
|
143
|
+
font-size: theme(explore-diclaimer-font-size, font-size-small);
|
|
144
|
+
color: theme(explore-disclaimer-color, text-color-alt);
|
|
145
|
+
opacity: 0.75;
|
|
146
|
+
|
|
147
|
+
:global(a) {
|
|
148
|
+
color: inherit;
|
|
149
|
+
font-style: inherit;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
136
153
|
.description {
|
|
137
154
|
max-width: theme(explore-header-max-width, 600px);
|
|
138
155
|
margin: 0;
|
|
@@ -7,10 +7,6 @@
|
|
|
7
7
|
import ExploreHome from './Routes/ExploreHome.svelte'
|
|
8
8
|
import ExploreResults from './Routes/ExploreResults.svelte'
|
|
9
9
|
import ExploreLayout from './ExploreLayout.svelte'
|
|
10
|
-
import ExploreTitle from './Routes/ExploreTitle.svelte'
|
|
11
|
-
import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
|
|
12
|
-
import { openModal } from '$lib/modal'
|
|
13
|
-
import type { TitleData } from '$lib/types/title'
|
|
14
10
|
|
|
15
11
|
const routes: ExploreRoute[] = [
|
|
16
12
|
{
|
|
@@ -24,74 +20,25 @@
|
|
|
24
20
|
key: 'home',
|
|
25
21
|
component: ExploreHome,
|
|
26
22
|
})
|
|
27
|
-
|
|
28
|
-
routes.push({
|
|
29
|
-
key: 'title',
|
|
30
|
-
component: ExploreTitle,
|
|
31
|
-
})
|
|
32
23
|
}
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let currentRoute: ExploreRoute = $state(routes.find(({ key }) => key === initialRouteKey) || routes[0])
|
|
25
|
+
let currentRoute: ExploreRoute = $state(routes[0])
|
|
37
26
|
let searchQuery: string = $state('')
|
|
38
27
|
let filter: ExploreFilter = $state({})
|
|
39
28
|
|
|
40
29
|
const CurrentRouteComponent = $derived(currentRoute.component)
|
|
41
30
|
|
|
42
|
-
if (initialRouteKey === 'modal') openModalViaRoute()
|
|
43
|
-
|
|
44
31
|
$effect(() => {
|
|
45
32
|
if (searchQuery) currentRoute = routes.find(route => route.key === 'results')!
|
|
46
33
|
})
|
|
47
34
|
|
|
48
|
-
function navigate(key: string
|
|
35
|
+
function navigate(key: string): void {
|
|
49
36
|
currentRoute = routes.find(route => route.key === key) || routes[0]
|
|
50
37
|
|
|
51
|
-
const currentUrl = new URL(document.location.toString())
|
|
52
|
-
|
|
53
|
-
if (key !== 'title' && key !== 'modal') currentUrl.searchParams.delete('sid')
|
|
54
|
-
|
|
55
|
-
if (key === routes[0].key) currentUrl.searchParams.delete('route')
|
|
56
|
-
else currentUrl.searchParams.set('route', currentRoute.key)
|
|
57
|
-
|
|
58
|
-
if (pushState) history.pushState({}, '', currentUrl)
|
|
59
|
-
|
|
60
38
|
track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
|
|
61
39
|
}
|
|
62
|
-
|
|
63
|
-
function onhashchange(): void {
|
|
64
|
-
navigate(getCurrentRouteParam(), false)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getCurrentRouteParam(): string {
|
|
68
|
-
return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// This is temporary while testing, please clean me up later
|
|
72
|
-
async function openModalViaRoute(): Promise<void> {
|
|
73
|
-
const currentUrl = new URL(document.location.toString())
|
|
74
|
-
const sid = currentUrl.searchParams.get('sid')
|
|
75
|
-
|
|
76
|
-
if (!sid) return
|
|
77
|
-
|
|
78
|
-
const [title, railTitles] = (await Promise.allSettled([
|
|
79
|
-
fetchTitleBySid(sid),
|
|
80
|
-
fetchSimilarTitles({ sid } as unknown as TitleData)])
|
|
81
|
-
).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
|
|
82
|
-
|
|
83
|
-
openModal({
|
|
84
|
-
type: 'titles-rail',
|
|
85
|
-
data: [(title as TitleData), ...(railTitles as TitleData[])],
|
|
86
|
-
props: {
|
|
87
|
-
onclose: () => navigate('home'),
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
40
|
</script>
|
|
92
41
|
|
|
93
|
-
<svelte:window on:popstate={onhashchange} />
|
|
94
|
-
|
|
95
42
|
<ExploreLayout {navigate} bind:searchQuery bind:filter>
|
|
96
43
|
<CurrentRouteComponent {searchQuery} {filter} {navigate} />
|
|
97
44
|
</ExploreLayout>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onDestroy } from 'svelte'
|
|
3
2
|
import { fetchTitles } from '$lib/api/titles'
|
|
4
3
|
import { MetaEvent, TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
5
4
|
import { openModal } from '$lib/modal'
|
|
@@ -19,7 +18,8 @@
|
|
|
19
18
|
|
|
20
19
|
interface Props {
|
|
21
20
|
searchQuery?: string,
|
|
22
|
-
filter?: ExploreFilter
|
|
21
|
+
filter?: ExploreFilter,
|
|
22
|
+
navigate?: (key: string) => void
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const { searchQuery = '', filter = {} }: Props = $props()
|
|
@@ -51,10 +51,6 @@
|
|
|
51
51
|
if (filter) setFilter()
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
onDestroy(() => {
|
|
55
|
-
emptyFilter()
|
|
56
|
-
})
|
|
57
|
-
|
|
58
54
|
async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
|
|
59
55
|
latestRequestId += 1
|
|
60
56
|
const requestId = latestRequestId
|
|
@@ -112,12 +108,6 @@
|
|
|
112
108
|
promise = getTitlesForFilter()
|
|
113
109
|
}
|
|
114
110
|
|
|
115
|
-
function emptyFilter(): void {
|
|
116
|
-
for (const key of Object.keys(filter)) {
|
|
117
|
-
delete filter[key]
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
111
|
function resetTitles(): void {
|
|
122
112
|
page = 1
|
|
123
113
|
titles = []
|
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
items: Record<string, any>[]
|
|
13
13
|
initialIndex?: number
|
|
14
14
|
onchange?: (index: number) => void
|
|
15
|
-
onclose?: () => void
|
|
16
15
|
each: Snippet<[item: any, currentIndex: number]>
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const { items, initialIndex = 0, onchange = () => null,
|
|
18
|
+
const { items, initialIndex = 0, onchange = () => null, each }: Props = $props()
|
|
20
19
|
|
|
21
20
|
const transitionDuration = 300
|
|
22
21
|
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
}
|
|
37
36
|
</script>
|
|
38
37
|
|
|
39
|
-
<Modal blur
|
|
38
|
+
<Modal blur>
|
|
40
39
|
{#snippet dialog()}
|
|
41
40
|
<div class="rail-modal" style:--transition-duration="{transitionDuration}ms">
|
|
42
41
|
<TinySlider threshold={40} moveThreshold={40} transitionDuration={initialized ? transitionDuration : 0} bind:this={slider}>
|
|
@@ -71,7 +70,7 @@
|
|
|
71
70
|
</div>
|
|
72
71
|
|
|
73
72
|
<div class="close" transition:scale|global>
|
|
74
|
-
<RoundButton size="42px" onclick={() =>
|
|
73
|
+
<RoundButton size="42px" onclick={() => destroyAllModals()} aria-label="Close">
|
|
75
74
|
<IconClose size={24} />
|
|
76
75
|
</RoundButton>
|
|
77
76
|
</div>
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
.rail {
|
|
59
59
|
--gap: var(--rail-gap, #{margin(0.5)});
|
|
60
60
|
position: relative;
|
|
61
|
-
width: calc(100% + margin(2)
|
|
62
|
-
margin: 0 margin(-1);
|
|
61
|
+
width: calc(100% + var(--rail-margin, margin(1)) * 2);
|
|
62
|
+
margin: 0 calc(var(--rail-margin, margin(1)) * -1);
|
|
63
63
|
|
|
64
64
|
:global(.slider) {
|
|
65
|
-
padding: 0 margin(1);
|
|
65
|
+
padding: 0 var(--rail-margin, margin(1));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
:global(.slider-content > :last-child) {
|
|
69
|
-
margin-right: margin(2);
|
|
69
|
+
margin-right: calc(var(--rail-margin, margin(1)) * 2);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
|
|
136
136
|
.content {
|
|
137
137
|
position: relative;
|
|
138
|
-
padding:
|
|
138
|
+
padding: margin(1);
|
|
139
139
|
color: theme(detail-text-color, text-color);
|
|
140
140
|
font-family: theme(detail-font-family, font-family);
|
|
141
141
|
font-weight: theme(detail-font-weight, normal);
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
|
|
173
173
|
@include desktop {
|
|
174
174
|
display: block;
|
|
175
|
-
padding-top:
|
|
175
|
+
padding-top: margin(3);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
.small & {
|
|
@@ -217,11 +217,6 @@
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
.main {
|
|
221
|
-
z-index: 1;
|
|
222
|
-
position: relative;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
220
|
.imdb {
|
|
226
221
|
display: flex;
|
|
227
222
|
align-items: center;
|
|
@@ -239,8 +234,6 @@
|
|
|
239
234
|
}
|
|
240
235
|
|
|
241
236
|
.actions {
|
|
242
|
-
z-index: 1;
|
|
243
|
-
position: relative;
|
|
244
237
|
grid-area: actions;
|
|
245
238
|
display: flex;
|
|
246
239
|
gap: margin(0.5);
|
|
@@ -252,15 +245,14 @@
|
|
|
252
245
|
top: 0;
|
|
253
246
|
left: 0;
|
|
254
247
|
width: 100%;
|
|
255
|
-
height:
|
|
248
|
+
height: margin(20);
|
|
256
249
|
border-radius: theme(detail-background-border-radius, 0px);
|
|
257
250
|
overflow: hidden;
|
|
258
251
|
background: theme(detail-background, lighter);
|
|
259
252
|
mask-image: linear-gradient(to bottom, black 40%, transparent);
|
|
260
|
-
opacity: theme(detail-background-opacity, 1);
|
|
261
253
|
|
|
262
254
|
@include desktop() {
|
|
263
|
-
height:
|
|
255
|
+
height: margin(12);
|
|
264
256
|
mask-image: linear-gradient(to bottom, black 60%, transparent);
|
|
265
257
|
}
|
|
266
258
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
|
+
import { t } from '$lib/localization'
|
|
4
|
+
import { track } from '$lib/tracking'
|
|
5
|
+
import type { LinkInjection } from '$lib/types/injection'
|
|
6
|
+
import type { TitleData } from '$lib/types/title'
|
|
7
|
+
import TitlesRail from '../Rails/TitlesRail.svelte'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
linkInjections: LinkInjection[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { linkInjections }: Props = $props()
|
|
14
|
+
|
|
15
|
+
let element: HTMLElement | null = $state(null)
|
|
16
|
+
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
const heading = $derived(element?.parentNode?.dataset.heading || t('Mentioned In This Article'))
|
|
19
|
+
|
|
20
|
+
const titles: TitleData[] = $derived.by(() => {
|
|
21
|
+
const uniqueTitles: TitleData[] = []
|
|
22
|
+
|
|
23
|
+
linkInjections.forEach(injection => {
|
|
24
|
+
const title = injection.title_details!
|
|
25
|
+
|
|
26
|
+
if (!title) return
|
|
27
|
+
if (uniqueTitles.some(t => t.sid === title.sid)) return
|
|
28
|
+
|
|
29
|
+
uniqueTitles.push(title)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return uniqueTitles
|
|
33
|
+
})
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
{#if titles.length}
|
|
37
|
+
<div class="widget" bind:this={element} data-testid="widget">
|
|
38
|
+
<TitlesRail {titles} {heading} onclick={(title) => track(TrackingEvent.WidgetArticleRailTitleClick, title)} />
|
|
39
|
+
</div>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
<style lang="scss">
|
|
43
|
+
.widget {
|
|
44
|
+
--rail-margin: 0;
|
|
45
|
+
--playpilot-rails-arrow-background: black;
|
|
46
|
+
--playpilot-detail-background-light: black;
|
|
47
|
+
--playpilot-rail-title-text-color: currentColor;
|
|
48
|
+
--playpilot-rail-text-color: currentColor;
|
|
49
|
+
margin: margin(1) 0;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { api } from '$lib/api/api'
|
|
4
|
-
import { fetchSimilarTitles,
|
|
4
|
+
import { fetchSimilarTitles, fetchTitles } from '$lib/api/titles'
|
|
5
5
|
import { title } from '$lib/fakeData'
|
|
6
6
|
import { getApiToken } from '$lib/token'
|
|
7
7
|
import { fakeFetch } from '../../helpers'
|
|
@@ -74,26 +74,4 @@ describe('$lib/api/titles', () => {
|
|
|
74
74
|
await expect(async () => await fetchSimilarTitles(title)).rejects.toThrow()
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
|
-
|
|
78
|
-
describe('fetchTitleBySid', () => {
|
|
79
|
-
it('Should call api with given sid and return the first result', async () => {
|
|
80
|
-
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
81
|
-
|
|
82
|
-
const response = await fetchTitleBySid(title.sid)
|
|
83
|
-
|
|
84
|
-
expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&sids=${title.sid}&no_region_filter=true&language=en-US&include_count=false`)
|
|
85
|
-
expect(response).toEqual(title)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('Should throw when no title is returned', async () => {
|
|
89
|
-
vi.mocked(api).mockResolvedValueOnce({ results: [] })
|
|
90
|
-
|
|
91
|
-
await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow('No title was returned')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('Should throw when api returns error', async () => {
|
|
95
|
-
vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
|
|
96
|
-
await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow()
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
77
|
})
|