@playpilot/tpi 8.35.1 → 8.36.0-beta.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.
@@ -1,9 +1,7 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte'
3
2
  import { heading } from '$lib/actions/heading'
4
- import { fetchTitlesForParticipant } from '$lib/api/participants'
5
- import { openModal } from '$lib/modal'
6
- import type { ParticipantData } from '$lib/types/participant'
3
+ import type { Job, ParticipantData } from '$lib/types/participant'
4
+ import type { ExploreFilter, ExploreTitleSorting } from '$lib/types/filter'
7
5
  import type { TitleData } from '$lib/types/title'
8
6
  import { Sorting } from '$lib/enums/Sorting'
9
7
  import { t } from '$lib/localization'
@@ -11,66 +9,41 @@
11
9
  import { trackViaPixel } from '@playpilot/retargeting-tracking'
12
10
  import { MetaEvent } from '$lib/enums/TrackingEvent'
13
11
  import ParticipantImage from './ParticipantImage.svelte'
14
- import ListTitle from '../ListTitle.svelte'
15
- import Dropdown from '../Explore/Filter/Dropdown.svelte'
12
+ import ExploreResults from '../Explore/Routes/ExploreResults.svelte'
13
+ import Filter from '../Explore/Filter/Filter.svelte'
16
14
  import Button from '../Button.svelte'
15
+ import { openModal } from '$lib/modal'
17
16
 
18
17
  interface Props {
19
18
  participant: ParticipantData
20
19
  small?: boolean
21
20
  }
22
21
 
23
- type TitleSorting = { label: string; value: (typeof Sorting)[keyof typeof Sorting] }
24
-
25
22
  const { participant, small = false }: Props = $props()
26
23
 
27
- const { name, birth_date, death_date } = $derived(participant)
24
+ const { name, description } = $derived(participant)
28
25
 
29
- const sortings: TitleSorting[] = [
26
+ const filter: ExploreFilter = $state({})
27
+ const sortings: ExploreTitleSorting[] = [
30
28
  { label: 'Popularity', value: Sorting.Popular },
31
29
  { label: 'Top Rated', value: Sorting.Best },
32
30
  { label: 'Year', value: Sorting.Chronological },
33
31
  ]
34
32
 
35
- const pageSize = 30
36
-
37
- let titles: TitleData[] = $state([])
38
- let page = $state(1)
39
- let hasMorePages = $state(true)
40
- let loading = $state(true)
41
- let currentSorting = $state(sortings[0])
33
+ let clientWidth = $state(0)
34
+ let currentJob: 'all' | Job = $state('all')
35
+ let descriptionExpanded: boolean = $state(false)
42
36
 
43
- onMount(loadMore)
37
+ const filterWithParticipant: ExploreFilter = $derived({
38
+ ...filter,
39
+ participant_sid: { type: 'string', value: participant.sid },
40
+ participant_job: { type: 'string', value: currentJob === 'all' ? null : currentJob }
41
+ })
44
42
 
45
43
  if (isRetargetingAllowed()) trackViaPixel(MetaEvent.ParticipantView, { participant: participant.name })
46
44
 
47
- function formatDate(dateString: string): string {
48
- const date = new Date(dateString)
49
- return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
50
- }
51
-
52
- async function loadMore(): Promise<void> {
53
- loading = true
54
-
55
- try {
56
- const results = await fetchTitlesForParticipant(participant, { page, sorting: currentSorting.value })
57
-
58
- titles = [...titles, ...results]
59
- hasMorePages = results?.length >= pageSize
60
- } catch {
61
- hasMorePages = false
62
- } finally {
63
- loading = false
64
- page += 1
65
- }
66
- }
67
-
68
- function setSorting(sorting: TitleSorting): void {
69
- currentSorting = sorting
70
- titles = []
71
- page = 1
72
-
73
- loadMore()
45
+ function onTitleClick(event: MouseEvent, titles: TitleData[], index: number): void {
46
+ openModal({ event, type: 'titles-rail', data: titles, props: { initialIndex: index } })
74
47
  }
75
48
  </script>
76
49
 
@@ -80,60 +53,46 @@
80
53
  <div>
81
54
  <div class="heading" use:heading={2} id="heading">{name}</div>
82
55
 
83
- {#if birth_date}
84
- <p class="dates">
85
- {t('Born On')} <strong>{formatDate(birth_date)}</strong>{#if death_date}, {t('Died On')} <strong>{formatDate(death_date)}</strong>{/if}
56
+ {#if description}
57
+ <p class="description" class:expanded={descriptionExpanded}>
58
+ {description}
86
59
  </p>
60
+
61
+ <div class="read-more">
62
+ <Button variant="link" onclick={() => descriptionExpanded = !descriptionExpanded}>
63
+ {descriptionExpanded ? t('Read Less') : t('Read More')}
64
+ </Button>
65
+ </div>
87
66
  {/if}
88
67
  </div>
89
68
  </div>
90
69
 
91
- <div class="content">
92
- {#if !small && (titles.length && !loading)}
70
+ <div class="content" class:small bind:clientWidth>
71
+ {#if !small}
93
72
  <div class="list-header">
94
73
  <div class="heading subheading" use:heading={3} id="credits">{t('Credits')}</div>
74
+ </div>
95
75
 
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>
76
+ <div class="jobs">
77
+ {#each ['all', ...participant.jobs] as job}
78
+ <Button variant="border" size="large" active={currentJob === job} onclick={() => currentJob = job as 'all' | Job}>
79
+ {t('Job: ' + job)}
80
+ </Button>
81
+ {/each}
113
82
  </div>
114
- {/if}
115
83
 
116
- <div class="list">
117
- {#each titles as title}
118
- <ListTitle {title} compact={small} onclick={(event) => openModal({ event, data: title })} />
119
- {/each}
84
+ <Filter {filter} {sortings} limit={clientWidth < 700} hideActive />
85
+ {/if}
120
86
 
121
- {#if loading}
122
- {#each { length: 6 }}
123
- <div class="skeleton" data-testid="skeleton"></div>
124
- {/each}
125
- {:else if hasMorePages}
126
- <button class="more" onclick={loadMore}>{t('Show More')}</button>
127
- {/if}
128
- </div>
87
+ <ExploreResults forceGrid hidePlaylinks filter={filterWithParticipant} {onTitleClick} />
129
88
  </div>
130
89
 
131
90
  <style lang="scss">
132
91
  .header {
133
- --participant-image-size: #{margin(6)};
92
+ --participant-image-size: #{margin(5)};
134
93
  display: grid;
135
- grid-template-columns: calc(var(--participant-image-size)) auto;
136
- align-items: center;
94
+ grid-template-columns: var(--participant-image-size) calc(100% - var(--participant-image-size) - margin(1));
95
+ align-items: start;
137
96
  gap: margin(1);
138
97
  padding: margin(3) margin(1) margin(2);
139
98
  font-family: theme(detail-font-family, font-family);
@@ -144,11 +103,13 @@
144
103
 
145
104
  @include desktop {
146
105
  --participant-image-size: #{margin(8)};
106
+ padding: margin(3) margin(2) margin(2);
147
107
  }
148
108
 
149
109
  &.small {
150
110
  --participant-image-size: #{margin(6)};
151
- padding: margin(1);
111
+ padding: margin(1) margin(1) 0;
112
+ align-items: center;
152
113
  }
153
114
  }
154
115
 
@@ -161,19 +122,14 @@
161
122
  font-style: theme(detail-title-font-style, normal);
162
123
 
163
124
  &.subheading {
164
- margin: 0;
165
125
  font-size: theme(detail-title-small-font-size, margin(1.25));
166
126
  }
167
127
 
168
128
  .small & {
169
- font-size: 1em;
129
+ font-size: 1.25em;
170
130
  }
171
131
  }
172
132
 
173
- .dates {
174
- margin: margin(0.5) 0 0;
175
- }
176
-
177
133
  .content {
178
134
  padding: 0 margin(1) margin(1);
179
135
  color: theme(detail-text-color, text-color);
@@ -183,16 +139,38 @@
183
139
  line-height: normal;
184
140
  font-style: normal;
185
141
 
142
+ @include desktop {
143
+ padding: 0 margin(2) margin(2);
144
+
145
+ &.small {
146
+ padding: 0 margin(1) margin(1);
147
+ }
148
+ }
149
+
186
150
  ::view-transition-old(content),
187
151
  ::view-transition-new(content) {
188
152
  height: 100%;
189
153
  }
190
154
  }
191
155
 
192
- .list {
193
- display: flex;
194
- flex-direction: column;
195
- gap: margin(0.5);
156
+ .description {
157
+ display: -webkit-box;
158
+ -webkit-box-orient: vertical;
159
+ margin: margin(0.5) 0 0;
160
+ overflow: hidden;
161
+ -webkit-line-clamp: 3;
162
+ line-clamp: 3;
163
+ color: theme(text-color-alt);
164
+
165
+ &.expanded {
166
+ display: block;
167
+ }
168
+ }
169
+
170
+ .read-more {
171
+ :global(.button) {
172
+ font-weight: bold;
173
+ }
196
174
  }
197
175
 
198
176
  .list-header {
@@ -202,47 +180,10 @@
202
180
  margin-bottom: margin(0.5);
203
181
  }
204
182
 
205
- .skeleton {
206
- min-height: margin(7.25);
207
- border-radius: theme(playlink-border-radius, border-radius);
208
- background: theme(detail-background-light, lighter);
209
- }
210
-
211
- .more {
212
- cursor: pointer;
213
- appearance: none;
214
- padding: margin(0.5) margin(1);
215
- border: 0;
216
- border-radius: theme(participants-load-more-border-radius, border-radius);
217
- background: theme(participants-load-more-background, content);
218
- color: inherit;
219
- font-size: inherit;
220
- font-family: inherit;
221
-
222
- &:hover {
223
- filter: brightness(1.2);
224
- }
225
- }
226
-
227
- .sort {
183
+ .jobs {
228
184
  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
- }
185
+ flex-wrap: wrap;
186
+ gap: margin(0.5);
187
+ margin-bottom: margin(0.5);
247
188
  }
248
189
  </style>
@@ -98,7 +98,8 @@
98
98
  margin: 0;
99
99
  border: 0;
100
100
  background: transparent;
101
- color: rgba(255, 255, 255, 0.75);
101
+ color: rgba(255, 255, 255, 0.65);
102
+ filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35));
102
103
 
103
104
  &:hover {
104
105
  color: white;
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
3
3
  import { api } from '$lib/api/api'
4
4
  import { fetchProviders } from '$lib/api/providers'
5
5
  import { getApiToken } from '$lib/token'
6
+ import { title } from '$lib/fakeData'
6
7
 
7
8
  vi.mock('$lib/token', () => ({
8
9
  getApiToken: vi.fn(),
@@ -33,5 +34,13 @@ describe('$lib/api/ads', () => {
33
34
 
34
35
  await expect(async () => await fetchProviders()).rejects.toThrow()
35
36
  })
37
+
38
+ it('Should remove duplicates', async () => {
39
+ vi.mocked(api).mockResolvedValueOnce([title.providers[0], title.providers[0]])
40
+
41
+ const response = await fetchProviders()
42
+
43
+ expect(response).toEqual([title.providers[0]])
44
+ })
36
45
  })
37
46
  })
@@ -25,8 +25,9 @@ describe('Filter.svelte', () => {
25
25
  it('Should not limit the number of filter items shown after pressing expand button', async () => {
26
26
  const { getByText, getAllByTestId } = render(Filter, { filter: {} })
27
27
 
28
- await fireEvent.click(getByText('All filters'))
28
+ await fireEvent.click(getByText('Show more'))
29
29
 
30
+ expect(getByText('Show less')).toBeTruthy()
30
31
  expect(getAllByTestId('filter-item')).toHaveLength(7)
31
32
  })
32
33
 
@@ -3,15 +3,15 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
3
3
 
4
4
  import Participant from '../../../../routes/components/Participants/Participant.svelte'
5
5
  import { participants, title } from '$lib/fakeData'
6
- import { fetchTitlesForParticipant } from '$lib/api/participants'
6
+ import { fetchTitles } from '$lib/api/titles'
7
7
  import { Sorting } from '$lib/enums/Sorting'
8
8
 
9
9
  vi.mock('$lib/tracking', () => ({
10
10
  track: vi.fn(),
11
11
  }))
12
12
 
13
- vi.mock('$lib/api/participants', () => ({
14
- fetchTitlesForParticipant: vi.fn(),
13
+ vi.mock('$lib/api/titles', () => ({
14
+ fetchTitles: vi.fn(),
15
15
  }))
16
16
 
17
17
  describe('Participant.svelte', () => {
@@ -19,18 +19,20 @@ describe('Participant.svelte', () => {
19
19
  vi.resetAllMocks()
20
20
  })
21
21
 
22
- it('Should call fetchTitlesForParticipant on mount', () => {
22
+ it('Should call fetchTitles on mount', async () => {
23
23
  render(Participant, { participant: participants[0] })
24
24
 
25
- expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 1, sorting: Sorting.Popular })
25
+ await waitFor(() => {
26
+ expect(fetchTitles).toHaveBeenCalled()
27
+ })
26
28
  })
27
29
 
28
30
  it('Should render fetched titles after showing skeletons', async () => {
29
- vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce([title, title])
31
+ vi.mocked(fetchTitles).mockResolvedValueOnce({ results: [title, title], next: 'next', previous: 'previous' })
30
32
 
31
33
  const { getAllByTestId } = render(Participant, { participant: participants[0] })
32
34
 
33
- expect(getAllByTestId('skeleton')).toHaveLength(6)
35
+ expect(getAllByTestId('skeleton')).toHaveLength(24)
34
36
 
35
37
  await waitFor(() => {
36
38
  expect(getAllByTestId('title')).toHaveLength(2)
@@ -38,41 +40,68 @@ describe('Participant.svelte', () => {
38
40
  })
39
41
 
40
42
  it('Should not show load more button while loading and when fewer than page size of titles are fetched', async () => {
41
- vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce([title, title])
43
+ vi.mocked(fetchTitles).mockResolvedValueOnce({ results: [title, title], next: null, previous: 'previous' })
42
44
 
43
- const { queryByText, getAllByTestId } = render(Participant, { participant: participants[0] })
45
+ const { queryByLabelText, getAllByTestId } = render(Participant, { participant: participants[0] })
44
46
 
45
- expect(queryByText('Show More')).not.toBeTruthy()
47
+ expect(queryByLabelText('Show more movies & shows')).not.toBeTruthy()
46
48
 
47
49
  await waitFor(() => {
48
50
  expect(getAllByTestId('title')).toHaveLength(2)
49
51
  })
50
52
 
51
- expect(queryByText('Show More')).not.toBeTruthy()
53
+ expect(queryByLabelText('Show more movies & shows')).not.toBeTruthy()
52
54
  })
53
55
 
54
56
  it('Should show load more button after loading and if more than page size of titles are fetched', async () => {
55
- const resolved = Array(30).fill(title)
56
- vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce(resolved)
57
+ const resolved = ({ results: Array(30).fill(title), next: 'next', previous: 'previous' })
58
+ vi.mocked(fetchTitles).mockResolvedValueOnce(resolved)
57
59
 
58
- const { getByText } = render(Participant, { participant: participants[0] })
60
+ const { getByLabelText } = render(Participant, { participant: participants[0] })
59
61
 
60
62
  await waitFor(() => {
61
- expect(getByText('Show more')).toBeTruthy()
63
+ expect(getByLabelText('Show more movies & shows')).toBeTruthy()
62
64
  })
63
65
  })
64
66
 
65
67
  it('Should fetch more titles when load more button is clicked', async () => {
66
- const resolved = Array(30).fill(title)
67
- vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce(resolved)
68
+ const resolved = ({ results: Array(30).fill(title), next: 'next', previous: 'previous' })
69
+ vi.mocked(fetchTitles).mockResolvedValueOnce(resolved)
70
+
71
+ const { getByLabelText } = render(Participant, { participant: participants[0] })
72
+
73
+ await waitFor(() => getByLabelText('Show more movies & shows'))
74
+
75
+ await fireEvent.click(getByLabelText('Show more movies & shows'))
76
+
77
+ expect(fetchTitles).toHaveBeenCalledWith({ page: 2, page_size: expect.any(Number), participant_job: null, participant_sid: participants[0].sid })
78
+ })
79
+
80
+ it('Should include participant role in fetchTitles when selected and refetch', async () => {
81
+ const resolved = ({ results: Array(30).fill(title), next: 'next', previous: 'previous' })
82
+ vi.mocked(fetchTitles).mockResolvedValueOnce(resolved)
68
83
 
69
84
  const { getByText } = render(Participant, { participant: participants[0] })
70
85
 
71
- await waitFor(() => getByText('Show more'))
86
+ await fireEvent.click(getByText('Actor'))
87
+
88
+ await waitFor(() => {
89
+ expect(fetchTitles).toHaveBeenCalledWith(expect.objectContaining({ participant_job: 'actor' }))
90
+ })
91
+ })
72
92
 
73
- await fireEvent.click(getByText('Show more'))
93
+ it('Should include selected sorting specific to participants', async () => {
94
+ const resolved = ({ results: Array(30).fill(title), next: 'next', previous: 'previous' })
95
+ vi.mocked(fetchTitles).mockResolvedValueOnce(resolved)
74
96
 
75
- expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 2, sorting: Sorting.Popular })
97
+ const { getByText } = render(Participant, { participant: participants[0] })
98
+
99
+ await fireEvent.click(getByText('Sort by'))
100
+ await fireEvent.click(getByText('Year'))
101
+
102
+ await waitFor(() => {
103
+ expect(fetchTitles).toHaveBeenCalledWith(expect.objectContaining({ ordering: Sorting.Chronological }))
104
+ })
76
105
  })
77
106
 
78
107
  it('Should render as small variant when given', () => {
@@ -93,11 +122,20 @@ describe('Participant.svelte', () => {
93
122
  })
94
123
 
95
124
  it('Should not render image if not given', async () => {
96
-
97
125
  const { queryByRole } = render(Participant, { participant: participants[0] })
98
126
 
99
127
  await waitFor(() => {
100
128
  expect(queryByRole('presentation')).not.toBeTruthy()
101
129
  })
102
130
  })
131
+
132
+ it('Should render description and toggle more on click of Read more button', async () => {
133
+ const { getByText } = render(Participant, { participant: participants[0] })
134
+
135
+ expect(getByText(participants[0].description)).toBeTruthy()
136
+
137
+ await fireEvent.click(getByText('Read more'))
138
+
139
+ expect(getByText('Read less')).toBeTruthy()
140
+ })
103
141
  })