@playpilot/tpi 8.31.0 → 8.33.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 +8 -8
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +6 -6
- package/package.json +1 -1
- package/src/lib/api/participants.ts +5 -2
- package/src/lib/enums/Sorting.ts +1 -0
- package/src/routes/components/Icons/IconFullscreen.svelte +3 -0
- package/src/routes/components/Participants/Participant.svelte +73 -4
- package/src/routes/components/Rails/TitlesRail.svelte +1 -1
- package/src/routes/components/YouTubeEmbed.svelte +49 -14
- package/src/routes/components/YouTubeEmbedBackground.svelte +1 -1
- package/src/routes/elements/+page.svelte +7 -1
- package/src/tests/lib/api/participants.test.js +17 -0
- package/src/tests/routes/components/Participants/Participant.test.js +3 -2
- package/src/tests/routes/components/YouTubeEmbed.test.js +8 -1
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
1
2
|
import { getLanguage } from '$lib/language'
|
|
2
3
|
import { getApiToken } from '$lib/token'
|
|
3
4
|
import type { ParticipantData } from '$lib/types/participant'
|
|
@@ -6,8 +7,8 @@ import type { TitleData } from '../types/title'
|
|
|
6
7
|
import { api } from './api'
|
|
7
8
|
import { getRegionBasedOnIp } from './region'
|
|
8
9
|
|
|
9
|
-
export async function fetchTitlesForParticipant(participant: ParticipantData, { page = 1 }: { page?: number } = {}): Promise<TitleData[]> {
|
|
10
|
-
const params = {
|
|
10
|
+
export async function fetchTitlesForParticipant(participant: ParticipantData, { page = 1, sorting = null }: { page?: number, sorting?: (typeof Sorting)[keyof typeof Sorting] | null } = {}): Promise<TitleData[]> {
|
|
11
|
+
const params: Record<string, any> = {
|
|
11
12
|
language: getLanguage(),
|
|
12
13
|
region: await getRegionBasedOnIp(),
|
|
13
14
|
include_count: false,
|
|
@@ -15,6 +16,8 @@ export async function fetchTitlesForParticipant(participant: ParticipantData, {
|
|
|
15
16
|
page,
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
if (sorting) params.ordering = sorting
|
|
20
|
+
|
|
18
21
|
const response = await api<{ results: TitleData[] }>(`/titles/browse?api-token=${getApiToken()}&${paramsToString(params)}`)
|
|
19
22
|
|
|
20
23
|
return response.results
|
package/src/lib/enums/Sorting.ts
CHANGED
|
@@ -5,28 +5,40 @@
|
|
|
5
5
|
import { openModal } from '$lib/modal'
|
|
6
6
|
import type { ParticipantData } from '$lib/types/participant'
|
|
7
7
|
import type { TitleData } from '$lib/types/title'
|
|
8
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
8
9
|
import { t } from '$lib/localization'
|
|
9
10
|
import { isRetargetingAllowed } from '$lib/retargeting'
|
|
10
11
|
import { trackViaPixel } from '@playpilot/retargeting-tracking'
|
|
11
12
|
import { MetaEvent } from '$lib/enums/TrackingEvent'
|
|
12
13
|
import ParticipantImage from './ParticipantImage.svelte'
|
|
13
14
|
import ListTitle from '../ListTitle.svelte'
|
|
15
|
+
import Dropdown from '../Explore/Filter/Dropdown.svelte'
|
|
16
|
+
import Button from '../Button.svelte'
|
|
14
17
|
|
|
15
18
|
interface Props {
|
|
16
19
|
participant: ParticipantData
|
|
17
20
|
small?: boolean
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
type TitleSorting = { label: string; value: (typeof Sorting)[keyof typeof Sorting] }
|
|
24
|
+
|
|
20
25
|
const { participant, small = false }: Props = $props()
|
|
21
26
|
|
|
22
27
|
const { name, birth_date, death_date } = $derived(participant)
|
|
23
28
|
|
|
29
|
+
const sortings: TitleSorting[] = [
|
|
30
|
+
{ label: 'Popularity', value: Sorting.Popular },
|
|
31
|
+
{ label: 'Top Rated', value: Sorting.Best },
|
|
32
|
+
{ label: 'Year', value: Sorting.Chronological },
|
|
33
|
+
]
|
|
34
|
+
|
|
24
35
|
const pageSize = 30
|
|
25
36
|
|
|
26
37
|
let titles: TitleData[] = $state([])
|
|
27
38
|
let page = $state(1)
|
|
28
39
|
let hasMorePages = $state(true)
|
|
29
40
|
let loading = $state(true)
|
|
41
|
+
let currentSorting = $state(sortings[0])
|
|
30
42
|
|
|
31
43
|
onMount(loadMore)
|
|
32
44
|
|
|
@@ -41,7 +53,7 @@
|
|
|
41
53
|
loading = true
|
|
42
54
|
|
|
43
55
|
try {
|
|
44
|
-
const results = await fetchTitlesForParticipant(participant, { page })
|
|
56
|
+
const results = await fetchTitlesForParticipant(participant, { page, sorting: currentSorting.value })
|
|
45
57
|
|
|
46
58
|
titles = [...titles, ...results]
|
|
47
59
|
hasMorePages = results?.length >= pageSize
|
|
@@ -52,6 +64,14 @@
|
|
|
52
64
|
page += 1
|
|
53
65
|
}
|
|
54
66
|
}
|
|
67
|
+
|
|
68
|
+
function setSorting(sorting: TitleSorting): void {
|
|
69
|
+
currentSorting = sorting
|
|
70
|
+
titles = []
|
|
71
|
+
page = 1
|
|
72
|
+
|
|
73
|
+
loadMore()
|
|
74
|
+
}
|
|
55
75
|
</script>
|
|
56
76
|
|
|
57
77
|
<div class="header" class:small>
|
|
@@ -69,8 +89,28 @@
|
|
|
69
89
|
</div>
|
|
70
90
|
|
|
71
91
|
<div class="content">
|
|
72
|
-
{#if !small}
|
|
73
|
-
<div class="
|
|
92
|
+
{#if !small && (titles.length && !loading)}
|
|
93
|
+
<div class="list-header">
|
|
94
|
+
<div class="heading subheading" use:heading={3} id="credits">{t('Credits')}</div>
|
|
95
|
+
|
|
96
|
+
<div class="sort">
|
|
97
|
+
{t('Sort By')}:
|
|
98
|
+
|
|
99
|
+
<Dropdown>
|
|
100
|
+
{#snippet button({ toggle })}
|
|
101
|
+
<Button variant="border" onclick={toggle}>{t(currentSorting.label)}</Button>
|
|
102
|
+
{/snippet}
|
|
103
|
+
|
|
104
|
+
{#snippet content()}
|
|
105
|
+
<div class="sortings">
|
|
106
|
+
{#each sortings as sorting}
|
|
107
|
+
<Button variant="link" onclick={() => setSorting(sorting)}>{t(sorting.label)}</Button>
|
|
108
|
+
{/each}
|
|
109
|
+
</div>
|
|
110
|
+
{/snippet}
|
|
111
|
+
</Dropdown>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
74
114
|
{/if}
|
|
75
115
|
|
|
76
116
|
<div class="list">
|
|
@@ -121,7 +161,7 @@
|
|
|
121
161
|
font-style: theme(detail-title-font-style, normal);
|
|
122
162
|
|
|
123
163
|
&.subheading {
|
|
124
|
-
margin: 0
|
|
164
|
+
margin: 0;
|
|
125
165
|
font-size: theme(detail-title-small-font-size, margin(1.25));
|
|
126
166
|
}
|
|
127
167
|
|
|
@@ -155,6 +195,13 @@
|
|
|
155
195
|
gap: margin(0.5);
|
|
156
196
|
}
|
|
157
197
|
|
|
198
|
+
.list-header {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: flex-start;
|
|
201
|
+
justify-content: space-between;
|
|
202
|
+
margin-bottom: margin(0.5);
|
|
203
|
+
}
|
|
204
|
+
|
|
158
205
|
.skeleton {
|
|
159
206
|
min-height: margin(7.25);
|
|
160
207
|
border-radius: theme(playlink-border-radius, border-radius);
|
|
@@ -176,4 +223,26 @@
|
|
|
176
223
|
filter: brightness(1.2);
|
|
177
224
|
}
|
|
178
225
|
}
|
|
226
|
+
|
|
227
|
+
.sort {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
gap: margin(0.25);
|
|
231
|
+
color: theme(text-color-alt);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.sortings {
|
|
235
|
+
:global(.button) {
|
|
236
|
+
display: block;
|
|
237
|
+
width: 100%;
|
|
238
|
+
padding: margin(0.5) margin(1);
|
|
239
|
+
text-align: left;
|
|
240
|
+
white-space: nowrap;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
:global(.button:hover) {
|
|
244
|
+
background: theme(content-light);
|
|
245
|
+
border-radius: 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
179
248
|
</style>
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
<div class="video-overlay" title="" {onclick}></div>
|
|
189
189
|
|
|
190
190
|
{#if !!title.embeddable_url}
|
|
191
|
-
<YouTubeEmbed bind:this={embed} embeddable_url={title.embeddable_url!} muted loop captions
|
|
191
|
+
<YouTubeEmbed bind:this={embed} embeddable_url={title.embeddable_url!} muted loop captions showActions />
|
|
192
192
|
{:else}
|
|
193
193
|
<img class="video-fallback" src={title.medium_poster} alt="" />
|
|
194
194
|
{/if}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getVideoId } from '$lib/trailer'
|
|
3
3
|
import { onMount } from 'svelte'
|
|
4
4
|
import IconMute from './Icons/IconMute.svelte'
|
|
5
|
+
import IconFullscreen from './Icons/IconFullscreen.svelte'
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
embeddable_url: string
|
|
@@ -10,17 +11,18 @@
|
|
|
10
11
|
loop?: boolean
|
|
11
12
|
captions?: boolean
|
|
12
13
|
autoplay?: boolean
|
|
13
|
-
|
|
14
|
+
showActions?: boolean
|
|
14
15
|
fallbackStartTime?: number
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
let { embeddable_url = '', controls = [], muted = false, loop = false, captions = false, autoplay = true,
|
|
18
|
+
let { embeddable_url = '', controls = [], muted = false, loop = false, captions = false, autoplay = true, showActions = false, fallbackStartTime = 0 }: Props = $props()
|
|
18
19
|
|
|
19
20
|
const videoId = $derived(getVideoId(embeddable_url))
|
|
20
21
|
const startTime = $derived((window?.PlayPilotLinkInjections?.video_playtimes?.[videoId || ''] || (fallbackStartTime + 1)) - 1)
|
|
21
22
|
const color = window?.getComputedStyle(document.body).getPropertyValue('--playpilot-primary')?.replace('#', '') || 'fa548a'
|
|
22
23
|
|
|
23
24
|
let element: HTMLElement | null = $state(null)
|
|
25
|
+
let fullscreen: boolean = $state(false)
|
|
24
26
|
let player: any
|
|
25
27
|
|
|
26
28
|
onMount(() => {
|
|
@@ -45,11 +47,12 @@
|
|
|
45
47
|
element.style.setProperty('--plyr-color-main', '#' + color)
|
|
46
48
|
|
|
47
49
|
player = new window.Plyr(element, {
|
|
48
|
-
controls,
|
|
50
|
+
controls: controls.length === 0 ? ['play', 'mute', 'fullscreen', 'progress', 'current-time'] : controls,
|
|
49
51
|
playsinline: true,
|
|
50
52
|
autoplay,
|
|
51
53
|
muted,
|
|
52
54
|
clickToPlay: false,
|
|
55
|
+
storage: { enabled: false },
|
|
53
56
|
youtube: {
|
|
54
57
|
noCookie: true,
|
|
55
58
|
cc_load_policy: captions ? 1 : 0,
|
|
@@ -62,6 +65,16 @@
|
|
|
62
65
|
toggleMute(muted)
|
|
63
66
|
})
|
|
64
67
|
|
|
68
|
+
player.on('enterfullscreen', () => {
|
|
69
|
+
fullscreen = true
|
|
70
|
+
toggleMute(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
player.on('exitfullscreen', () => {
|
|
74
|
+
fullscreen = false
|
|
75
|
+
toggleMute(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
65
78
|
if (!loop) return
|
|
66
79
|
|
|
67
80
|
player.on('statechange', (event: { detail: { code: number } }) => {
|
|
@@ -75,10 +88,9 @@
|
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
export function toggleMute(state = !muted): void {
|
|
78
|
-
player.muted = state
|
|
79
|
-
player.volume = muted ? 0 : 1
|
|
80
|
-
|
|
81
91
|
muted = state
|
|
92
|
+
player.muted = state
|
|
93
|
+
player.volume = state ? 0 : 1
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
export function play(event: CustomEvent): void {
|
|
@@ -100,13 +112,19 @@
|
|
|
100
112
|
</svelte:head>
|
|
101
113
|
|
|
102
114
|
{#if videoId}
|
|
103
|
-
<div
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
<div class:fullscreen class:hide-controls={controls.length === 0 && !fullscreen}>
|
|
116
|
+
<div bind:this={element} class="player" data-plyr-provider="youtube" data-plyr-embed-id data-testid={embeddable_url}></div>
|
|
117
|
+
|
|
118
|
+
{#if showActions}
|
|
119
|
+
<button class="action" onclick={() => toggleMute()} aria-label="Mute">
|
|
120
|
+
<IconMute {muted} />
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
<button class="action secondary" onclick={() => player.fullscreen.enter()} aria-label="Fullscreen">
|
|
124
|
+
<IconFullscreen />
|
|
125
|
+
</button>
|
|
126
|
+
{/if}
|
|
127
|
+
</div>
|
|
110
128
|
{:else}
|
|
111
129
|
Something went wrong
|
|
112
130
|
{/if}
|
|
@@ -123,7 +141,20 @@
|
|
|
123
141
|
height: 100%;
|
|
124
142
|
}
|
|
125
143
|
|
|
126
|
-
.
|
|
144
|
+
.fullscreen :global(iframe) {
|
|
145
|
+
height: 100%;
|
|
146
|
+
width: 100%;
|
|
147
|
+
top: 0;
|
|
148
|
+
left: 0;
|
|
149
|
+
transform: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.hide-controls :global(.plyr__controls) {
|
|
153
|
+
display: none;
|
|
154
|
+
opacity: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.action {
|
|
127
158
|
z-index: 10;
|
|
128
159
|
display: flex;
|
|
129
160
|
align-items: center;
|
|
@@ -148,5 +179,9 @@
|
|
|
148
179
|
&:active {
|
|
149
180
|
transform: scale(0.95);
|
|
150
181
|
}
|
|
182
|
+
|
|
183
|
+
&.secondary {
|
|
184
|
+
top: calc(margin(3) + var(--mute-top, 0px));
|
|
185
|
+
}
|
|
151
186
|
}
|
|
152
187
|
</style>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<div class="video-background" style="--height: {height}px; --width: {clientWidth}px;" bind:clientWidth bind:clientHeight data-testid="video-background">
|
|
18
|
-
<YouTubeEmbed {embeddable_url} {autoplay} fallbackStartTime={5} muted loop
|
|
18
|
+
<YouTubeEmbed {embeddable_url} {autoplay} fallbackStartTime={5} muted loop showActions />
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
21
|
<style lang="scss">
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import ParticipantsRail from '../components/Rails/ParticipantsRail.svelte'
|
|
24
24
|
import InfiniteTitlesRail from '../components/Rails/InfiniteTitlesRail.svelte'
|
|
25
25
|
import YouTubeEmbed from '../components/YouTubeEmbed.svelte'
|
|
26
|
+
import Participant from '../components/Participants/Participant.svelte'
|
|
26
27
|
|
|
27
28
|
if (browser) {
|
|
28
29
|
// @ts-ignore
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
<h2>Youtube Embed</h2>
|
|
48
49
|
|
|
49
50
|
<div class="video">
|
|
50
|
-
<YouTubeEmbed embeddable_url="https://www.youtube.com/watch?v=bBMajXwi6Cs" controls={['progress', 'play']} loop
|
|
51
|
+
<YouTubeEmbed embeddable_url="https://www.youtube.com/watch?v=bBMajXwi6Cs" controls={['progress', 'play']} loop showActions muted />
|
|
51
52
|
</div>
|
|
52
53
|
|
|
53
54
|
<h2>Title</h2>
|
|
@@ -137,6 +138,11 @@
|
|
|
137
138
|
<h2>Participants</h2>
|
|
138
139
|
|
|
139
140
|
<div class="group">
|
|
141
|
+
<div>
|
|
142
|
+
<h3>Participant.svelte</h3>
|
|
143
|
+
<div class="item"><Participant participant={participants[0]} /></div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
140
146
|
<div>
|
|
141
147
|
<h3>Participants.svelte</h3>
|
|
142
148
|
<div class="item"><ParticipantsRail {title} /></div>
|
|
@@ -5,6 +5,7 @@ import { fetchTitlesForParticipant } from '$lib/api/participants'
|
|
|
5
5
|
import { participants, title } from '$lib/fakeData'
|
|
6
6
|
import { getApiToken } from '$lib/token'
|
|
7
7
|
import { fakeFetch } from '../../helpers'
|
|
8
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
8
9
|
|
|
9
10
|
vi.mock('$lib/token', () => ({
|
|
10
11
|
getApiToken: vi.fn(),
|
|
@@ -57,5 +58,21 @@ describe('$lib/api/participants', () => {
|
|
|
57
58
|
|
|
58
59
|
expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&language=en-US®ion=nl&include_count=false&participant_sid=${participants[0].sid}&page=1`)
|
|
59
60
|
})
|
|
61
|
+
|
|
62
|
+
it('Should use given sorting', async () => {
|
|
63
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
64
|
+
|
|
65
|
+
await fetchTitlesForParticipant(participants[0], { page: 5, sorting: Sorting.New })
|
|
66
|
+
|
|
67
|
+
expect(api).toHaveBeenCalledWith(expect.stringContaining(`ordering=${Sorting.New}`))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('Should not add ordering when not sorting is given', async () => {
|
|
71
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
72
|
+
|
|
73
|
+
await fetchTitlesForParticipant(participants[0], { page: 5 })
|
|
74
|
+
|
|
75
|
+
expect(api).toHaveBeenCalledWith(expect.not.stringContaining('ordering'))
|
|
76
|
+
})
|
|
60
77
|
})
|
|
61
78
|
})
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
|
4
4
|
import Participant from '../../../../routes/components/Participants/Participant.svelte'
|
|
5
5
|
import { participants, title } from '$lib/fakeData'
|
|
6
6
|
import { fetchTitlesForParticipant } from '$lib/api/participants'
|
|
7
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
7
8
|
|
|
8
9
|
vi.mock('$lib/tracking', () => ({
|
|
9
10
|
track: vi.fn(),
|
|
@@ -21,7 +22,7 @@ describe('Participant.svelte', () => {
|
|
|
21
22
|
it('Should call fetchTitlesForParticipant on mount', () => {
|
|
22
23
|
render(Participant, { participant: participants[0] })
|
|
23
24
|
|
|
24
|
-
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 1 })
|
|
25
|
+
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 1, sorting: Sorting.Popular })
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
it('Should render fetched titles after showing skeletons', async () => {
|
|
@@ -71,7 +72,7 @@ describe('Participant.svelte', () => {
|
|
|
71
72
|
|
|
72
73
|
await fireEvent.click(getByText('Show more'))
|
|
73
74
|
|
|
74
|
-
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 2 })
|
|
75
|
+
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 2, sorting: Sorting.Popular })
|
|
75
76
|
})
|
|
76
77
|
|
|
77
78
|
it('Should render as small variant when given', () => {
|
|
@@ -54,7 +54,7 @@ describe('YouTubeEmbed.svelte', () => {
|
|
|
54
54
|
expect(Plyr).toHaveBeenCalledWith(
|
|
55
55
|
playerElement,
|
|
56
56
|
expect.objectContaining({
|
|
57
|
-
controls: [],
|
|
57
|
+
controls: ['play', 'mute', 'fullscreen', 'progress', 'current-time'],
|
|
58
58
|
autoplay: true,
|
|
59
59
|
muted: false,
|
|
60
60
|
clickToPlay: false,
|
|
@@ -145,4 +145,11 @@ describe('YouTubeEmbed.svelte', () => {
|
|
|
145
145
|
expect(container.querySelector('.player')).not.toBeTruthy()
|
|
146
146
|
expect(getByText('Something went wrong')).toBeTruthy()
|
|
147
147
|
})
|
|
148
|
+
|
|
149
|
+
it('Should render actions when showActions is true', () => {
|
|
150
|
+
const { getByLabelText } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', showActions: true })
|
|
151
|
+
|
|
152
|
+
expect(getByLabelText('Mute')).toBeTruthy()
|
|
153
|
+
expect(getByLabelText('Fullscreen')).toBeTruthy()
|
|
154
|
+
})
|
|
148
155
|
})
|