@playpilot/tpi 8.1.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.1.3",
3
+ "version": "8.2.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -36,7 +36,7 @@
36
36
  "svelte": "5.44.1",
37
37
  "svelte-check": "^4.0.0",
38
38
  "svelte-preprocess": "^6.0.3",
39
- "svelte-tiny-slider": "^2.7.0",
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",
@@ -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, string | number> = {}): Promise<APIPaginatedResult<TitleData>> {
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',
@@ -0,0 +1,5 @@
1
+ export const Sorting = Object.freeze({
2
+ Popular: '-popularity',
3
+ New: '-new',
4
+ Best: '-best',
5
+ })
@@ -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: '-popularity' },
19
- { label: 'New', value: '-new' },
20
- { label: 'Top Rated', value: '-best' },
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 { fetchSimilarTitles } from '$lib/api/titles'
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
- {#await fetchSimilarTitles(title)}
14
- Loading...
15
- {:then titles}
16
- {#if titles}
17
- <TitlesRail heading="Some titles" {titles} expandable bind:expandedTitle bind:expandedRailKey />
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
- {#if titles[1]}
20
- <TitlesRail heading="Some other titles" titles={fetchSimilarTitles(titles[1])} expandable bind:expandedTitle bind:expandedRailKey />
21
- {/if}
33
+ async function getListTitles(params: Record<string, any> = {}): Promise<TitleData[]> {
34
+ return (await fetchTitles(params))?.results
35
+ }
36
+ </script>
22
37
 
23
- {#if titles[5]}
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
- <!-- TODO: Remove me. This is just a placeholder element to make scrolling easier -->
30
- <div style="height: 1000px"></div>
40
+ {#each rails as { heading, params, properties }}
41
+ <TitlesRail {heading} titles={getListTitles(params)} {...properties} bind:expandedTitle bind:expandedRailKey />
42
+ {/each}
@@ -56,7 +56,7 @@
56
56
  }
57
57
 
58
58
  .rail {
59
- --gap: #{margin(0.5)};
59
+ --gap: var(--rail-gap, #{margin(0.5)});
60
60
  position: relative;
61
61
  width: calc(100% + margin(2));
62
62
  margin: 0 margin(-1);
@@ -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
- $width: margin(6);
160
- $image-height: #{calc($width * 3 / 2)};
161
- $expanded-width: #{calc($image-height / 9 * 16)};
162
- $border-radius: #{theme(rail-border-radius, border-radius)};
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
- width: $width;
175
- border-radius: $border-radius;
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: $expanded-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: $image-height;
195
- width: $expanded-width;
232
+ height: var(--image-height);
233
+ width: var(--expanded-width);
196
234
  aspect-ratio: 16/9;
197
- border-radius: $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: $image-height;
226
- width: $expanded-width;
263
+ height: var(--image-height);
264
+ width: var(--expanded-width);
227
265
  }
228
266
 
229
267
  .poster {
230
268
  display: block;
231
- height: $image-height;
232
- width: $width;
269
+ height: var(--image-height);
270
+ width: var(--width);
233
271
  aspect-ratio: 2 / 3;
234
- border-radius: $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: $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 {
@@ -129,6 +129,8 @@
129
129
 
130
130
  {#key renderKey}
131
131
  <TitlesRail titles={fetchSimilarTitles(title)} expandable />
132
+ <br>
133
+ <TitlesRail titles={fetchSimilarTitles(title)} aside size="large" />
132
134
  {/key}
133
135
 
134
136
  <h2>TitlesRailModal</h2>
@@ -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&region=nl&include_count=false')
45
+ expect(api).toHaveBeenCalledWith('/titles/browse?api-token=some-token&language=en-US&include_count=false&region=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&region=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
- fetchSimilarTitles: vi.fn(),
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 do nothing yet, this is a placeholder', () => {
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', () => {