@playpilot/tpi 8.30.0 → 8.32.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 +7 -7
- package/package.json +1 -1
- package/src/lib/api/externalPages.ts +1 -0
- package/src/lib/api/participants.ts +6 -3
- package/src/lib/api/providers.ts +1 -1
- package/src/lib/api/search.ts +9 -0
- package/src/lib/api/titles.ts +1 -1
- package/src/lib/enums/Sorting.ts +1 -0
- package/src/routes/components/Editorial/Editor.svelte +0 -2
- package/src/routes/components/Editorial/EditorItem.svelte +2 -2
- package/src/routes/components/Editorial/ManualInjection.svelte +26 -19
- package/src/routes/components/Editorial/Search/ParticipantSearchItem.svelte +86 -0
- package/src/routes/components/Editorial/Search/TypeSearch.svelte +120 -0
- package/src/routes/components/Participants/Participant.svelte +73 -4
- package/src/routes/components/Tabs.svelte +12 -23
- package/src/routes/elements/+page.svelte +6 -0
- package/src/tests/lib/api/externalPages.test.js +134 -1
- package/src/tests/lib/api/participants.test.js +17 -0
- package/src/tests/routes/components/Editorial/EditorItem.test.js +1 -2
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +43 -6
- package/src/tests/routes/components/Editorial/Search/{TitleSearch.test.js → TypeSearch.test.js} +21 -9
- package/src/tests/routes/components/Participants/Participant.test.js +3 -2
- package/src/routes/components/Editorial/Search/TitleSearch.svelte +0 -96
package/package.json
CHANGED
|
@@ -151,6 +151,7 @@ export async function saveLinkInjections(linkInjections: LinkInjection[]): Promi
|
|
|
151
151
|
in_text: linkInjection.in_text ?? true,
|
|
152
152
|
inactive: !!linkInjection.inactive,
|
|
153
153
|
removed: !!linkInjection.removed,
|
|
154
|
+
type: linkInjection.type,
|
|
154
155
|
}))
|
|
155
156
|
|
|
156
157
|
const response = await fetchLinkInjections({
|
|
@@ -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,15 +7,17 @@ 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
|
-
include_count:
|
|
14
|
+
include_count: false,
|
|
14
15
|
participant_sid: participant.sid,
|
|
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/api/providers.ts
CHANGED
|
@@ -12,7 +12,7 @@ export async function fetchProviders(): Promise<PlaylinkData[]> {
|
|
|
12
12
|
const params = {
|
|
13
13
|
region: await getRegionBasedOnIp(),
|
|
14
14
|
page: 1,
|
|
15
|
-
include_count:
|
|
15
|
+
include_count: false,
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const response = await api<PlaylinkData[]>(`/providers/?api-token=${apiToken}&` + paramsToString(params))
|
package/src/lib/api/search.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getApiToken } from '$lib/token'
|
|
2
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
2
3
|
import type { TitleData } from '../types/title'
|
|
3
4
|
import { api } from './api'
|
|
4
5
|
|
|
@@ -9,3 +10,11 @@ export async function searchTitles(query: string): Promise<TitleData[]> {
|
|
|
9
10
|
|
|
10
11
|
return await api<TitleData[]>(`/search/titles/?api-token=${apiToken}&query=${query}`)
|
|
11
12
|
}
|
|
13
|
+
|
|
14
|
+
export async function searchParticipants(query: string): Promise<ParticipantData[]> {
|
|
15
|
+
const apiToken = getApiToken()
|
|
16
|
+
|
|
17
|
+
if (!apiToken) throw new Error('No token was provided')
|
|
18
|
+
|
|
19
|
+
return await api<ParticipantData[]>(`/search/participants/?api-token=${apiToken}&query=${query}`)
|
|
20
|
+
}
|
package/src/lib/api/titles.ts
CHANGED
package/src/lib/enums/Sorting.ts
CHANGED
|
@@ -45,8 +45,6 @@
|
|
|
45
45
|
const position: Position = JSON.parse(localStorage.getItem(editorPositionKey) || '{ "x": 0, "y": 0 }')
|
|
46
46
|
const height: number = parseInt(localStorage.getItem(editorHeightKey) || '0')
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
48
|
let editorElement: HTMLElement | null = $state(null)
|
|
51
49
|
let manualInjectionActive = $state(false)
|
|
52
50
|
let saving = $state(false)
|
|
@@ -113,6 +113,8 @@
|
|
|
113
113
|
<div class="header">
|
|
114
114
|
{#if type === 'title'}
|
|
115
115
|
<img class="poster" src={removeImageUrlPrefix(title_details!.standing_poster)} alt="" width="32" height="48" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
116
|
+
{:else if type === 'participant'}
|
|
117
|
+
<img class="poster participant" src={participant_details!.image} alt="" width="32" height="32" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
116
118
|
{:else}
|
|
117
119
|
<div class="placeholder-image"></div>
|
|
118
120
|
{/if}
|
|
@@ -207,7 +209,6 @@
|
|
|
207
209
|
.poster {
|
|
208
210
|
display: block;
|
|
209
211
|
width: margin(2);
|
|
210
|
-
height: margin(3);
|
|
211
212
|
border-radius: margin(0.25);
|
|
212
213
|
background: theme(content);
|
|
213
214
|
|
|
@@ -355,4 +356,3 @@
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
</style>
|
|
358
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { LinkInjection } from '$lib/types/injection'
|
|
2
|
+
import type { LinkInjection, LinkInjectionDataType } from '$lib/types/injection'
|
|
3
3
|
import type { TitleData } from '$lib/types/title'
|
|
4
4
|
import { onMount } from 'svelte'
|
|
5
5
|
import { playPilotBaseUrl } from '$lib/constants'
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
import RoundButton from '../RoundButton.svelte'
|
|
13
13
|
import Alert from './Alert.svelte'
|
|
14
14
|
import TextInput from './TextInput.svelte'
|
|
15
|
-
import
|
|
15
|
+
import TypeSearch from './Search/TypeSearch.svelte'
|
|
16
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
17
|
+
import { participantUrl, titleUrl } from '$lib/routes'
|
|
16
18
|
|
|
17
19
|
interface Props {
|
|
18
20
|
onsave: (linkInjection: LinkInjection) => void
|
|
@@ -25,7 +27,8 @@
|
|
|
25
27
|
|
|
26
28
|
let currentSelection = $state('')
|
|
27
29
|
let selectionSentence = $state('')
|
|
28
|
-
let
|
|
30
|
+
let selectedItem: TitleData | ParticipantData | null = $state(null)
|
|
31
|
+
let selectedType: LinkInjectionDataType = $state('title')
|
|
29
32
|
let phraseAfter = $state('')
|
|
30
33
|
let phraseBefore = $state('')
|
|
31
34
|
let error = $state('')
|
|
@@ -145,8 +148,9 @@
|
|
|
145
148
|
return total
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
function onselect(title: TitleData): void {
|
|
149
|
-
|
|
151
|
+
function onselect(title: TitleData | ParticipantData, type: LinkInjectionDataType): void {
|
|
152
|
+
selectedItem = title
|
|
153
|
+
selectedType = type
|
|
150
154
|
|
|
151
155
|
// Remove selection that was made anywhere on the page. This is to avoid the input from being overwritten
|
|
152
156
|
// by the selection right after selecting a result.
|
|
@@ -155,25 +159,28 @@
|
|
|
155
159
|
|
|
156
160
|
function save(): void {
|
|
157
161
|
if (!currentSelection) return
|
|
158
|
-
if (!
|
|
162
|
+
if (!selectedItem) return
|
|
159
163
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const linkInjection: LinkInjection = {
|
|
164
|
-
sid: selectedTitle.sid,
|
|
164
|
+
const linkInjection: Partial<LinkInjection> = {
|
|
165
|
+
sid: selectedItem.sid,
|
|
165
166
|
title: currentSelection,
|
|
166
167
|
sentence: selectionSentence,
|
|
167
168
|
phrase_before: phraseBefore,
|
|
168
169
|
phrase_after: phraseAfter,
|
|
169
|
-
|
|
170
|
-
key: generateInjectionKey(selectedTitle.sid),
|
|
171
|
-
title_details: selectedTitle,
|
|
170
|
+
key: generateInjectionKey(selectedItem.sid),
|
|
172
171
|
manual: true,
|
|
173
|
-
type:
|
|
172
|
+
type: selectedType,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (selectedType === 'title') {
|
|
176
|
+
linkInjection.title_details = selectedItem as TitleData
|
|
177
|
+
linkInjection.playpilot_url = titleUrl(selectedItem as TitleData)
|
|
178
|
+
} else if (selectedType === 'participant') {
|
|
179
|
+
linkInjection.participant_details = selectedItem as ParticipantData
|
|
180
|
+
linkInjection.playpilot_url = participantUrl(selectedItem as ParticipantData)
|
|
174
181
|
}
|
|
175
182
|
|
|
176
|
-
onsave(linkInjection)
|
|
183
|
+
onsave(linkInjection as LinkInjection)
|
|
177
184
|
onclose()
|
|
178
185
|
}
|
|
179
186
|
</script>
|
|
@@ -198,13 +205,13 @@
|
|
|
198
205
|
{/if}
|
|
199
206
|
|
|
200
207
|
{#if !error}
|
|
201
|
-
<label for="text">Paired
|
|
208
|
+
<label for="text">Paired link</label>
|
|
202
209
|
{#key selectionSentence + currentSelection}
|
|
203
|
-
<
|
|
210
|
+
<TypeSearch {onselect} bind:query />
|
|
204
211
|
{/key}
|
|
205
212
|
{/if}
|
|
206
213
|
|
|
207
|
-
<button class="save" onclick={save} disabled={!currentSelection || !
|
|
214
|
+
<button class="save" onclick={save} disabled={!currentSelection || !selectedItem}>
|
|
208
215
|
Add playlink
|
|
209
216
|
</button>
|
|
210
217
|
</section>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// In many parts this is a copy of ./TitleSearchItem.svelte
|
|
3
|
+
// Perhaps worth unifying the two in some sense, but for now I decided to keep it simple.
|
|
4
|
+
|
|
5
|
+
import { imagePlaceholderDataUrl } from '$lib/constants'
|
|
6
|
+
import { removeImageUrlPrefix } from '$lib/image'
|
|
7
|
+
import { participantUrl } from '$lib/routes'
|
|
8
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
9
|
+
import IconNewTab from '../../Icons/IconNewTab.svelte'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
participant: ParticipantData
|
|
13
|
+
hoverable?: boolean
|
|
14
|
+
onclick?: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { participant, hoverable = true, onclick = () => null }: Props = $props()
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<button class="item" class:hoverable {onclick}>
|
|
21
|
+
<img class="image" src={participant.image || imagePlaceholderDataUrl} alt="" width="42" height="42" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
22
|
+
|
|
23
|
+
<div class="content">
|
|
24
|
+
<div class="name">{participant.name}</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<a
|
|
28
|
+
href={participantUrl(participant)}
|
|
29
|
+
target="_blank"
|
|
30
|
+
class="open-in-new-tab"
|
|
31
|
+
onclick={event => event.stopImmediatePropagation()}>
|
|
32
|
+
<IconNewTab />
|
|
33
|
+
</a>
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<style lang="scss">
|
|
37
|
+
.item {
|
|
38
|
+
appearance: none;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: margin(1);
|
|
42
|
+
width: 100%;
|
|
43
|
+
background: transparent;
|
|
44
|
+
border: 0;
|
|
45
|
+
padding: margin(0.5);
|
|
46
|
+
border-bottom: 1px solid theme(content);
|
|
47
|
+
color: theme(text-color-alt);
|
|
48
|
+
font-family: inherit;
|
|
49
|
+
text-align: left;
|
|
50
|
+
font-size: margin(0.85);
|
|
51
|
+
|
|
52
|
+
&.hoverable {
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
|
|
55
|
+
&:hover {
|
|
56
|
+
background: theme(lighter);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&:last-child {
|
|
61
|
+
border-bottom: 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.image {
|
|
66
|
+
flex: 0 0 auto;
|
|
67
|
+
height: auto;
|
|
68
|
+
width: margin(1.75);
|
|
69
|
+
aspect-ratio: 1/1;
|
|
70
|
+
border-radius: margin(0.25);
|
|
71
|
+
background: theme(content);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.name {
|
|
75
|
+
color: theme(text-color);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.open-in-new-tab {
|
|
79
|
+
margin-left: auto;
|
|
80
|
+
color: theme(text-color-alt);
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
color: theme(text-color);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { searchTitles, searchParticipants } from '$lib/api/search'
|
|
3
|
+
import type { LinkInjectionDataType } from '$lib/types/injection'
|
|
4
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
5
|
+
import type { TitleData } from '$lib/types/title'
|
|
6
|
+
import Tabs from '../../Tabs.svelte'
|
|
7
|
+
import TextInput from '../TextInput.svelte'
|
|
8
|
+
import ParticipantSearchItem from './ParticipantSearchItem.svelte'
|
|
9
|
+
import TitleSearchItem from './TitleSearchItem.svelte'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
onselect?: (item: TitleData | ParticipantData, type: LinkInjectionDataType) => void
|
|
13
|
+
query: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { onselect = () => null, query = $bindable() }: Props = $props()
|
|
17
|
+
|
|
18
|
+
const tabs: { label: string, value: LinkInjectionDataType }[] = [{ label: 'Movies & Shows', value: 'title' }, { label: 'Cast & Crew', value: 'participant' }]
|
|
19
|
+
|
|
20
|
+
let results: TitleData[] | ParticipantData[] = $state([])
|
|
21
|
+
let selectedResult: TitleData | ParticipantData | null = $state(null)
|
|
22
|
+
let currentTab: LinkInjectionDataType = $state(tabs[0].value)
|
|
23
|
+
let loading = $state(false)
|
|
24
|
+
|
|
25
|
+
$effect(() => {
|
|
26
|
+
if (query) search(query)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
async function search(query: string): Promise<void> {
|
|
30
|
+
loading = true
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
results = currentTab === 'title' ? await searchTitles(query) : await searchParticipants(query)
|
|
34
|
+
} finally {
|
|
35
|
+
loading = false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function select(item: TitleData | ParticipantData): void {
|
|
40
|
+
query = currentTab === 'title' ? (item as TitleData).title : (item as ParticipantData).name
|
|
41
|
+
selectedResult = item
|
|
42
|
+
|
|
43
|
+
onselect(item, currentTab)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setTab(value: string): void {
|
|
47
|
+
currentTab = value as LinkInjectionDataType
|
|
48
|
+
query = ''
|
|
49
|
+
selectedResult = null
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<Tabs options={tabs} onclick={(value) => setTab(value)} />
|
|
54
|
+
|
|
55
|
+
<div class="search">
|
|
56
|
+
<TextInput
|
|
57
|
+
bind:value={query}
|
|
58
|
+
oninput={() => selectedResult = null}
|
|
59
|
+
name="search"
|
|
60
|
+
label="Search..." />
|
|
61
|
+
|
|
62
|
+
{#if query && !selectedResult}
|
|
63
|
+
<div class="results" data-testid="search-results">
|
|
64
|
+
{#each results as item (item.sid)}
|
|
65
|
+
{#if currentTab === 'title'}
|
|
66
|
+
<TitleSearchItem title={item as TitleData} onclick={() => select(item as TitleData)} />
|
|
67
|
+
{:else if currentTab === 'participant'}
|
|
68
|
+
<ParticipantSearchItem participant={item as ParticipantData} onclick={() => select(item as ParticipantData)} />
|
|
69
|
+
{/if}
|
|
70
|
+
{:else}
|
|
71
|
+
{#if !loading}
|
|
72
|
+
<em class="empty">No results found</em>
|
|
73
|
+
{/if}
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
{#if selectedResult}
|
|
79
|
+
<div class="selected">
|
|
80
|
+
{#if currentTab === 'title'}
|
|
81
|
+
<TitleSearchItem title={selectedResult as TitleData} hoverable={false} />
|
|
82
|
+
{:else if currentTab === 'participant'}
|
|
83
|
+
<ParticipantSearchItem participant={selectedResult as ParticipantData} hoverable={false} />
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<style lang="scss">
|
|
90
|
+
.search {
|
|
91
|
+
position: relative;
|
|
92
|
+
margin-top: margin(0.5);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.results {
|
|
96
|
+
z-index: 1;
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: calc(100% + margin(0.5));
|
|
99
|
+
left: 0;
|
|
100
|
+
right: 0;
|
|
101
|
+
height: 30vh;
|
|
102
|
+
max-height: margin(10);
|
|
103
|
+
border-radius: margin(0.5);
|
|
104
|
+
background: theme(lighter);
|
|
105
|
+
scrollbar-width: thin;
|
|
106
|
+
overflow: auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.empty {
|
|
110
|
+
padding: margin(0.5);
|
|
111
|
+
font-size: margin(0.75);
|
|
112
|
+
color: theme(text-color-alt);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.selected {
|
|
116
|
+
margin: margin(0.5) 0;
|
|
117
|
+
border-radius: 0.5rem;
|
|
118
|
+
border: 2px solid theme(green);
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
@@ -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>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import Button from './Button.svelte'
|
|
3
|
+
|
|
2
4
|
interface Option {
|
|
3
5
|
label: string
|
|
4
6
|
value: string
|
|
@@ -6,16 +8,22 @@
|
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
options: Option[]
|
|
11
|
+
onclick?: (value: string) => void
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
const { options }: Props = $props()
|
|
14
|
+
const { options, onclick = () => null }: Props = $props()
|
|
15
|
+
|
|
16
|
+
let active = $derived(options[0]?.value)
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
function onchange(value: string): void {
|
|
19
|
+
active = value
|
|
20
|
+
onclick(value)
|
|
21
|
+
}
|
|
14
22
|
</script>
|
|
15
23
|
|
|
16
24
|
<div class="tabs">
|
|
17
25
|
{#each options as { label, value }}
|
|
18
|
-
<
|
|
26
|
+
<Button variant={value === active ? 'filled' : 'border'} onclick={() => onchange(value)}>{label}</Button>
|
|
19
27
|
{/each}
|
|
20
28
|
</div>
|
|
21
29
|
|
|
@@ -23,25 +31,6 @@
|
|
|
23
31
|
.tabs {
|
|
24
32
|
display: flex;
|
|
25
33
|
gap: margin(0.5);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.tab {
|
|
29
|
-
cursor: pointer;
|
|
30
|
-
appearance: none;
|
|
31
|
-
background: transparent;
|
|
32
|
-
padding: margin(0.25) margin(0.5);
|
|
33
|
-
border: 1px solid currentColor;
|
|
34
|
-
border-radius: theme(tabs-border-radius, border-radius-small);
|
|
35
|
-
transition: opacity 100ms;
|
|
36
|
-
font-family: inherit;
|
|
37
|
-
font-weight: theme(tabs-font-weight, 500);
|
|
38
|
-
color: theme(tabs-text-color, text-color-alt);
|
|
39
|
-
font-size: inherit;
|
|
40
|
-
|
|
41
|
-
&.active,
|
|
42
|
-
&:hover {
|
|
43
|
-
background: theme(tabs-background-active, content);
|
|
44
|
-
color: theme(tabs-text-color-active, text-color);
|
|
45
|
-
}
|
|
34
|
+
font-size: theme(font-size-base);
|
|
46
35
|
}
|
|
47
36
|
</style>
|
|
@@ -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
|
|
@@ -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>
|