@playpilot/tpi 8.1.4 → 8.2.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/package.json +1 -1
- package/src/lib/api/titles.ts +3 -2
- package/src/lib/data/translations.ts +27 -0
- package/src/lib/enums/Sorting.ts +5 -0
- package/src/routes/components/Explore/Filter/FilterSorting.svelte +4 -3
- package/src/routes/components/Explore/Routes/ExploreHome.svelte +32 -20
- package/src/routes/components/Rails/Rail.svelte +1 -1
- package/src/routes/components/Rails/TitlesRail.svelte +74 -19
- package/src/routes/elements/+page.svelte +2 -0
- package/src/tests/lib/api/titles.test.js +13 -1
- package/src/tests/routes/components/Explore/ExploreRouter.test.js +3 -0
- package/src/tests/routes/components/Explore/Routes/ExploreHome.test.js +8 -3
- package/src/tests/routes/components/Rails/TitlesRail.test.js +27 -0
- package/src/tests/routes/components/Title.test.js +2 -2
package/package.json
CHANGED
package/src/lib/api/titles.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { TitleData } from '../types/title'
|
|
|
6
6
|
import { api } from './api'
|
|
7
7
|
import { getRegionBasedOnIp } from './region'
|
|
8
8
|
|
|
9
|
-
export async function fetchTitles(params: Record<string,
|
|
9
|
+
export async function fetchTitles(params: Record<string, any> = {}): Promise<APIPaginatedResult<TitleData>> {
|
|
10
10
|
const apiToken = getApiToken()
|
|
11
11
|
|
|
12
12
|
if (!apiToken) throw new Error('No token was provided')
|
|
@@ -14,10 +14,11 @@ export async function fetchTitles(params: Record<string, string | number> = {}):
|
|
|
14
14
|
params = {
|
|
15
15
|
...params,
|
|
16
16
|
language: getLanguage(),
|
|
17
|
-
region: await getRegionBasedOnIp(),
|
|
18
17
|
include_count: 'false',
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
if (params.region !== null) params.region = params.region || await getRegionBasedOnIp()
|
|
21
|
+
|
|
21
22
|
const response = await api<APIPaginatedResult<TitleData>>(`/titles/browse?api-token=${apiToken}&` + paramsToString(params))
|
|
22
23
|
|
|
23
24
|
return response
|
|
@@ -252,6 +252,33 @@ export const translations = {
|
|
|
252
252
|
[Language.Danish]: 'Medvirkende',
|
|
253
253
|
},
|
|
254
254
|
|
|
255
|
+
// List titles
|
|
256
|
+
'List: Trending': {
|
|
257
|
+
[Language.English]: 'Trending Movies & Shows',
|
|
258
|
+
[Language.Swedish]: 'Trendande Titlar',
|
|
259
|
+
[Language.Danish]: 'Populære film og serier',
|
|
260
|
+
},
|
|
261
|
+
'List: Upcoming': {
|
|
262
|
+
[Language.English]: 'Upcoming Releases',
|
|
263
|
+
[Language.Swedish]: 'Kommande Premiärer',
|
|
264
|
+
[Language.Danish]: 'Mest ventede film',
|
|
265
|
+
},
|
|
266
|
+
'List: New': {
|
|
267
|
+
[Language.English]: 'New Releases',
|
|
268
|
+
[Language.Swedish]: 'Nya Filmer & Serier',
|
|
269
|
+
[Language.Danish]: 'Nye film og serier',
|
|
270
|
+
},
|
|
271
|
+
'List: Demand': {
|
|
272
|
+
[Language.English]: 'Always in Demand',
|
|
273
|
+
[Language.Swedish]: 'Tidlösa Favoriter',
|
|
274
|
+
[Language.Danish]: 'Tidløse favoritter',
|
|
275
|
+
},
|
|
276
|
+
'List: Cinema': {
|
|
277
|
+
[Language.English]: 'Coming to Cinemas',
|
|
278
|
+
[Language.Swedish]: 'Snart på Bio',
|
|
279
|
+
[Language.Danish]: 'Kommende film',
|
|
280
|
+
},
|
|
281
|
+
|
|
255
282
|
// Genres
|
|
256
283
|
'All': {
|
|
257
284
|
[Language.English]: 'All',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { ExploreFilter } from '$lib/types/filter'
|
|
5
5
|
import { t } from '$lib/localization'
|
|
6
6
|
import Dropdown from './Dropdown.svelte'
|
|
7
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
filter: ExploreFilter
|
|
@@ -15,9 +16,9 @@
|
|
|
15
16
|
const param = 'ordering'
|
|
16
17
|
|
|
17
18
|
const options = [
|
|
18
|
-
{ label: 'Popularity', value:
|
|
19
|
-
{ label: 'New', value:
|
|
20
|
-
{ label: 'Top Rated', value:
|
|
19
|
+
{ label: 'Popularity', value: Sorting.Popular },
|
|
20
|
+
{ label: 'New', value: Sorting.New },
|
|
21
|
+
{ label: 'Top Rated', value: Sorting.Best },
|
|
21
22
|
]
|
|
22
23
|
|
|
23
24
|
const label = $derived(options.find(({ value }) => value === filter[param]?.value)?.label || 'Sort By')
|
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { TitleData } from '$lib/types/title'
|
|
3
|
-
import {
|
|
4
|
-
import { title } from '$lib/fakeData'
|
|
3
|
+
import { fetchTitles } from '$lib/api/titles'
|
|
5
4
|
import TitlesRail from '../../Rails/TitlesRail.svelte'
|
|
5
|
+
import { t } from '$lib/localization'
|
|
6
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
6
7
|
|
|
7
8
|
let expandedTitle: TitleData | null = $state(null)
|
|
8
9
|
let expandedRailKey: string | null = $state(null)
|
|
9
|
-
</script>
|
|
10
|
-
|
|
11
|
-
<div data-testid="explore-home"></div>
|
|
12
10
|
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
{:
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const rails: { heading: string, params: Record<string, any>, properties: Record<string, any> }[] = [{
|
|
12
|
+
heading: t('List: Trending'),
|
|
13
|
+
params: { ordering: Sorting.Popular },
|
|
14
|
+
properties: { expandable: true },
|
|
15
|
+
}, {
|
|
16
|
+
heading: t('List: Upcoming'),
|
|
17
|
+
params: { region: null },
|
|
18
|
+
properties: { expandable: true },
|
|
19
|
+
}, {
|
|
20
|
+
heading: t('List: New'),
|
|
21
|
+
params: { ordering: Sorting.New },
|
|
22
|
+
properties: { aside: true, size: 'large' },
|
|
23
|
+
}, {
|
|
24
|
+
heading: t('List: Demand'),
|
|
25
|
+
params: { ordering: Sorting.Best },
|
|
26
|
+
properties: {},
|
|
27
|
+
}, {
|
|
28
|
+
heading: t('List: Cinema'),
|
|
29
|
+
params: { region: null, include_cinema: true },
|
|
30
|
+
properties: {},
|
|
31
|
+
}]
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
async function getListTitles(params: Record<string, any> = {}): Promise<TitleData[]> {
|
|
34
|
+
return (await fetchTitles(params))?.results
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
<TitlesRail heading="Some more titles" titles={fetchSimilarTitles(titles[5])} expandable bind:expandedTitle bind:expandedRailKey />
|
|
25
|
-
{/if}
|
|
26
|
-
{/if}
|
|
27
|
-
{/await}
|
|
38
|
+
<div data-testid="explore-home"></div>
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
<
|
|
40
|
+
{#each rails as { heading, params, properties }}
|
|
41
|
+
<TitlesRail {heading} titles={getListTitles(params)} {...properties} bind:expandedTitle bind:expandedRailKey />
|
|
42
|
+
{/each}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
interface Props {
|
|
16
16
|
titles: Promise<TitleData[]> | TitleData[]
|
|
17
17
|
heading?: string,
|
|
18
|
+
size?: 'small' | 'large'
|
|
19
|
+
aside?: boolean,
|
|
18
20
|
expandable?: boolean,
|
|
19
21
|
expandedTitle?: TitleData | null,
|
|
20
22
|
expandedRailKey?: string | null,
|
|
@@ -24,6 +26,8 @@
|
|
|
24
26
|
let {
|
|
25
27
|
titles,
|
|
26
28
|
heading = '',
|
|
29
|
+
size = 'small',
|
|
30
|
+
aside = false,
|
|
27
31
|
expandable = false,
|
|
28
32
|
expandedTitle = $bindable(null),
|
|
29
33
|
expandedRailKey = $bindable(null),
|
|
@@ -100,7 +104,7 @@
|
|
|
100
104
|
|
|
101
105
|
<svelte:window onscroll={expandWhenFirstInView} />
|
|
102
106
|
|
|
103
|
-
<div class="titles" bind:this={element} data-role="{expandable ? 'expandable-rail' : null}">
|
|
107
|
+
<div class="titles {size}" class:with-aside={aside} bind:this={element} data-role="{expandable ? 'expandable-rail' : null}">
|
|
104
108
|
{#await titles}
|
|
105
109
|
<Rail {heading}>
|
|
106
110
|
{#each { length: 12 }}
|
|
@@ -148,6 +152,14 @@
|
|
|
148
152
|
<a class="heading" {href} {onclick} data-testid="heading">
|
|
149
153
|
{title.title}
|
|
150
154
|
</a>
|
|
155
|
+
|
|
156
|
+
{#if aside}
|
|
157
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
158
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
159
|
+
<div class="aside" {onclick} data-testid="aside">
|
|
160
|
+
{title.description}
|
|
161
|
+
</div>
|
|
162
|
+
{/if}
|
|
151
163
|
</div>
|
|
152
164
|
{/each}
|
|
153
165
|
</Rail>
|
|
@@ -156,29 +168,55 @@
|
|
|
156
168
|
</div>
|
|
157
169
|
|
|
158
170
|
<style lang="scss">
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
.titles {
|
|
172
|
+
--width: #{margin(6)};
|
|
173
|
+
--image-height: #{calc(var(--width) * 3 / 2)};
|
|
174
|
+
--expanded-width: #{calc(var(--image-height) / 9 * 16)};
|
|
175
|
+
--border-radius: #{theme(rail-border-radius, border-radius)};
|
|
176
|
+
|
|
177
|
+
&.with-aside {
|
|
178
|
+
--rail-gap: #{margin(1)};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&.large {
|
|
182
|
+
--width: #{margin(7.5)};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
163
185
|
|
|
164
186
|
.title {
|
|
165
187
|
width: unset;
|
|
188
|
+
font-size: theme(rail-font-size, font-size-small);
|
|
189
|
+
line-height: 1.2;
|
|
166
190
|
|
|
167
191
|
&:hover,
|
|
168
192
|
&:active {
|
|
169
193
|
filter: brightness(1.1);
|
|
170
194
|
}
|
|
195
|
+
|
|
196
|
+
.with-aside & {
|
|
197
|
+
display: grid;
|
|
198
|
+
grid-template:
|
|
199
|
+
"media heading"
|
|
200
|
+
"media aside";
|
|
201
|
+
grid-template-rows: 1lh auto;
|
|
202
|
+
align-items: flex-start;
|
|
203
|
+
gap: margin(0.25) margin(0.5);
|
|
204
|
+
width: calc(var(--width) * 2);
|
|
205
|
+
height: var(--image-height);
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
}
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
.media {
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
grid-area: media;
|
|
212
|
+
width: var(--width);
|
|
213
|
+
border-radius: var(--border-radius);
|
|
176
214
|
transition: width 200ms;
|
|
177
215
|
transition-delay: 1ms;
|
|
178
216
|
overflow: hidden;
|
|
179
217
|
|
|
180
218
|
.expanded & {
|
|
181
|
-
width:
|
|
219
|
+
width: var(--expanded-width);
|
|
182
220
|
transition-delay: 0ms;
|
|
183
221
|
}
|
|
184
222
|
}
|
|
@@ -191,10 +229,10 @@
|
|
|
191
229
|
|
|
192
230
|
.video {
|
|
193
231
|
position: relative;
|
|
194
|
-
height:
|
|
195
|
-
width:
|
|
232
|
+
height: var(--image-height);
|
|
233
|
+
width: var(--expanded-width);
|
|
196
234
|
aspect-ratio: 16/9;
|
|
197
|
-
border-radius:
|
|
235
|
+
border-radius: var(--border-radius);
|
|
198
236
|
overflow: hidden;
|
|
199
237
|
background: black;
|
|
200
238
|
z-index: 1;
|
|
@@ -222,35 +260,52 @@
|
|
|
222
260
|
.video-fallback {
|
|
223
261
|
position: absolute;
|
|
224
262
|
object-fit: cover;
|
|
225
|
-
height:
|
|
226
|
-
width:
|
|
263
|
+
height: var(--image-height);
|
|
264
|
+
width: var(--expanded-width);
|
|
227
265
|
}
|
|
228
266
|
|
|
229
267
|
.poster {
|
|
230
268
|
display: block;
|
|
231
|
-
height:
|
|
232
|
-
width:
|
|
269
|
+
height: var(--image-height);
|
|
270
|
+
width: var(--width);
|
|
233
271
|
aspect-ratio: 2 / 3;
|
|
234
|
-
border-radius:
|
|
272
|
+
border-radius: var(--border-radius);
|
|
235
273
|
background: theme(detail-background-light, lighter);
|
|
236
274
|
text-decoration: none;
|
|
237
275
|
overflow: hidden;
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
.heading {
|
|
279
|
+
grid-area: heading;
|
|
241
280
|
display: -webkit-box;
|
|
242
281
|
padding-top: margin(0.5);
|
|
243
282
|
overflow: hidden;
|
|
244
|
-
max-width:
|
|
283
|
+
max-width: var(--width);
|
|
245
284
|
text-decoration: none;
|
|
246
285
|
color: theme(rail-text-color, text-color-alt) !important;
|
|
247
|
-
font-size: theme(rail-font-size, font-size-small);
|
|
248
286
|
font-style: normal !important;
|
|
249
|
-
line-height: 1.2;
|
|
250
287
|
line-clamp: 2;
|
|
251
288
|
-webkit-line-clamp: 2;
|
|
252
289
|
-webkit-box-orient: vertical;
|
|
253
290
|
text-overflow: ellipsis;
|
|
291
|
+
|
|
292
|
+
.with-aside & {
|
|
293
|
+
padding-top: 0;
|
|
294
|
+
font-weight: theme(rail-aside-heading-font-weight, font-bold);
|
|
295
|
+
line-clamp: 1;
|
|
296
|
+
-webkit-line-clamp: 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.aside {
|
|
301
|
+
grid-area: aside;
|
|
302
|
+
display: -webkit-box;
|
|
303
|
+
height: 100%;
|
|
304
|
+
mask-image: linear-gradient(to top, transparent 0.5lh, white 1.5lh);
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
color: theme(rail-text-color, text-color-alt) !important;
|
|
307
|
+
font-size: theme(rail-aside-font-size, 11px);
|
|
308
|
+
line-height: 1.2;
|
|
254
309
|
}
|
|
255
310
|
|
|
256
311
|
.skeleton {
|
|
@@ -42,7 +42,19 @@ describe('$lib/api/titles', () => {
|
|
|
42
42
|
|
|
43
43
|
await fetchTitles()
|
|
44
44
|
|
|
45
|
-
expect(api).toHaveBeenCalledWith('/titles/browse?api-token=some-token&language=en-US®ion=nl
|
|
45
|
+
expect(api).toHaveBeenCalledWith('/titles/browse?api-token=some-token&language=en-US&include_count=false®ion=nl')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('Should not set region if region is given as null in params', async () => {
|
|
49
|
+
await fetchTitles({ region: null })
|
|
50
|
+
|
|
51
|
+
expect(api).toHaveBeenCalledWith('/titles/browse?api-token=some-token&language=en-US&include_count=false')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('Should use region given as param', async () => {
|
|
55
|
+
await fetchTitles({ region: 'be' })
|
|
56
|
+
|
|
57
|
+
expect(api).toHaveBeenCalledWith('/titles/browse?api-token=some-token®ion=be&language=en-US&include_count=false')
|
|
46
58
|
})
|
|
47
59
|
})
|
|
48
60
|
|
|
@@ -2,6 +2,7 @@ import { fireEvent, render } from '@testing-library/svelte'
|
|
|
2
2
|
import { expect, describe, it, beforeEach, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import ExploreRouter from '../../../../routes/components/Explore/ExploreRouter.svelte'
|
|
5
|
+
import { fetchTitles } from '$lib/api/titles'
|
|
5
6
|
|
|
6
7
|
vi.mock('$lib/api/titles', () => ({
|
|
7
8
|
fetchTitles: vi.fn(),
|
|
@@ -10,6 +11,8 @@ vi.mock('$lib/api/titles', () => ({
|
|
|
10
11
|
|
|
11
12
|
describe('ExploreRouter.svelte', () => {
|
|
12
13
|
beforeEach(() => {
|
|
14
|
+
vi.mocked(fetchTitles).mockResolvedValue({ next: null, previous: null, results: [] })
|
|
15
|
+
|
|
13
16
|
// @ts-ignore
|
|
14
17
|
window.PlayPilotLinkInjections = { config: { explore_use_router: true } }
|
|
15
18
|
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { render } from '@testing-library/svelte'
|
|
2
|
-
import { beforeEach, describe, it, vi } from 'vitest'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import ExploreHome from '../../../../../routes/components/Explore/Routes/ExploreHome.svelte'
|
|
5
|
+
import { fetchTitles } from '$lib/api/titles'
|
|
5
6
|
|
|
6
7
|
vi.mock('$lib/api/titles', () => ({
|
|
7
|
-
|
|
8
|
+
fetchTitles: vi.fn(),
|
|
8
9
|
}))
|
|
9
10
|
|
|
10
11
|
describe('ExploreResults.svelte', () => {
|
|
@@ -12,7 +13,11 @@ describe('ExploreResults.svelte', () => {
|
|
|
12
13
|
vi.resetAllMocks()
|
|
13
14
|
})
|
|
14
15
|
|
|
15
|
-
it('Should
|
|
16
|
+
it('Should fetch titles for each rail', () => {
|
|
17
|
+
vi.mocked(fetchTitles).mockResolvedValue({ next: null, previous: null, results: [] })
|
|
18
|
+
|
|
16
19
|
render(ExploreHome)
|
|
20
|
+
|
|
21
|
+
expect(fetchTitles).toHaveBeenCalledTimes(5)
|
|
17
22
|
})
|
|
18
23
|
})
|
|
@@ -167,4 +167,31 @@ describe('TitlesRail.svelte', () => {
|
|
|
167
167
|
|
|
168
168
|
expect(openModal).not.toHaveBeenCalled()
|
|
169
169
|
})
|
|
170
|
+
|
|
171
|
+
it('Should not contain with-aside class or show aside by default', async () => {
|
|
172
|
+
const { container, queryByTestId } = render(TitlesRail, { titles: [title] })
|
|
173
|
+
|
|
174
|
+
expect(/** @type {HTMLElement} */ (container.querySelector('.with-aside'))).not.toBeTruthy()
|
|
175
|
+
expect(queryByTestId('aside')).not.toBeTruthy()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('Should contain with-aside class and show aside when aside is given as true', async () => {
|
|
179
|
+
const { container, getByTestId } = render(TitlesRail, { titles: [title], aside: true })
|
|
180
|
+
|
|
181
|
+
expect(/** @type {HTMLElement} */ (container.querySelector('.with-aside'))).toBeTruthy()
|
|
182
|
+
expect(getByTestId('aside')).toBeTruthy()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('Should contain small class by default', async () => {
|
|
186
|
+
const { container } = render(TitlesRail, { titles: [title] })
|
|
187
|
+
|
|
188
|
+
expect(/** @type {HTMLElement} */ (container.querySelector('.titles.small'))).toBeTruthy()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('Should contain large class when size is given as large', async () => {
|
|
192
|
+
const { container } = render(TitlesRail, { titles: [title], size: 'large' })
|
|
193
|
+
|
|
194
|
+
expect(/** @type {HTMLElement} */ (container.querySelector('.titles.small'))).not.toBeTruthy()
|
|
195
|
+
expect(/** @type {HTMLElement} */ (container.querySelector('.titles.large'))).toBeTruthy()
|
|
196
|
+
})
|
|
170
197
|
})
|
|
@@ -60,13 +60,13 @@ describe('Title.svelte', () => {
|
|
|
60
60
|
it('Should not have small class by default', () => {
|
|
61
61
|
const { container } = render(Title, { title })
|
|
62
62
|
|
|
63
|
-
expect(container.querySelector('.small')).not.toBeTruthy()
|
|
63
|
+
expect(container.querySelector('.content.small')).not.toBeTruthy()
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
it('Should have small class when prop is given', () => {
|
|
67
67
|
const { container } = render(Title, { title, small: true })
|
|
68
68
|
|
|
69
|
-
expect(container.querySelector('.small')).toBeTruthy()
|
|
69
|
+
expect(container.querySelector('.content.small')).toBeTruthy()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
72
|
it('Should truncate title when small prop is given', () => {
|