@playpilot/tpi 8.13.0-beta.2 → 8.14.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editorial.mount.js +9 -9
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +9 -9
- package/events.md +0 -5
- package/package.json +2 -2
- package/src/lib/api/titles.ts +10 -0
- package/src/lib/data/translations.ts +4 -4
- package/src/lib/enums/TrackingEvent.ts +0 -3
- package/src/lib/fakeData.ts +3 -3
- package/src/lib/injection.ts +13 -7
- package/src/lib/routes.ts +8 -0
- package/src/lib/types/config.d.ts +5 -0
- package/src/routes/+layout.svelte +0 -2
- package/src/routes/components/Debugger.svelte +5 -0
- package/src/routes/components/Description.svelte +1 -0
- package/src/routes/components/Explore/ExploreRouter.svelte +58 -2
- package/src/routes/components/Explore/Routes/ExploreResults.svelte +12 -2
- package/src/routes/components/Explore/Routes/ExploreTitle.svelte +94 -0
- package/src/routes/components/Modals/RailModal.svelte +6 -3
- package/src/routes/components/Playlinks/Playlinks.svelte +1 -0
- package/src/routes/components/Rails/Rail.svelte +4 -4
- package/src/routes/components/Title.svelte +12 -4
- package/src/tests/lib/api/titles.test.js +23 -1
- package/src/tests/lib/injection.test.js +44 -3
- package/src/tests/lib/routes.test.js +14 -2
- package/src/tests/routes/components/Explore/Routes/ExploreTitle.test.js +87 -0
- package/src/lib/inTextWidgets.ts +0 -43
- package/src/routes/components/Widgets/InjectionsWidgetRail.svelte +0 -51
- package/src/tests/lib/inTextWidgets.test.js +0 -160
- package/src/tests/routes/components/Widgets/InjectionsWidgetRail.test.js +0 -28
package/events.md
CHANGED
|
@@ -100,11 +100,6 @@ 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
|
-
|
|
108
103
|
### Various
|
|
109
104
|
Event | Action | Info | Payload
|
|
110
105
|
--- | --- | --- | ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playpilot/tpi",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.14.0-beta.2",
|
|
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.1",
|
|
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,3 +27,13 @@ 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 })
|
|
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
|
+
}
|
|
@@ -281,10 +281,10 @@ export const translations = {
|
|
|
281
281
|
[Language.Swedish]: 'Utforska',
|
|
282
282
|
[Language.Danish]: 'Udforsk',
|
|
283
283
|
},
|
|
284
|
-
'
|
|
285
|
-
[Language.English]: '
|
|
286
|
-
[Language.Swedish]: '
|
|
287
|
-
[Language.Danish]: '
|
|
284
|
+
'Page Not Found': {
|
|
285
|
+
[Language.English]: 'Page not found',
|
|
286
|
+
[Language.Swedish]: 'Sidan hittades inte',
|
|
287
|
+
[Language.Danish]: 'Siden blev ikke fundet',
|
|
288
288
|
},
|
|
289
289
|
|
|
290
290
|
// List titles
|
|
@@ -55,9 +55,6 @@ 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
|
-
|
|
61
58
|
// Various
|
|
62
59
|
ShareTitle: 'ali_share_title',
|
|
63
60
|
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:
|
|
52
|
+
title_details: title,
|
|
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:
|
|
60
|
+
title_details: title,
|
|
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:
|
|
68
|
+
title_details: title,
|
|
69
69
|
manual: false,
|
|
70
70
|
}, {
|
|
71
71
|
sid: '4',
|
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 { exploreTitleUrl, titleUrl } from './routes'
|
|
9
9
|
|
|
10
10
|
export const keyDataAttribute = 'data-playpilot-injection-key'
|
|
11
11
|
export const keySelector = `[${keyDataAttribute}]`
|
|
@@ -169,8 +169,6 @@ 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
|
-
|
|
174
172
|
return mergedInjections.filter(i => i.title_details).map((injection, index) => {
|
|
175
173
|
const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
|
|
176
174
|
const duplicate = injection.duplicate ?? hasManualEquivalent
|
|
@@ -202,11 +200,15 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
202
200
|
const injectionElement = document.createElement('span')
|
|
203
201
|
injectionElement.dataset.playpilotInjectionKey = injection.key
|
|
204
202
|
|
|
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
|
+
|
|
205
207
|
const linkElement = document.createElement('a')
|
|
206
208
|
linkElement.dataset.playpilotPosterUrl = injection.title_details?.standing_poster
|
|
207
209
|
linkElement.innerText = injection.title
|
|
208
|
-
linkElement.href =
|
|
209
|
-
linkElement.target = '_blank'
|
|
210
|
+
linkElement.href = href
|
|
211
|
+
linkElement.target = openInExplore ? '' : '_blank'
|
|
210
212
|
linkElement.rel = 'noopener nofollow noreferrer'
|
|
211
213
|
|
|
212
214
|
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
@@ -311,7 +313,12 @@ function addCSSVariablesToLinks(): void {
|
|
|
311
313
|
|
|
312
314
|
function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
313
315
|
window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
|
|
314
|
-
|
|
316
|
+
|
|
317
|
+
window.addEventListener('click', (event) => {
|
|
318
|
+
if (window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore) return
|
|
319
|
+
|
|
320
|
+
openModalForInjectedLink(event, injections)
|
|
321
|
+
})
|
|
315
322
|
|
|
316
323
|
const createdInjectionElements = document.querySelectorAll<HTMLElement>(keySelector)
|
|
317
324
|
|
|
@@ -336,7 +343,6 @@ export function clearLinkInjections(): void {
|
|
|
336
343
|
|
|
337
344
|
clearAfterArticlePlaylinks()
|
|
338
345
|
clearInTextDisclaimer()
|
|
339
|
-
clearInTextWidgets()
|
|
340
346
|
destroyAllModals(false)
|
|
341
347
|
destroyLinkPopover(false)
|
|
342
348
|
}
|
package/src/lib/routes.ts
CHANGED
|
@@ -4,3 +4,11 @@ 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,6 +97,11 @@ 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
|
+
|
|
100
105
|
/**
|
|
101
106
|
* These options are all relevant for the Explore component, which can be inserted as a widget on any page or as a modal.
|
|
102
107
|
* `explore_navigation_selector` is used to select the navigation element that should be copied and inserted _after_.
|
|
@@ -70,8 +70,6 @@
|
|
|
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
|
-
|
|
75
73
|
<h2>A matching link is already present</h2>
|
|
76
74
|
<p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
|
|
77
75
|
|
|
@@ -226,6 +226,11 @@
|
|
|
226
226
|
|
|
227
227
|
<hr />
|
|
228
228
|
|
|
229
|
+
<button onclick={() => localStorage.setItem('tpi-open-explore-as-modal', 'true')}>Open links as modal in explore</button>
|
|
230
|
+
<button onclick={() => localStorage.setItem('tpi-open-explore-as-modal', 'false')}>Open links as separate page in explore</button>
|
|
231
|
+
|
|
232
|
+
<hr />
|
|
233
|
+
|
|
229
234
|
<button onclick={() => shown = false}>Close</button>
|
|
230
235
|
</div>
|
|
231
236
|
{/if}
|
|
@@ -7,6 +7,10 @@
|
|
|
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'
|
|
10
14
|
|
|
11
15
|
const routes: ExploreRoute[] = [
|
|
12
16
|
{
|
|
@@ -20,25 +24,77 @@
|
|
|
20
24
|
key: 'home',
|
|
21
25
|
component: ExploreHome,
|
|
22
26
|
})
|
|
27
|
+
|
|
28
|
+
routes.push({
|
|
29
|
+
key: 'title',
|
|
30
|
+
component: ExploreTitle,
|
|
31
|
+
})
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
const initialRouteKey = getCurrentRouteParam() || routes[0].key
|
|
35
|
+
|
|
36
|
+
let currentRoute: ExploreRoute = $state(routes.find(({ key }) => key === initialRouteKey) || routes[0])
|
|
26
37
|
let searchQuery: string = $state('')
|
|
27
38
|
let filter: ExploreFilter = $state({})
|
|
28
39
|
|
|
29
40
|
const CurrentRouteComponent = $derived(currentRoute.component)
|
|
30
41
|
|
|
42
|
+
if (initialRouteKey === 'modal') openModalViaRoute()
|
|
43
|
+
|
|
31
44
|
$effect(() => {
|
|
32
45
|
if (searchQuery) currentRoute = routes.find(route => route.key === 'results')!
|
|
33
46
|
})
|
|
34
47
|
|
|
35
|
-
function navigate(key: string): void {
|
|
48
|
+
function navigate(key: string, pushState: boolean = true): void {
|
|
36
49
|
currentRoute = routes.find(route => route.key === key) || routes[0]
|
|
37
50
|
|
|
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) {
|
|
59
|
+
history.pushState({}, '', currentUrl)
|
|
60
|
+
console.log('navigate push state')
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
|
|
39
64
|
}
|
|
65
|
+
|
|
66
|
+
function onhashchange(): void {
|
|
67
|
+
navigate(getCurrentRouteParam(), false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getCurrentRouteParam(): string {
|
|
71
|
+
return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// This is temporary while testing, please clean me up later
|
|
75
|
+
async function openModalViaRoute(): Promise<void> {
|
|
76
|
+
const currentUrl = new URL(document.location.toString())
|
|
77
|
+
const sid = currentUrl.searchParams.get('sid')
|
|
78
|
+
|
|
79
|
+
if (!sid) return
|
|
80
|
+
|
|
81
|
+
const [title, railTitles] = (await Promise.allSettled([
|
|
82
|
+
fetchTitleBySid(sid),
|
|
83
|
+
fetchSimilarTitles({ sid } as unknown as TitleData)])
|
|
84
|
+
).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
|
|
85
|
+
|
|
86
|
+
openModal({
|
|
87
|
+
type: 'titles-rail',
|
|
88
|
+
data: [(title as TitleData), ...(railTitles as TitleData[])],
|
|
89
|
+
props: {
|
|
90
|
+
onclose: () => navigate('home'),
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
40
94
|
</script>
|
|
41
95
|
|
|
96
|
+
<svelte:window on:popstate={onhashchange} />
|
|
97
|
+
|
|
42
98
|
<ExploreLayout {navigate} bind:searchQuery bind:filter>
|
|
43
99
|
<CurrentRouteComponent {searchQuery} {filter} {navigate} />
|
|
44
100
|
</ExploreLayout>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte'
|
|
2
3
|
import { fetchTitles } from '$lib/api/titles'
|
|
3
4
|
import { MetaEvent, TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
4
5
|
import { openModal } from '$lib/modal'
|
|
@@ -18,8 +19,7 @@
|
|
|
18
19
|
|
|
19
20
|
interface Props {
|
|
20
21
|
searchQuery?: string,
|
|
21
|
-
filter?: ExploreFilter
|
|
22
|
-
navigate?: (key: string) => void
|
|
22
|
+
filter?: ExploreFilter
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const { searchQuery = '', filter = {} }: Props = $props()
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
if (filter) setFilter()
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
onDestroy(() => {
|
|
55
|
+
emptyFilter()
|
|
56
|
+
})
|
|
57
|
+
|
|
54
58
|
async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
|
|
55
59
|
latestRequestId += 1
|
|
56
60
|
const requestId = latestRequestId
|
|
@@ -108,6 +112,12 @@
|
|
|
108
112
|
promise = getTitlesForFilter()
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function emptyFilter(): void {
|
|
116
|
+
for (const key of Object.keys(filter)) {
|
|
117
|
+
delete filter[key]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
function resetTitles(): void {
|
|
112
122
|
page = 1
|
|
113
123
|
titles = []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchTitleBySid } from '$lib/api/titles'
|
|
3
|
+
import { mobileBreakpoint } from '$lib/constants'
|
|
4
|
+
import { t } from '$lib/localization'
|
|
5
|
+
import Button from '../../Button.svelte'
|
|
6
|
+
import IconArrow from '../../Icons/IconArrow.svelte'
|
|
7
|
+
import Title from '../../Title.svelte'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
navigate?: (key: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { navigate = () => null }: Props = $props()
|
|
14
|
+
|
|
15
|
+
let screenWidth = $state(window.innerWidth)
|
|
16
|
+
|
|
17
|
+
const isMobile = $derived(screenWidth < mobileBreakpoint)
|
|
18
|
+
const sid = new URL(document.location.toString()).searchParams.get('sid')
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<svelte:window bind:innerWidth={screenWidth} />
|
|
22
|
+
|
|
23
|
+
{#snippet empty()}
|
|
24
|
+
{@render back()}
|
|
25
|
+
|
|
26
|
+
<p class="empty">{t('Page Not Found')}</p>
|
|
27
|
+
{/snippet}
|
|
28
|
+
|
|
29
|
+
{#snippet back(offset = false)}
|
|
30
|
+
<div class="back" class:offset>
|
|
31
|
+
<Button variant="link" onclick={() => { navigate('home') }}>
|
|
32
|
+
<IconArrow direction="left" />
|
|
33
|
+
{t('Home')}
|
|
34
|
+
</Button>
|
|
35
|
+
</div>
|
|
36
|
+
{/snippet}
|
|
37
|
+
|
|
38
|
+
{#if sid}
|
|
39
|
+
{#await fetchTitleBySid(sid)}
|
|
40
|
+
{@render back()}
|
|
41
|
+
|
|
42
|
+
Loading...
|
|
43
|
+
{:then title}
|
|
44
|
+
{@render back(true)}
|
|
45
|
+
|
|
46
|
+
<div class="title" data-testid="title">
|
|
47
|
+
<Title {title} useVideoBackground={isMobile} />
|
|
48
|
+
</div>
|
|
49
|
+
{:catch}
|
|
50
|
+
{@render empty()}
|
|
51
|
+
{/await}
|
|
52
|
+
{:else}
|
|
53
|
+
{@render empty()}
|
|
54
|
+
{/if}
|
|
55
|
+
|
|
56
|
+
<style lang="scss">
|
|
57
|
+
.title {
|
|
58
|
+
--playpilot-description-max-width: 600px;
|
|
59
|
+
--playpilot-playlinks-max-width: 600px;
|
|
60
|
+
position: relative;
|
|
61
|
+
border-radius: theme(border-radius);
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
margin-top: margin(1);
|
|
64
|
+
|
|
65
|
+
@include desktop() {
|
|
66
|
+
--playpilot-detail-header-offset: #{margin(5)};
|
|
67
|
+
--playpilot-detail-background-height: #{margin(35)};
|
|
68
|
+
--playpilot-detail-background-opacity: 0.5;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.back {
|
|
73
|
+
--playpilot-button-text-color: white;
|
|
74
|
+
font-weight: theme(font-bold);
|
|
75
|
+
|
|
76
|
+
&.offset {
|
|
77
|
+
z-index: 1;
|
|
78
|
+
position: absolute;
|
|
79
|
+
margin-left: margin(1);
|
|
80
|
+
margin-top: margin(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.empty {
|
|
85
|
+
margin-top: margin(1);
|
|
86
|
+
padding: margin(1);
|
|
87
|
+
border: 1px solid theme(content);
|
|
88
|
+
max-width: margin(20);
|
|
89
|
+
border-radius: theme(border-radius);
|
|
90
|
+
font-size: theme(font-size-large);
|
|
91
|
+
font-weight: theme(font-bold);
|
|
92
|
+
color: theme(text-color);
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -12,13 +12,16 @@
|
|
|
12
12
|
items: Record<string, any>[]
|
|
13
13
|
initialIndex?: number
|
|
14
14
|
onchange?: (index: number) => void
|
|
15
|
+
onclose?: () => void
|
|
15
16
|
each: Snippet<[item: any, currentIndex: number]>
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const { items, initialIndex = 0, onchange = () => null, each }: Props = $props()
|
|
19
|
+
const { items, initialIndex = 0, onchange = () => null, onclose = () => null, each }: Props = $props()
|
|
19
20
|
|
|
20
21
|
const transitionDuration = 300
|
|
21
22
|
|
|
23
|
+
console.log({onclose})
|
|
24
|
+
|
|
22
25
|
let slider: TinySlider
|
|
23
26
|
let initialized = $state(false)
|
|
24
27
|
|
|
@@ -35,7 +38,7 @@
|
|
|
35
38
|
}
|
|
36
39
|
</script>
|
|
37
40
|
|
|
38
|
-
<Modal blur>
|
|
41
|
+
<Modal blur {onclose}>
|
|
39
42
|
{#snippet dialog()}
|
|
40
43
|
<div class="rail-modal" style:--transition-duration="{transitionDuration}ms">
|
|
41
44
|
<TinySlider threshold={40} moveThreshold={40} transitionDuration={initialized ? transitionDuration : 0} bind:this={slider}>
|
|
@@ -70,7 +73,7 @@
|
|
|
70
73
|
</div>
|
|
71
74
|
|
|
72
75
|
<div class="close" transition:scale|global>
|
|
73
|
-
<RoundButton size="42px" onclick={() => destroyAllModals()} aria-label="Close">
|
|
76
|
+
<RoundButton size="42px" onclick={() => { onclose(); destroyAllModals() }} aria-label="Close">
|
|
74
77
|
<IconClose size={24} />
|
|
75
78
|
</RoundButton>
|
|
76
79
|
</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% +
|
|
62
|
-
margin: 0
|
|
61
|
+
width: calc(100% + margin(2));
|
|
62
|
+
margin: 0 margin(-1);
|
|
63
63
|
|
|
64
64
|
:global(.slider) {
|
|
65
|
-
padding: 0
|
|
65
|
+
padding: 0 margin(1);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
:global(.slider-content > :last-child) {
|
|
69
|
-
margin-right:
|
|
69
|
+
margin-right: margin(2);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
|
|
136
136
|
.content {
|
|
137
137
|
position: relative;
|
|
138
|
-
padding: margin(1);
|
|
138
|
+
padding: theme(detail-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: margin(3);
|
|
175
|
+
padding-top: theme(title-header-offset, margin(3));
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
.small & {
|
|
@@ -217,6 +217,11 @@
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
.main {
|
|
221
|
+
z-index: 1;
|
|
222
|
+
position: relative;
|
|
223
|
+
}
|
|
224
|
+
|
|
220
225
|
.imdb {
|
|
221
226
|
display: flex;
|
|
222
227
|
align-items: center;
|
|
@@ -234,6 +239,8 @@
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
.actions {
|
|
242
|
+
z-index: 1;
|
|
243
|
+
position: relative;
|
|
237
244
|
grid-area: actions;
|
|
238
245
|
display: flex;
|
|
239
246
|
gap: margin(0.5);
|
|
@@ -245,14 +252,15 @@
|
|
|
245
252
|
top: 0;
|
|
246
253
|
left: 0;
|
|
247
254
|
width: 100%;
|
|
248
|
-
height: margin(20);
|
|
255
|
+
height: theme(detail-background-height, margin(20));
|
|
249
256
|
border-radius: theme(detail-background-border-radius, 0px);
|
|
250
257
|
overflow: hidden;
|
|
251
258
|
background: theme(detail-background, lighter);
|
|
252
259
|
mask-image: linear-gradient(to bottom, black 40%, transparent);
|
|
260
|
+
opacity: theme(detail-background-opacity, 1);
|
|
253
261
|
|
|
254
262
|
@include desktop() {
|
|
255
|
-
height: margin(12);
|
|
263
|
+
height: theme(detail-background-height, margin(12));
|
|
256
264
|
mask-image: linear-gradient(to bottom, black 60%, transparent);
|
|
257
265
|
}
|
|
258
266
|
|
|
@@ -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, fetchTitles } from '$lib/api/titles'
|
|
4
|
+
import { fetchSimilarTitles, fetchTitleBySid, 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,4 +74,26 @@ 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}&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
|
+
})
|
|
77
99
|
})
|
|
@@ -6,6 +6,7 @@ import { mount, unmount } from 'svelte'
|
|
|
6
6
|
import { fakeFetch, generateInjection } from '../helpers'
|
|
7
7
|
import { openModalForInjectedLink } from '$lib/modal'
|
|
8
8
|
import { getLinkInjectionElements } from '$lib/injectionElements'
|
|
9
|
+
import { titleUrl } from '$lib/routes'
|
|
9
10
|
|
|
10
11
|
vi.mock('svelte', () => ({
|
|
11
12
|
mount: vi.fn(),
|
|
@@ -64,8 +65,9 @@ describe('injection.ts', () => {
|
|
|
64
65
|
|
|
65
66
|
const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
|
|
66
67
|
|
|
68
|
+
// @ts-ignore
|
|
69
|
+
expect(link.href).toBe(titleUrl(injection.title_details))
|
|
67
70
|
expect(link.innerText).toBe(injection.title)
|
|
68
|
-
expect(link.href).toBe(injection.playpilot_url)
|
|
69
71
|
})
|
|
70
72
|
|
|
71
73
|
it('Should replace given words as expected when more than 1 injection per sentence is present', () => {
|
|
@@ -83,11 +85,13 @@ describe('injection.ts', () => {
|
|
|
83
85
|
|
|
84
86
|
const links = /** @type {HTMLAnchorElement[]} */ (Array.from(document.querySelectorAll('a')))
|
|
85
87
|
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
expect(links[0].href).toBe(titleUrl(linkInjections[0].title_details))
|
|
86
90
|
expect(links[0].innerText).toBe(linkInjections[0].title)
|
|
87
|
-
expect(links[0].href).toBe(linkInjections[0].playpilot_url)
|
|
88
91
|
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
expect(links[1].href).toBe(titleUrl(linkInjections[1].title_details))
|
|
89
94
|
expect(links[1].innerText).toBe(linkInjections[1].title)
|
|
90
|
-
expect(links[1].href).toBe(linkInjections[1].playpilot_url)
|
|
91
95
|
})
|
|
92
96
|
|
|
93
97
|
it('Should ignore injections that are marked as inactive', () => {
|
|
@@ -946,6 +950,43 @@ describe('injection.ts', () => {
|
|
|
946
950
|
|
|
947
951
|
expect(document.querySelector('a')?.closest('[data-playpilot-injection-key]')).toBeTruthy()
|
|
948
952
|
})
|
|
953
|
+
|
|
954
|
+
describe('config.open_tpi_links_in_explore', () => {
|
|
955
|
+
beforeEach(() => {
|
|
956
|
+
window.PlayPilotLinkInjections.config = {
|
|
957
|
+
open_tpi_links_in_explore: true,
|
|
958
|
+
explore_navigation_path: 'https://some-path.com/explore',
|
|
959
|
+
}
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
it('Should use href with explore links if open_tpi_links_in_explore is true', () => {
|
|
963
|
+
const injection = generateInjection('This is a sentence with an injection.', 'an injection')
|
|
964
|
+
|
|
965
|
+
document.body.innerHTML = `<p>${injection.sentence}</p>`
|
|
966
|
+
|
|
967
|
+
const elements = Array.from(document.querySelectorAll('p'))
|
|
968
|
+
|
|
969
|
+
injectLinksInDocument(elements, { aiInjections: [injection], manualInjections: [] })
|
|
970
|
+
|
|
971
|
+
const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
|
|
972
|
+
expect(link.href).toBe(window.PlayPilotLinkInjections.config.explore_navigation_path + `?route=title&sid=${injection.title_details?.sid}`)
|
|
973
|
+
expect(link.target).not.toBeTruthy()
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
it('Should not open modal when link is clicked when open_tpi_links_in_explore is true', async () => {
|
|
977
|
+
document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
|
|
978
|
+
|
|
979
|
+
const elements = Array.from(document.body.querySelectorAll('p'))
|
|
980
|
+
const injection = generateInjection('This is a sentence with an injection.', 'an injection')
|
|
981
|
+
|
|
982
|
+
injectLinksInDocument(elements, { aiInjections: [injection], manualInjections: [] })
|
|
983
|
+
|
|
984
|
+
const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
|
|
985
|
+
await fireEvent.click(link)
|
|
986
|
+
|
|
987
|
+
expect(openModalForInjectedLink).not.toHaveBeenCalled()
|
|
988
|
+
})
|
|
989
|
+
})
|
|
949
990
|
})
|
|
950
991
|
|
|
951
992
|
describe('clearLinkInjections', () => {
|