@playpilot/tpi 8.35.0-beta.2 → 8.35.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,7 +1,9 @@
1
1
  <script lang="ts">
2
+ import { onMount } from 'svelte'
2
3
  import { heading } from '$lib/actions/heading'
3
- import type { Job, ParticipantData } from '$lib/types/participant'
4
- import type { ExploreFilter, ExploreTitleSorting } from '$lib/types/filter'
4
+ import { fetchTitlesForParticipant } from '$lib/api/participants'
5
+ import { openModal } from '$lib/modal'
6
+ import type { ParticipantData } from '$lib/types/participant'
5
7
  import type { TitleData } from '$lib/types/title'
6
8
  import { Sorting } from '$lib/enums/Sorting'
7
9
  import { t } from '$lib/localization'
@@ -9,40 +11,66 @@
9
11
  import { trackViaPixel } from '@playpilot/retargeting-tracking'
10
12
  import { MetaEvent } from '$lib/enums/TrackingEvent'
11
13
  import ParticipantImage from './ParticipantImage.svelte'
12
- import ExploreResults from '../Explore/Routes/ExploreResults.svelte'
13
- import Filter from '../Explore/Filter/Filter.svelte'
14
+ import ListTitle from '../ListTitle.svelte'
15
+ import Dropdown from '../Explore/Filter/Dropdown.svelte'
14
16
  import Button from '../Button.svelte'
15
- import { openModal } from '$lib/modal'
16
17
 
17
18
  interface Props {
18
19
  participant: ParticipantData
19
20
  small?: boolean
20
21
  }
21
22
 
23
+ type TitleSorting = { label: string; value: (typeof Sorting)[keyof typeof Sorting] }
24
+
22
25
  const { participant, small = false }: Props = $props()
23
26
 
24
- const { name, description } = $derived(participant)
27
+ const { name, birth_date, death_date } = $derived(participant)
25
28
 
26
- const filter: ExploreFilter = $state({})
27
- const sortings: ExploreTitleSorting[] = [
29
+ const sortings: TitleSorting[] = [
28
30
  { label: 'Popularity', value: Sorting.Popular },
29
31
  { label: 'Top Rated', value: Sorting.Best },
30
32
  { label: 'Year', value: Sorting.Chronological },
31
33
  ]
32
34
 
33
- let currentJob: 'all' | Job = $state('all')
34
- let descriptionExpanded: boolean = $state(false)
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])
35
42
 
36
- const filterWithParticipant: ExploreFilter = $derived({
37
- ...filter,
38
- participant_sid: { type: 'string', value: participant.sid },
39
- participant_job: { type: 'string', value: currentJob === 'all' ? null : currentJob }
40
- })
43
+ onMount(loadMore)
41
44
 
42
45
  if (isRetargetingAllowed()) trackViaPixel(MetaEvent.ParticipantView, { participant: participant.name })
43
46
 
44
- function onTitleClick(event: MouseEvent, titles: TitleData[], index: number): void {
45
- openModal({ event, type: 'titles-rail', data: titles, props: { initialIndex: index } })
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()
46
74
  }
47
75
  </script>
48
76
 
@@ -52,46 +80,60 @@
52
80
  <div>
53
81
  <div class="heading" use:heading={2} id="heading">{name}</div>
54
82
 
55
- {#if description}
56
- <p class="description" class:expanded={descriptionExpanded}>
57
- {description}
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}
58
86
  </p>
59
-
60
- <div class="read-more">
61
- <Button variant="link" onclick={() => descriptionExpanded = !descriptionExpanded}>
62
- {descriptionExpanded ? t('Read Less') : t('Read More')}
63
- </Button>
64
- </div>
65
87
  {/if}
66
88
  </div>
67
89
  </div>
68
90
 
69
- <div class="content" class:small>
70
- {#if !small}
91
+ <div class="content">
92
+ {#if !small && (titles.length && !loading)}
71
93
  <div class="list-header">
72
94
  <div class="heading subheading" use:heading={3} id="credits">{t('Credits')}</div>
73
- </div>
74
95
 
75
- <div class="jobs">
76
- {#each ['all', ...participant.jobs] as job}
77
- <Button variant="border" size="large" active={currentJob === job} onclick={() => currentJob = job as 'all' | Job}>
78
- {t('Job: ' + job)}
79
- </Button>
80
- {/each}
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>
81
113
  </div>
82
-
83
- <Filter {filter} {sortings} hideActive />
84
114
  {/if}
85
115
 
86
- <ExploreResults forceGrid hidePlaylinks filter={filterWithParticipant} {onTitleClick} />
116
+ <div class="list">
117
+ {#each titles as title}
118
+ <ListTitle {title} compact={small} onclick={(event) => openModal({ event, data: title })} />
119
+ {/each}
120
+
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
129
  </div>
88
130
 
89
131
  <style lang="scss">
90
132
  .header {
91
- --participant-image-size: #{margin(5)};
133
+ --participant-image-size: #{margin(6)};
92
134
  display: grid;
93
- grid-template-columns: var(--participant-image-size) calc(100% - var(--participant-image-size) - margin(1));
94
- align-items: start;
135
+ grid-template-columns: calc(var(--participant-image-size)) auto;
136
+ align-items: center;
95
137
  gap: margin(1);
96
138
  padding: margin(3) margin(1) margin(2);
97
139
  font-family: theme(detail-font-family, font-family);
@@ -102,13 +144,11 @@
102
144
 
103
145
  @include desktop {
104
146
  --participant-image-size: #{margin(8)};
105
- padding: margin(3) margin(2) margin(2);
106
147
  }
107
148
 
108
149
  &.small {
109
150
  --participant-image-size: #{margin(6)};
110
- padding: margin(1) margin(1) 0;
111
- align-items: center;
151
+ padding: margin(1);
112
152
  }
113
153
  }
114
154
 
@@ -121,14 +161,19 @@
121
161
  font-style: theme(detail-title-font-style, normal);
122
162
 
123
163
  &.subheading {
164
+ margin: 0;
124
165
  font-size: theme(detail-title-small-font-size, margin(1.25));
125
166
  }
126
167
 
127
168
  .small & {
128
- font-size: 1.25em;
169
+ font-size: 1em;
129
170
  }
130
171
  }
131
172
 
173
+ .dates {
174
+ margin: margin(0.5) 0 0;
175
+ }
176
+
132
177
  .content {
133
178
  padding: 0 margin(1) margin(1);
134
179
  color: theme(detail-text-color, text-color);
@@ -138,38 +183,16 @@
138
183
  line-height: normal;
139
184
  font-style: normal;
140
185
 
141
- @include desktop {
142
- padding: 0 margin(2) margin(2);
143
-
144
- &.small {
145
- padding: 0 margin(1) margin(1);
146
- }
147
- }
148
-
149
186
  ::view-transition-old(content),
150
187
  ::view-transition-new(content) {
151
188
  height: 100%;
152
189
  }
153
190
  }
154
191
 
155
- .description {
156
- display: -webkit-box;
157
- -webkit-box-orient: vertical;
158
- margin: margin(0.5) 0 0;
159
- overflow: hidden;
160
- -webkit-line-clamp: 3;
161
- line-clamp: 3;
162
- color: theme(text-color-alt);
163
-
164
- &.expanded {
165
- display: block;
166
- }
167
- }
168
-
169
- .read-more {
170
- :global(.button) {
171
- font-weight: bold;
172
- }
192
+ .list {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: margin(0.5);
173
196
  }
174
197
 
175
198
  .list-header {
@@ -179,10 +202,47 @@
179
202
  margin-bottom: margin(0.5);
180
203
  }
181
204
 
182
- .jobs {
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
228
  display: flex;
184
- flex-wrap: wrap;
185
- gap: margin(0.5);
186
- margin-bottom: margin(0.5);
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
+ }
187
247
  }
188
248
  </style>
@@ -113,10 +113,11 @@ describe('main.ts', () => {
113
113
  })
114
114
 
115
115
  describe('mount', () => {
116
- it('Should insert script tag', () => {
116
+ it('Should insert both script and style tags', () => {
117
117
  window.PlayPilotLinkInjections.mount()
118
118
 
119
119
  expect(document.querySelector('script#playpilot-mount')).toBeTruthy()
120
+ expect(document.querySelector('link#playpilot-mount-style')).toBeTruthy()
120
121
  })
121
122
 
122
123
  it('Should insert script tag with editorial mode if editiorial mode is enabled', () => {
@@ -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 { fetchTitles } from '$lib/api/titles'
6
+ import { fetchTitlesForParticipant } from '$lib/api/participants'
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/titles', () => ({
14
- fetchTitles: vi.fn(),
13
+ vi.mock('$lib/api/participants', () => ({
14
+ fetchTitlesForParticipant: vi.fn(),
15
15
  }))
16
16
 
17
17
  describe('Participant.svelte', () => {
@@ -19,20 +19,18 @@ describe('Participant.svelte', () => {
19
19
  vi.resetAllMocks()
20
20
  })
21
21
 
22
- it('Should call fetchTitles on mount', async () => {
22
+ it('Should call fetchTitlesForParticipant on mount', () => {
23
23
  render(Participant, { participant: participants[0] })
24
24
 
25
- await waitFor(() => {
26
- expect(fetchTitles).toHaveBeenCalled()
27
- })
25
+ expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 1, sorting: Sorting.Popular })
28
26
  })
29
27
 
30
28
  it('Should render fetched titles after showing skeletons', async () => {
31
- vi.mocked(fetchTitles).mockResolvedValueOnce({ results: [title, title], next: 'next', previous: 'previous' })
29
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce([title, title])
32
30
 
33
31
  const { getAllByTestId } = render(Participant, { participant: participants[0] })
34
32
 
35
- expect(getAllByTestId('skeleton')).toHaveLength(24)
33
+ expect(getAllByTestId('skeleton')).toHaveLength(6)
36
34
 
37
35
  await waitFor(() => {
38
36
  expect(getAllByTestId('title')).toHaveLength(2)
@@ -40,7 +38,7 @@ describe('Participant.svelte', () => {
40
38
  })
41
39
 
42
40
  it('Should not show load more button while loading and when fewer than page size of titles are fetched', async () => {
43
- vi.mocked(fetchTitles).mockResolvedValueOnce({ results: [title, title], next: 'next', previous: 'previous' })
41
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce([title, title])
44
42
 
45
43
  const { queryByText, getAllByTestId } = render(Participant, { participant: participants[0] })
46
44
 
@@ -54,8 +52,8 @@ describe('Participant.svelte', () => {
54
52
  })
55
53
 
56
54
  it('Should show load more button after loading and if more than page size of titles are fetched', async () => {
57
- const resolved = ({ results: Array(30).fill(title), next: 'next', previous: 'previous' })
58
- vi.mocked(fetchTitles).mockResolvedValueOnce(resolved)
55
+ const resolved = Array(30).fill(title)
56
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce(resolved)
59
57
 
60
58
  const { getByText } = render(Participant, { participant: participants[0] })
61
59
 
@@ -65,8 +63,8 @@ describe('Participant.svelte', () => {
65
63
  })
66
64
 
67
65
  it('Should fetch more titles when load more button is clicked', async () => {
68
- const resolved = ({ results: Array(30).fill(title), next: 'next', previous: 'previous' })
69
- vi.mocked(fetchTitles).mockResolvedValueOnce(resolved)
66
+ const resolved = Array(30).fill(title)
67
+ vi.mocked(fetchTitlesForParticipant).mockResolvedValueOnce(resolved)
70
68
 
71
69
  const { getByText } = render(Participant, { participant: participants[0] })
72
70
 
@@ -74,34 +72,7 @@ describe('Participant.svelte', () => {
74
72
 
75
73
  await fireEvent.click(getByText('Show more'))
76
74
 
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)
83
-
84
- const { getByText } = render(Participant, { participant: participants[0] })
85
-
86
- await fireEvent.click(getByText('Actor'))
87
-
88
- await waitFor(() => {
89
- expect(fetchTitles).toHaveBeenCalledWith(expect.objectContaining({ participant_job: 'actor' }))
90
- })
91
- })
92
-
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)
96
-
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
- })
75
+ expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 2, sorting: Sorting.Popular })
105
76
  })
106
77
 
107
78
  it('Should render as small variant when given', () => {
@@ -122,20 +93,11 @@ describe('Participant.svelte', () => {
122
93
  })
123
94
 
124
95
  it('Should not render image if not given', async () => {
96
+
125
97
  const { queryByRole } = render(Participant, { participant: participants[0] })
126
98
 
127
99
  await waitFor(() => {
128
100
  expect(queryByRole('presentation')).not.toBeTruthy()
129
101
  })
130
102
  })
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
- })
141
103
  })
@@ -2,7 +2,6 @@ import path from 'path'
2
2
  import * as dotenv from 'dotenv'
3
3
  import { defineConfig } from 'vitest/config'
4
4
  import { svelte } from '@sveltejs/vite-plugin-svelte'
5
- import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
6
5
  import packageJson from './package.json'
7
6
 
8
7
  dotenv.config({ path: '.env' })
@@ -13,7 +12,6 @@ export default defineConfig(({ mode }) => {
13
12
  return {
14
13
  plugins: [
15
14
  svelte(),
16
- cssInjectedByJsPlugin(),
17
15
  injectEnvVariables,
18
16
  ],
19
17
 
@@ -22,9 +20,9 @@ export default defineConfig(({ mode }) => {
22
20
  rollupOptions: {
23
21
  input: './src/mount.ts',
24
22
  output: {
25
- format: 'iife',
26
23
  name: 'PlayPilotMount',
27
24
  entryFileNames: isEditiorial ? 'editorial.mount.js' : 'mount.js',
25
+ assetFileNames: (assetInfo) => assetInfo.name,
28
26
  },
29
27
  },
30
28
  },