@playpilot/tpi 8.14.0 → 8.15.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 +8 -8
- package/package.json +1 -1
- package/src/lib/api/externalPages.ts +0 -7
- package/src/lib/api/titles.ts +10 -0
- package/src/lib/data/translations.ts +5 -0
- package/src/lib/injection.ts +13 -3
- package/src/lib/routes.ts +4 -0
- package/src/lib/types/config.d.ts +5 -0
- package/src/lib/types/explore.d.ts +1 -0
- package/src/routes/components/Description.svelte +1 -0
- package/src/routes/components/Explore/ExploreRouter.svelte +38 -2
- package/src/routes/components/Explore/Routes/ExploreModal.svelte +38 -0
- 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/Modal.svelte +3 -1
- package/src/routes/components/Modals/RailModal.svelte +4 -3
- package/src/routes/components/Modals/TitlesRailModal.svelte +5 -4
- package/src/routes/components/Playlinks/Playlinks.svelte +1 -0
- package/src/routes/components/Title.svelte +12 -4
- package/src/routes/explore/+page.svelte +4 -0
- 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/package.json
CHANGED
|
@@ -26,8 +26,6 @@ export async function fetchLinkInjections(
|
|
|
26
26
|
const apiUrl = `/external-pages/?api-token=${apiToken}&include_title_details=true${isEditorialMode ? '&editorial_mode_enabled=true' : ''}&language=${language}`
|
|
27
27
|
let response: LinkInjectionResponse
|
|
28
28
|
|
|
29
|
-
// We use separate requests when running the AI or setting the editor session vs when only getting the results.
|
|
30
|
-
// For regular requests we use a GET endpoint, but when saving data we POST to the same url.
|
|
31
29
|
if (method === 'POST') {
|
|
32
30
|
const { pageText } = getPageTextAndElements()
|
|
33
31
|
|
|
@@ -44,14 +42,11 @@ export async function fetchLinkInjections(
|
|
|
44
42
|
body: params,
|
|
45
43
|
})
|
|
46
44
|
} else {
|
|
47
|
-
// When getting injections without posting we append the URL of the page to the URL for the request.
|
|
48
|
-
// All other params are only relevant during the POST request.
|
|
49
45
|
response = await api<LinkInjectionResponse>(apiUrl + `&url=${url}`, {
|
|
50
46
|
method: 'GET',
|
|
51
47
|
})
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
// This is used when debugging (using window.PlayPilotLinkInjections.debug())
|
|
55
50
|
window.PlayPilotLinkInjections.last_successful_fetch = response
|
|
56
51
|
|
|
57
52
|
return response
|
|
@@ -67,8 +62,6 @@ export async function pollLinkInjections(
|
|
|
67
62
|
): Promise<LinkInjectionResponse> {
|
|
68
63
|
let currentTry = 0
|
|
69
64
|
|
|
70
|
-
// Clear pollTimeout if it is already running to prevent multiple timeouts from running at the same time
|
|
71
|
-
// This is mostly handy during HMR, but also during navigation changes
|
|
72
65
|
if (pollTimeout) clearTimeout(pollTimeout)
|
|
73
66
|
|
|
74
67
|
const poll = async (resolve: Function, reject: Function): Promise<void> => {
|
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, 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
|
+
}
|
|
@@ -286,6 +286,11 @@ export const translations = {
|
|
|
286
286
|
[Language.Swedish]: 'Utforska',
|
|
287
287
|
[Language.Danish]: 'Udforsk',
|
|
288
288
|
},
|
|
289
|
+
'Page Not Found': {
|
|
290
|
+
[Language.English]: 'Page not found',
|
|
291
|
+
[Language.Swedish]: 'Sidan hittades inte',
|
|
292
|
+
[Language.Danish]: 'Siden blev ikke fundet',
|
|
293
|
+
},
|
|
289
294
|
'Mentioned In This Article': {
|
|
290
295
|
[Language.English]: 'Mentioned in this article',
|
|
291
296
|
[Language.Swedish]: 'Nämnda i den här artikeln',
|
package/src/lib/injection.ts
CHANGED
|
@@ -5,6 +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 { exploreTitleUrl, titleUrl } from './routes'
|
|
8
9
|
import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
|
|
9
10
|
|
|
10
11
|
export const keyDataAttribute = 'data-playpilot-injection-key'
|
|
@@ -202,11 +203,15 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
202
203
|
const injectionElement = document.createElement('span')
|
|
203
204
|
injectionElement.dataset.playpilotInjectionKey = injection.key
|
|
204
205
|
|
|
206
|
+
const openInExplore = !!window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore
|
|
207
|
+
|
|
208
|
+
const href = openInExplore ? exploreTitleUrl(injection.title_details!) : titleUrl(injection.title_details!)
|
|
209
|
+
|
|
205
210
|
const linkElement = document.createElement('a')
|
|
206
211
|
linkElement.dataset.playpilotPosterUrl = injection.title_details?.standing_poster
|
|
207
212
|
linkElement.innerText = injection.title
|
|
208
|
-
linkElement.href =
|
|
209
|
-
linkElement.target = '_blank'
|
|
213
|
+
linkElement.href = href
|
|
214
|
+
linkElement.target = openInExplore ? '' : '_blank'
|
|
210
215
|
linkElement.rel = 'noopener nofollow noreferrer'
|
|
211
216
|
|
|
212
217
|
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
@@ -311,7 +316,12 @@ function addCSSVariablesToLinks(): void {
|
|
|
311
316
|
|
|
312
317
|
function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
313
318
|
window.addEventListener('mousemove', destroyLinkPopoverOnMouseleave)
|
|
314
|
-
|
|
319
|
+
|
|
320
|
+
window.addEventListener('click', (event) => {
|
|
321
|
+
if (window.PlayPilotLinkInjections?.config?.open_tpi_links_in_explore) return
|
|
322
|
+
|
|
323
|
+
openModalForInjectedLink(event, injections)
|
|
324
|
+
})
|
|
315
325
|
|
|
316
326
|
const createdInjectionElements = document.querySelectorAll<HTMLElement>(keySelector)
|
|
317
327
|
|
package/src/lib/routes.ts
CHANGED
|
@@ -4,3 +4,7 @@ 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
|
+
return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=modal&sid=${title.sid}`
|
|
10
|
+
}
|
|
@@ -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_.
|
|
@@ -7,6 +7,8 @@
|
|
|
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 ExploreModal from './Routes/ExploreModal.svelte'
|
|
10
12
|
|
|
11
13
|
const routes: ExploreRoute[] = [
|
|
12
14
|
{
|
|
@@ -20,25 +22,59 @@
|
|
|
20
22
|
key: 'home',
|
|
21
23
|
component: ExploreHome,
|
|
22
24
|
})
|
|
25
|
+
|
|
26
|
+
routes.push({
|
|
27
|
+
key: 'title',
|
|
28
|
+
component: ExploreTitle,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
routes.push({
|
|
32
|
+
key: 'modal',
|
|
33
|
+
component: ExploreHome,
|
|
34
|
+
appendComponent: ExploreModal,
|
|
35
|
+
})
|
|
23
36
|
}
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
const initialRouteKey = getCurrentRouteParam() || routes[0].key
|
|
39
|
+
|
|
40
|
+
let currentRoute: ExploreRoute = $state(routes.find(({ key }) => key === initialRouteKey) || routes[0])
|
|
26
41
|
let searchQuery: string = $state('')
|
|
27
42
|
let filter: ExploreFilter = $state({})
|
|
28
43
|
|
|
29
44
|
const CurrentRouteComponent = $derived(currentRoute.component)
|
|
45
|
+
const CurrentAppendComponent = $derived(currentRoute.appendComponent)
|
|
30
46
|
|
|
31
47
|
$effect(() => {
|
|
32
48
|
if (searchQuery) currentRoute = routes.find(route => route.key === 'results')!
|
|
33
49
|
})
|
|
34
50
|
|
|
35
|
-
function navigate(key: string): void {
|
|
51
|
+
function navigate(key: string, pushState: boolean = true): void {
|
|
36
52
|
currentRoute = routes.find(route => route.key === key) || routes[0]
|
|
37
53
|
|
|
54
|
+
const currentUrl = new URL(document.location.toString())
|
|
55
|
+
|
|
56
|
+
if (key !== 'title' && key !== 'modal') currentUrl.searchParams.delete('sid')
|
|
57
|
+
|
|
58
|
+
if (key === routes[0].key) currentUrl.searchParams.delete('route')
|
|
59
|
+
else currentUrl.searchParams.set('route', currentRoute.key)
|
|
60
|
+
|
|
61
|
+
if (pushState) history.pushState({}, '', currentUrl)
|
|
62
|
+
|
|
38
63
|
track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
|
|
39
64
|
}
|
|
65
|
+
|
|
66
|
+
function getCurrentRouteParam(): string {
|
|
67
|
+
return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onhashchange(): void {
|
|
71
|
+
navigate(getCurrentRouteParam(), false)
|
|
72
|
+
}
|
|
40
73
|
</script>
|
|
41
74
|
|
|
75
|
+
<svelte:window on:popstate={onhashchange} />
|
|
76
|
+
|
|
42
77
|
<ExploreLayout {navigate} bind:searchQuery bind:filter>
|
|
43
78
|
<CurrentRouteComponent {searchQuery} {filter} {navigate} />
|
|
79
|
+
<CurrentAppendComponent {navigate} />
|
|
44
80
|
</ExploreLayout>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
|
|
3
|
+
import { openModal } from '$lib/modal'
|
|
4
|
+
import type { TitleData } from '$lib/types/title'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
navigate?: (key: string, pushState?: boolean) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { navigate = () => null }: Props = $props()
|
|
11
|
+
|
|
12
|
+
openModalViaRoute()
|
|
13
|
+
|
|
14
|
+
async function openModalViaRoute(): Promise<void> {
|
|
15
|
+
const currentUrl = new URL(document.location.toString())
|
|
16
|
+
const sid = currentUrl.searchParams.get('sid')
|
|
17
|
+
|
|
18
|
+
if (!sid) return
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const [title, railTitles] = (await Promise.allSettled([
|
|
22
|
+
fetchTitleBySid(sid),
|
|
23
|
+
fetchSimilarTitles({ sid } as unknown as TitleData)])
|
|
24
|
+
).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
|
|
25
|
+
|
|
26
|
+
openModal({
|
|
27
|
+
type: 'titles-rail',
|
|
28
|
+
data: [(title as TitleData), ...(railTitles as TitleData[])],
|
|
29
|
+
props: {
|
|
30
|
+
onclose: () => navigate('home'),
|
|
31
|
+
pushState: false,
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
} catch {
|
|
35
|
+
navigate('home')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
@@ -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>
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
blur?: boolean
|
|
22
22
|
closeButtonStyle?: 'shadow' | 'flat'
|
|
23
23
|
initialScrollPosition?: number
|
|
24
|
+
pushState?: boolean
|
|
24
25
|
onscroll?: () => void
|
|
25
26
|
onclose?: () => void
|
|
26
27
|
}
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
blur = false,
|
|
36
37
|
closeButtonStyle = 'shadow',
|
|
37
38
|
initialScrollPosition = 0,
|
|
39
|
+
pushState = true,
|
|
38
40
|
onscroll = () => null,
|
|
39
41
|
onclose = () => null,
|
|
40
42
|
}: Props = $props()
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
|
|
64
66
|
// Add modal state to the browser history. This allows us to close to modal when using the back button.
|
|
65
67
|
// Only do this for the very first modal opened in the stack. The back button always fully closes all modals.
|
|
66
|
-
if (!hasPreviousModal) window.history.pushState({ modal: true }, '', historyHash)
|
|
68
|
+
if (pushState && !hasPreviousModal) window.history.pushState({ modal: true }, '', historyHash)
|
|
67
69
|
|
|
68
70
|
requestAnimationFrame(setInitialScrollPosition)
|
|
69
71
|
|
|
@@ -12,10 +12,11 @@
|
|
|
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, ...restProps }: Props = $props()
|
|
19
20
|
|
|
20
21
|
const transitionDuration = 300
|
|
21
22
|
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
}
|
|
36
37
|
</script>
|
|
37
38
|
|
|
38
|
-
<Modal blur>
|
|
39
|
+
<Modal blur {onclose} {...restProps}>
|
|
39
40
|
{#snippet dialog()}
|
|
40
41
|
<div class="rail-modal" style:--transition-duration="{transitionDuration}ms">
|
|
41
42
|
<TinySlider threshold={40} moveThreshold={40} transitionDuration={initialized ? transitionDuration : 0} bind:this={slider}>
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
</div>
|
|
71
72
|
|
|
72
73
|
<div class="close" transition:scale|global>
|
|
73
|
-
<RoundButton size="42px" onclick={() => destroyAllModals()} aria-label="Close">
|
|
74
|
+
<RoundButton size="42px" onclick={() => { onclose(); destroyAllModals() }} aria-label="Close">
|
|
74
75
|
<IconClose size={24} />
|
|
75
76
|
</RoundButton>
|
|
76
77
|
</div>
|
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
import RailModal from './RailModal.svelte'
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
|
-
titles: TitleData[]
|
|
10
|
-
initialIndex?: number
|
|
9
|
+
titles: TitleData[],
|
|
10
|
+
initialIndex?: number,
|
|
11
|
+
onclose?: () => void
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const { titles, initialIndex = 0 }: Props = $props()
|
|
14
|
+
const { titles, initialIndex = 0, onclose = () => null, ...restProps }: Props = $props()
|
|
14
15
|
</script>
|
|
15
16
|
|
|
16
|
-
<RailModal items={titles} {initialIndex} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })}>
|
|
17
|
+
<RailModal items={titles} {initialIndex} {onclose} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })} {...restProps}>
|
|
17
18
|
{#snippet each(title, currentIndex)}
|
|
18
19
|
<Title {title} useVideoBackground={title.sid === titles[currentIndex]?.sid} />
|
|
19
20
|
{/snippet}
|
|
@@ -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
|
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
setTimeout(insertExplore, 100)
|
|
30
30
|
</script>
|
|
31
31
|
|
|
32
|
+
<svelte:head>
|
|
33
|
+
<title>PlayPilot Link Injections - Explore</title>
|
|
34
|
+
</svelte:head>
|
|
35
|
+
|
|
32
36
|
<button onclick={(() => {
|
|
33
37
|
destroyExplore()
|
|
34
38
|
window.PlayPilotLinkInjections.config.explore_use_router = !useExploreRouter()
|
|
@@ -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}&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
|
+
})
|
|
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', () => {
|