@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/.github/workflows/tests.yml +20 -8
- package/dist/link-injections.js +8 -8
- package/package.json +2 -1
- package/src/lib/data/genres.json +0 -12
- package/src/lib/types/config.d.ts +6 -1
- package/src/routes/components/Editorial/AIIndicator.svelte +1 -5
- package/src/routes/components/Explore/Empty.svelte +23 -0
- package/src/routes/components/Explore/Explore.svelte +26 -35
- package/src/routes/components/Explore/Filter/Filter.svelte +8 -5
- package/src/routes/components/Playlinks/Playlinks.svelte +1 -1
- package/src/routes/components/TitlePoster.svelte +1 -1
- package/src/tests/routes/components/Explore/Explore.test.js +51 -16
- package/src/tests/routes/components/Explore/Filter/Filter.test.js +12 -0
- package/src/tests/routes/components/Playlinks/Playlinks.test.js +24 -2
- package/tsconfig.json +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playpilot/tpi",
|
|
3
|
-
"version": "6.1
|
|
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",
|
package/src/lib/data/genres.json
CHANGED
|
@@ -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>
|
|
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
|
-
|
|
55
|
+
const params: Record<string, string | number> = { page_size: 24, page }
|
|
56
56
|
|
|
57
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
params[key + '_max'] = max
|
|
80
|
-
}
|
|
81
|
-
})
|
|
72
|
+
const response = await fetchTitles(params)
|
|
82
73
|
|
|
83
|
-
|
|
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(
|
|
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
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
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}
|
|
@@ -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
|