@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.
Files changed (30) hide show
  1. package/dist/editorial.mount.js +9 -9
  2. package/dist/link-injections.js +1 -1
  3. package/dist/mount.js +9 -9
  4. package/events.md +0 -5
  5. package/package.json +2 -2
  6. package/src/lib/api/titles.ts +10 -0
  7. package/src/lib/data/translations.ts +4 -4
  8. package/src/lib/enums/TrackingEvent.ts +0 -3
  9. package/src/lib/fakeData.ts +3 -3
  10. package/src/lib/injection.ts +13 -7
  11. package/src/lib/routes.ts +8 -0
  12. package/src/lib/types/config.d.ts +5 -0
  13. package/src/routes/+layout.svelte +0 -2
  14. package/src/routes/components/Debugger.svelte +5 -0
  15. package/src/routes/components/Description.svelte +1 -0
  16. package/src/routes/components/Explore/ExploreRouter.svelte +58 -2
  17. package/src/routes/components/Explore/Routes/ExploreResults.svelte +12 -2
  18. package/src/routes/components/Explore/Routes/ExploreTitle.svelte +94 -0
  19. package/src/routes/components/Modals/RailModal.svelte +6 -3
  20. package/src/routes/components/Playlinks/Playlinks.svelte +1 -0
  21. package/src/routes/components/Rails/Rail.svelte +4 -4
  22. package/src/routes/components/Title.svelte +12 -4
  23. package/src/tests/lib/api/titles.test.js +23 -1
  24. package/src/tests/lib/injection.test.js +44 -3
  25. package/src/tests/lib/routes.test.js +14 -2
  26. package/src/tests/routes/components/Explore/Routes/ExploreTitle.test.js +87 -0
  27. package/src/lib/inTextWidgets.ts +0 -43
  28. package/src/routes/components/Widgets/InjectionsWidgetRail.svelte +0 -51
  29. package/src/tests/lib/inTextWidgets.test.js +0 -160
  30. 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.13.0-beta.2",
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.2",
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",
@@ -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
- 'Mentioned In This Article': {
285
- [Language.English]: 'Mentioned in this article',
286
- [Language.Swedish]: 'Nämnda i den här artikeln',
287
- [Language.Danish]: 'Nævnt i denne artikel',
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',
@@ -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, sid: '1' },
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: { ...title, sid: '2' },
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: { ...title, sid: '3' },
68
+ title_details: title,
69
69
  manual: false,
70
70
  }, {
71
71
  sid: '4',
@@ -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 { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
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 = injection.playpilot_url
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
- window.addEventListener('click', (event) => openModalForInjectedLink(event, injections))
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}
@@ -35,6 +35,7 @@
35
35
  <style lang="scss">
36
36
  .description:first-child {
37
37
  margin: margin(-1) 0 margin(1);
38
+ max-width: theme(description-max-width, 100%);
38
39
  }
39
40
 
40
41
  .paragraph {
@@ -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
- let currentRoute: ExploreRoute = $state(routes[0])
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>
@@ -123,6 +123,7 @@
123
123
  display: grid;
124
124
  grid-template-columns: repeat(auto-fill, minmax(margin(15), 1fr));
125
125
  gap: margin(0.5);
126
+ max-width: theme(playlinks-max-width, 100%);
126
127
 
127
128
  &.list {
128
129
  grid-template-columns: 1fr;
@@ -58,15 +58,15 @@
58
58
  .rail {
59
59
  --gap: var(--rail-gap, #{margin(0.5)});
60
60
  position: relative;
61
- width: calc(100% + var(--rail-margin, margin(1)) * 2);
62
- margin: 0 calc(var(--rail-margin, margin(1)) * -1);
61
+ width: calc(100% + margin(2));
62
+ margin: 0 margin(-1);
63
63
 
64
64
  :global(.slider) {
65
- padding: 0 var(--rail-margin, margin(1));
65
+ padding: 0 margin(1);
66
66
  }
67
67
 
68
68
  :global(.slider-content > :last-child) {
69
- margin-right: calc(var(--rail-margin, margin(1)) * 2);
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', () => {