@playpilot/tpi 6.1.2 → 6.2.1

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": "6.1.2",
3
+ "version": "6.2.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -19,6 +19,7 @@
19
19
  "@sveltejs/kit": "^2.0.0",
20
20
  "@sveltejs/vite-plugin-svelte": "^4.0.0",
21
21
  "@testing-library/svelte": "^5.2.6",
22
+ "@types/node": "^25.2.0",
22
23
  "@typescript-eslint/eslint-plugin": "^8.32.1",
23
24
  "@typescript-eslint/parser": "^8.32.1",
24
25
  "eslint": "^9.27.0",
@@ -1,16 +1,4 @@
1
1
  [
2
- {
3
- "slug": "100",
4
- "name": "All",
5
- "categories": {
6
- "movie": 100,
7
- "series": 100,
8
- "documentary": 100,
9
- "kids": 100,
10
- "cinema": 100,
11
- "podcasts": 100
12
- }
13
- },
14
2
  {
15
3
  "slug": "999",
16
4
  "name": "Unscripted",
@@ -33,12 +33,17 @@ export type ConfigResponse = {
33
33
  */
34
34
  disable_public_injections?: boolean
35
35
 
36
- /*
36
+ /**
37
37
  * Region to fall back on if other methods of getting the region fail. This should be equal to the region most relevant
38
38
  * to the partner. Should be lowercase.
39
39
  */
40
40
  fallback_region?: string
41
41
 
42
+ /**
43
+ * Disclaimer text that is shown above playlinks in title cards used to replace the default text
44
+ */
45
+ playlinks_disclaimer_text?: string
46
+
42
47
  /**
43
48
  * The following options are all relevant for in text disclaimers, which renders as a disclaimer text within the article,
44
49
  * rather than only inside of title cards.
@@ -25,7 +25,7 @@
25
25
 
26
26
  <div>
27
27
  {#if !aiEnabled}
28
- <strong>AI processing is disabled.</strong> Enable AI from the <a href="https://partner.playpilot.net">Partner Portal</a>
28
+ <strong>AI processing is disabled.</strong>
29
29
  {:else if aiRunning}
30
30
  <strong>AI links are currently processing.</strong> This can take several minutes. We'll insert all found injections once ready.
31
31
 
@@ -70,10 +70,6 @@
70
70
  {/if}
71
71
 
72
72
  <style lang="scss">
73
- a {
74
- color: currentColor;
75
- }
76
-
77
73
  p {
78
74
  margin: 0;
79
75
  }
@@ -0,0 +1,23 @@
1
+ <div class="empty">
2
+ <p>
3
+ <strong>No results were found</strong><br>
4
+ Sorry, we couldn't find anything matching your filter.
5
+ </p>
6
+ </div>
7
+
8
+ <style lang="scss">
9
+ p {
10
+ margin: 0;
11
+ }
12
+
13
+ strong {
14
+ font-size: theme(font-size-large);
15
+ }
16
+
17
+ .empty {
18
+ max-width: theme(explore-header-max-width, 600px);
19
+ padding: margin(2);
20
+ border-radius: theme(border-radius-large);
21
+ background: theme(lighter);
22
+ }
23
+ </style>
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { heading } from '$lib/actions/heading'
3
- import { searchTitles } from '$lib/api/search'
4
3
  import { fetchTitles } from '$lib/api/titles'
5
4
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
6
5
  import { exploreParentSelector } from '$lib/explore'
@@ -17,15 +16,16 @@
17
16
  import ListTitleSkeleton from '../ListTitleSkeleton.svelte'
18
17
  import Filter from './Filter/Filter.svelte'
19
18
  import Search from './Filter/Search.svelte'
19
+ import Empty from './Empty.svelte'
20
20
 
21
21
  const filter: ExploreFilter = $state({})
22
22
 
23
23
  let element: HTMLElement | null = null
24
24
  let titles: TitleData[] = $state([])
25
- let page = 1
26
- let searchQuery = ''
25
+ let page = $state(1)
27
26
  let debounce: ReturnType<typeof setTimeout> | null = null
28
27
  let latestRequestId = 0
28
+ let searchQuery = $state('')
29
29
  let promise = $state(getTitlesForFilter())
30
30
  let height: string | null = $state(null)
31
31
  let width = $state(0)
@@ -52,40 +52,26 @@
52
52
  latestRequestId += 1
53
53
  const requestId = latestRequestId
54
54
 
55
- let response: APIPaginatedResult<TitleData>
55
+ const params: Record<string, string | number> = { page_size: 24, page }
56
56
 
57
- // If a search query is given we use searchTitles with just the query.
58
- // If not, we use fetchTitles with the given params for the filter.
59
- // This is because the backend does not support filters for search results yet.
60
- // In the future we will likely merge these two, either by adding filters in the backend
61
- // or by filtering results in the frontend (this seems like the less good option).
62
- if (searchQuery) {
63
- const results: TitleData[] = await searchTitles(searchQuery)
64
- response = { results, next: null, previous: null }
57
+ if (searchQuery) params.search = searchQuery
65
58
 
66
- if (requestId === latestRequestId) titles = results
67
- } else {
68
- const params: Record<string, string | number> = { page_size: 24, page }
59
+ Object.entries(filter).forEach(([key, { type, value }]) => {
60
+ if (type === 'string') {
61
+ params[key] = value as string
62
+ } else if (type === 'array') {
63
+ params[key] = (value as string[]).join(',')
64
+ } else if (type === 'range') {
65
+ const [min, max] = value as number[]
69
66
 
70
- Object.entries(filter).forEach(([key, { type, value }]) => {
71
- if (type === 'string') {
72
- params[key] = value as string
73
- } else if (type === 'array') {
74
- params[key] = (value as string[]).join(',')
75
- } else if (type === 'range') {
76
- const [min, max] = value as number[]
67
+ params[key + '_min'] = min
68
+ params[key + '_max'] = max
69
+ }
70
+ })
77
71
 
78
- params[key + '_min'] = min
79
- params[key + '_max'] = max
80
- }
81
- })
72
+ const response = await fetchTitles(params)
82
73
 
83
- response = await fetchTitles(params)
84
-
85
- if (!response?.results) throw new Error('Something went wrong when fetching titles in Explore')
86
-
87
- if (requestId === latestRequestId) titles = [...titles, ...response.results]
88
- }
74
+ if (requestId === latestRequestId) titles = [...titles, ...response.results]
89
75
 
90
76
  return response
91
77
  }
@@ -142,7 +128,7 @@
142
128
 
143
129
  {#key grid}
144
130
  <Search oninput={search} />
145
- <Filter {filter} limit={!grid} onchange={setFilter} />
131
+ <Filter {filter} limit={!grid} onchange={setFilter} showSorting={!searchQuery} />
146
132
  {/key}
147
133
  </div>
148
134
 
@@ -168,8 +154,12 @@
168
154
  <Button size="large" onclick={fetchMoreTitles}>Show more</Button>
169
155
  </div>
170
156
  {/if}
157
+
158
+ {#if !titles?.length && page === 1}
159
+ <Empty />
160
+ {/if}
171
161
  {:catch}
172
- Something went wrong
162
+ <p>Something went wrong</p>
173
163
  {/await}
174
164
  </div>
175
165
 
@@ -178,8 +168,9 @@
178
168
  background: theme(explore-background, light);
179
169
  border-radius: theme(border-radius-large);
180
170
  max-width: theme(explore-max-width, 1200px);
171
+ min-height: 75vh;
181
172
  margin: 0 auto;
182
- padding: theme(explore-padding, margin(1) margin(1) margin(2));
173
+ padding: theme(explore-padding, margin(1) margin(1) margin(6));
183
174
  overflow: auto;
184
175
  font-family: theme(font-family);
185
176
  font-family: theme(detail-font-family, font-family);
@@ -8,15 +8,16 @@
8
8
  import Button from '../../Button.svelte'
9
9
  import IconArrow from '../../Icons/IconArrow.svelte'
10
10
  import IconFilter from '../../Icons/IconFilter.svelte'
11
- import { scale } from 'svelte/transition'
11
+ import { fly, scale } from 'svelte/transition'
12
12
 
13
13
  interface Props {
14
14
  filter: ExploreFilter
15
15
  onchange?: () => void
16
16
  limit?: boolean
17
+ showSorting?: boolean
17
18
  }
18
19
 
19
- const { filter, onchange = () => null, limit = true }: Props = $props()
20
+ const { filter, onchange = () => null, limit = true, showSorting = true }: Props = $props()
20
21
 
21
22
  const shownItemsLimit = 3
22
23
  const items = [{
@@ -67,9 +68,11 @@
67
68
  </Button>
68
69
  {/if}
69
70
 
70
- <div class="sorting">
71
- <FilterSorting {filter} {onchange} />
72
- </div>
71
+ {#if showSorting}
72
+ <div class="sorting" transition:fly={{ x: 5, duration: 100 }}>
73
+ <FilterSorting {filter} {onchange} />
74
+ </div>
75
+ {/if}
73
76
  </div>
74
77
  </div>
75
78
 
@@ -38,7 +38,7 @@
38
38
 
39
39
  {#if playlinks.length}
40
40
  <div class="disclaimer" data-testid="commission-disclaimer">
41
- {t('Commission Disclaimer')}
41
+ {window?.PlayPilotLinkInjections?.config?.playlinks_disclaimer_text || t('Commission Disclaimer')}
42
42
  <a href="https://playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>
43
43
  </div>
44
44
  {/if}
@@ -28,6 +28,6 @@
28
28
  display: block;
29
29
  width: 100%;
30
30
  height: auto;
31
- color: theme(detail-text-color, text-color-alt);
31
+ color: transparent;
32
32
  }
33
33
  </style>
@@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
4
  import Explore from '../../../../routes/components/Explore/Explore.svelte'
5
5
  import { fetchTitles } from '$lib/api/titles'
6
6
  import { title } from '$lib/fakeData'
7
- import { searchTitles } from '$lib/api/search'
8
7
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
9
8
  import { track } from '$lib/tracking'
10
9
 
@@ -103,21 +102,6 @@ describe('Explore.svelte', () => {
103
102
  expect(queryByTestId('skeleton')).not.toBeTruthy()
104
103
  })
105
104
 
106
- it('Should fetch using searchTitles when query is given, resetting previous titles', async () => {
107
- vi.mocked(fetchTitles).mockResolvedValueOnce({ results: [title, title], next: 'truthy', previous: null })
108
-
109
- const { getByRole, getAllByTestId } = render(Explore)
110
-
111
- vi.mocked(searchTitles).mockResolvedValueOnce([title])
112
-
113
- fireEvent.input(getByRole('searchbox'), { target: { value: 'some query' } })
114
-
115
- await waitFor(() => {
116
- expect(searchTitles).toHaveBeenCalledWith('some query')
117
- expect(getAllByTestId('title')).toHaveLength(1)
118
- })
119
- })
120
-
121
105
  it('Should call fetchTitles with string of comma separated values when filtering with array value', async () => {
122
106
  vi.mocked(fetchTitles).mockResolvedValue({ results: [title], next: 'truthy', previous: null })
123
107
 
@@ -131,6 +115,21 @@ describe('Explore.svelte', () => {
131
115
  expect(fetchTitles).toHaveBeenCalledWith({ page: 1, page_size: 24, genres: '101,109' })
132
116
  })
133
117
 
118
+ it('Should include search param when query is given', async () => {
119
+ vi.mocked(fetchTitles).mockResolvedValueOnce({ results: [title, title], next: 'truthy', previous: null })
120
+
121
+ const { getByRole, getAllByTestId } = render(Explore)
122
+
123
+ vi.mocked(fetchTitles).mockResolvedValueOnce({ results: [title], next: 'truthy', previous: null })
124
+
125
+ fireEvent.input(getByRole('searchbox'), { target: { value: 'some query' } })
126
+
127
+ await waitFor(() => {
128
+ expect(fetchTitles).toHaveBeenCalledWith({ page: 1, page_size: 24, search: 'some query' })
129
+ expect(getAllByTestId('title')).toHaveLength(1)
130
+ })
131
+ })
132
+
134
133
  it('Should call fetchTitles with min and max values when filtering with range value', async () => {
135
134
  vi.mocked(fetchTitles).mockResolvedValue({ results: [title], next: 'truthy', previous: null })
136
135
 
@@ -173,4 +172,40 @@ describe('Explore.svelte', () => {
173
172
 
174
173
  expect(fetchTitles).toHaveBeenCalledWith({ page: 1, page_size: 24, ordering: '-new' })
175
174
  })
175
+
176
+ it('Should show empty message if no titles are returned for first page', async () => {
177
+ vi.mocked(fetchTitles).mockResolvedValue({ results: [], next: null, previous: null })
178
+
179
+ const { getByText } = render(Explore)
180
+
181
+ await waitFor(() => {
182
+ expect(getByText('No results were found')).toBeTruthy()
183
+ })
184
+ })
185
+
186
+ it('Should not show empty message if no titles are returned for pages past the first page', async () => {
187
+ vi.mocked(fetchTitles).mockResolvedValue({ results: [title], next: 'truthy', previous: null })
188
+
189
+ const { getByText, queryByText } = render(Explore)
190
+
191
+ await waitFor(() => getByText('Show more'))
192
+
193
+ vi.mocked(fetchTitles).mockResolvedValue({ results: [], next: null, previous: null })
194
+
195
+ await fireEvent.click(getByText('Show more'))
196
+
197
+ await waitFor(() => {
198
+ expect(queryByText('No results were found')).not.toBeTruthy()
199
+ })
200
+ })
201
+
202
+ it('Should show error message if api responded with error', async () => {
203
+ vi.mocked(fetchTitles).mockRejectedValueOnce(null)
204
+
205
+ const { getByText } = render(Explore)
206
+
207
+ await waitFor(() => {
208
+ expect(getByText('Something went wrong')).toBeTruthy()
209
+ })
210
+ })
176
211
  })
@@ -55,4 +55,16 @@ describe('Filter.svelte', () => {
55
55
 
56
56
  expect(fetchProviders).toHaveBeenCalled()
57
57
  })
58
+
59
+ it('Should not show sortings when showSortings is false', () => {
60
+ const { container } = render(Filter, { filter: {}, showSorting: false })
61
+
62
+ expect(container.querySelector('.sorting')).not.toBeTruthy()
63
+ })
64
+
65
+ it('Should show sortings when showSortings is true', () => {
66
+ const { container } = render(Filter, { filter: {}, showSorting: true })
67
+
68
+ expect(container.querySelector('.sorting')).toBeTruthy()
69
+ })
58
70
  })
@@ -1,5 +1,5 @@
1
1
  import { fireEvent, render } from '@testing-library/svelte'
2
- import { describe, expect, it, vi } from 'vitest'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import Playlinks from '../../../../routes/components/Playlinks/Playlinks.svelte'
5
5
  import { title } from '$lib/fakeData'
@@ -17,6 +17,11 @@ vi.mock('svelte', async (importActual) => ({
17
17
  }))
18
18
 
19
19
  describe('Playlinks.svelte', () => {
20
+ beforeEach(() => {
21
+ // @ts-ignore
22
+ window.PlayPilotLinkInjections = {}
23
+ })
24
+
20
25
  it('Should render each given playlink', () => {
21
26
  const playlinks = [
22
27
  { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
@@ -35,9 +40,26 @@ describe('Playlinks.svelte', () => {
35
40
  { name: 'Some other playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
36
41
  ]
37
42
  // @ts-ignore
38
- const { getByTestId } = render(Playlinks, { playlinks, title })
43
+ const { getByTestId, getByText } = render(Playlinks, { playlinks, title })
39
44
 
40
45
  expect(getByTestId('commission-disclaimer')).toBeTruthy()
46
+ expect(getByText('We may earn a commission', { exact: false })).toBeTruthy()
47
+ })
48
+
49
+ it('Should replace default disclaimer text with config value when given', () => {
50
+ // @ts-ignore
51
+ window.PlayPilotLinkInjections = {
52
+ config: { playlinks_disclaimer_text: 'Some disclaimer' },
53
+ }
54
+
55
+ const playlinks = [
56
+ { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
57
+ ]
58
+
59
+ // @ts-ignore
60
+ const { getByText } = render(Playlinks, { playlinks, title })
61
+
62
+ expect(getByText('Some disclaimer')).toBeTruthy()
41
63
  })
42
64
 
43
65
  it('Should show empty state without commission disclaimer when no playlinks were given', () => {
package/tsconfig.json CHANGED
@@ -9,7 +9,8 @@
9
9
  "skipLibCheck": true,
10
10
  "sourceMap": true,
11
11
  "strict": true,
12
- "moduleResolution": "bundler"
12
+ "moduleResolution": "bundler",
13
+ "types": ["node"]
13
14
  }
14
15
  // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
15
16
  // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files