@playpilot/tpi 8.35.0-beta.2 → 8.35.2

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,67 @@
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 },
32
+ { label: 'IMDb Rating', value: Sorting.IMDb },
30
33
  { label: 'Year', value: Sorting.Chronological },
31
34
  ]
32
35
 
33
- let currentJob: 'all' | Job = $state('all')
34
- let descriptionExpanded: boolean = $state(false)
36
+ const pageSize = 30
37
+
38
+ let titles: TitleData[] = $state([])
39
+ let page = $state(1)
40
+ let hasMorePages = $state(true)
41
+ let loading = $state(true)
42
+ let currentSorting = $state(sortings[0])
35
43
 
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
- })
44
+ onMount(loadMore)
41
45
 
42
46
  if (isRetargetingAllowed()) trackViaPixel(MetaEvent.ParticipantView, { participant: participant.name })
43
47
 
44
- function onTitleClick(event: MouseEvent, titles: TitleData[], index: number): void {
45
- openModal({ event, type: 'titles-rail', data: titles, props: { initialIndex: index } })
48
+ function formatDate(dateString: string): string {
49
+ const date = new Date(dateString)
50
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
51
+ }
52
+
53
+ async function loadMore(): Promise<void> {
54
+ loading = true
55
+
56
+ try {
57
+ const results = await fetchTitlesForParticipant(participant, { page, sorting: currentSorting.value })
58
+
59
+ titles = [...titles, ...results]
60
+ hasMorePages = results?.length >= pageSize
61
+ } catch {
62
+ hasMorePages = false
63
+ } finally {
64
+ loading = false
65
+ page += 1
66
+ }
67
+ }
68
+
69
+ function setSorting(sorting: TitleSorting): void {
70
+ currentSorting = sorting
71
+ titles = []
72
+ page = 1
73
+
74
+ loadMore()
46
75
  }
47
76
  </script>
48
77
 
@@ -52,46 +81,60 @@
52
81
  <div>
53
82
  <div class="heading" use:heading={2} id="heading">{name}</div>
54
83
 
55
- {#if description}
56
- <p class="description" class:expanded={descriptionExpanded}>
57
- {description}
84
+ {#if birth_date}
85
+ <p class="dates">
86
+ {t('Born On')} <strong>{formatDate(birth_date)}</strong>{#if death_date}, {t('Died On')} <strong>{formatDate(death_date)}</strong>{/if}
58
87
  </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
88
  {/if}
66
89
  </div>
67
90
  </div>
68
91
 
69
- <div class="content" class:small>
70
- {#if !small}
92
+ <div class="content">
93
+ {#if !small && (titles.length && !loading)}
71
94
  <div class="list-header">
72
95
  <div class="heading subheading" use:heading={3} id="credits">{t('Credits')}</div>
73
- </div>
74
96
 
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}
97
+ <div class="sort">
98
+ {t('Sort By')}:
99
+
100
+ <Dropdown>
101
+ {#snippet button({ toggle })}
102
+ <Button variant="border" onclick={toggle}>{t(currentSorting.label)}</Button>
103
+ {/snippet}
104
+
105
+ {#snippet content()}
106
+ <div class="sortings">
107
+ {#each sortings as sorting}
108
+ <Button variant="link" onclick={() => setSorting(sorting)}>{t(sorting.label)}</Button>
109
+ {/each}
110
+ </div>
111
+ {/snippet}
112
+ </Dropdown>
113
+ </div>
81
114
  </div>
82
-
83
- <Filter {filter} {sortings} hideActive />
84
115
  {/if}
85
116
 
86
- <ExploreResults forceGrid hidePlaylinks filter={filterWithParticipant} {onTitleClick} />
117
+ <div class="list">
118
+ {#each titles as title}
119
+ <ListTitle {title} compact={small} onclick={(event) => openModal({ event, data: title })} />
120
+ {/each}
121
+
122
+ {#if loading}
123
+ {#each { length: 6 }}
124
+ <div class="skeleton" data-testid="skeleton"></div>
125
+ {/each}
126
+ {:else if hasMorePages}
127
+ <button class="more" onclick={loadMore}>{t('Show More')}</button>
128
+ {/if}
129
+ </div>
87
130
  </div>
88
131
 
89
132
  <style lang="scss">
90
133
  .header {
91
- --participant-image-size: #{margin(5)};
134
+ --participant-image-size: #{margin(6)};
92
135
  display: grid;
93
- grid-template-columns: var(--participant-image-size) calc(100% - var(--participant-image-size) - margin(1));
94
- align-items: start;
136
+ grid-template-columns: calc(var(--participant-image-size)) auto;
137
+ align-items: center;
95
138
  gap: margin(1);
96
139
  padding: margin(3) margin(1) margin(2);
97
140
  font-family: theme(detail-font-family, font-family);
@@ -102,13 +145,11 @@
102
145
 
103
146
  @include desktop {
104
147
  --participant-image-size: #{margin(8)};
105
- padding: margin(3) margin(2) margin(2);
106
148
  }
107
149
 
108
150
  &.small {
109
151
  --participant-image-size: #{margin(6)};
110
- padding: margin(1) margin(1) 0;
111
- align-items: center;
152
+ padding: margin(1);
112
153
  }
113
154
  }
114
155
 
@@ -121,14 +162,19 @@
121
162
  font-style: theme(detail-title-font-style, normal);
122
163
 
123
164
  &.subheading {
165
+ margin: 0;
124
166
  font-size: theme(detail-title-small-font-size, margin(1.25));
125
167
  }
126
168
 
127
169
  .small & {
128
- font-size: 1.25em;
170
+ font-size: 1em;
129
171
  }
130
172
  }
131
173
 
174
+ .dates {
175
+ margin: margin(0.5) 0 0;
176
+ }
177
+
132
178
  .content {
133
179
  padding: 0 margin(1) margin(1);
134
180
  color: theme(detail-text-color, text-color);
@@ -138,38 +184,16 @@
138
184
  line-height: normal;
139
185
  font-style: normal;
140
186
 
141
- @include desktop {
142
- padding: 0 margin(2) margin(2);
143
-
144
- &.small {
145
- padding: 0 margin(1) margin(1);
146
- }
147
- }
148
-
149
187
  ::view-transition-old(content),
150
188
  ::view-transition-new(content) {
151
189
  height: 100%;
152
190
  }
153
191
  }
154
192
 
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
- }
193
+ .list {
194
+ display: flex;
195
+ flex-direction: column;
196
+ gap: margin(0.5);
173
197
  }
174
198
 
175
199
  .list-header {
@@ -179,10 +203,47 @@
179
203
  margin-bottom: margin(0.5);
180
204
  }
181
205
 
182
- .jobs {
206
+ .skeleton {
207
+ min-height: margin(7.25);
208
+ border-radius: theme(playlink-border-radius, border-radius);
209
+ background: theme(detail-background-light, lighter);
210
+ }
211
+
212
+ .more {
213
+ cursor: pointer;
214
+ appearance: none;
215
+ padding: margin(0.5) margin(1);
216
+ border: 0;
217
+ border-radius: theme(participants-load-more-border-radius, border-radius);
218
+ background: theme(participants-load-more-background, content);
219
+ color: inherit;
220
+ font-size: inherit;
221
+ font-family: inherit;
222
+
223
+ &:hover {
224
+ filter: brightness(1.2);
225
+ }
226
+ }
227
+
228
+ .sort {
183
229
  display: flex;
184
- flex-wrap: wrap;
185
- gap: margin(0.5);
186
- margin-bottom: margin(0.5);
230
+ align-items: center;
231
+ gap: margin(0.25);
232
+ color: theme(text-color-alt);
233
+ }
234
+
235
+ .sortings {
236
+ :global(.button) {
237
+ display: block;
238
+ width: 100%;
239
+ padding: margin(0.5) margin(1);
240
+ text-align: left;
241
+ white-space: nowrap;
242
+ }
243
+
244
+ :global(.button:hover) {
245
+ background: theme(content-light);
246
+ border-radius: 0;
247
+ }
187
248
  }
188
249
  </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
  },