@playpilot/tpi 8.25.0-beta.1 → 8.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.25.0-beta.1",
3
+ "version": "8.25.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -9,20 +9,22 @@
9
9
  import { getLanguage } from '$lib/language'
10
10
  import type { LanguageCode } from '$lib/types/language'
11
11
  import { api } from '$lib/api/api'
12
+ import InfiniteTitlesRail from '../../Rails/InfiniteTitlesRail.svelte'
13
+ import type { APIPaginatedResult } from '$lib/types/api'
14
+
15
+ type Rail = { headings: Record<LanguageCode, string>, params: Record<string, any>, properties: Record<string, any> }
12
16
 
13
17
  let expandedTitle: TitleData | null = $state(null)
14
18
  let expandedRailKey: string | null = $state(null)
15
19
  let windowWidth: number = $state(window.innerWidth)
16
20
 
17
- type Rail = { headings: Record<LanguageCode, string>, params: Record<string, any>, properties: Record<string, any> }
18
-
19
21
  async function fetchRails(): Promise<Rail[]> {
20
22
  return await api<Rail[]>('/streaming-guide/rails', { baseUrl: 'https://partner.playpilot.net/api' })
21
23
  }
22
24
 
23
- async function getListTitles(params: Record<string, any> = {}): Promise<TitleData[]> {
25
+ async function getListTitles(params: Record<string, any> = {}): Promise<APIPaginatedResult<TitleData>> {
24
26
  try {
25
- return (await fetchTitles(params))?.results
27
+ return (await fetchTitles({ ...params, page_size: 20 }))
26
28
  } catch (error: any) {
27
29
  console.log(error)
28
30
  track(TrackingEvent.ExploreHomeRailError, null, { message: error.message || error.status.toString() || '', params })
@@ -38,7 +40,7 @@
38
40
  {#await fetchRails()}
39
41
  {#each { length: 3 }}
40
42
  <div class="rail">
41
- <TitlesRail titles={new Promise((res) => setTimeout(res, 10000))} size={windowWidth >= mobileBreakpoint ? 'flexible' : 'huge'} />
43
+ <TitlesRail titles={new Promise(() => {})} size={windowWidth >= mobileBreakpoint ? 'flexible' : 'huge'} />
42
44
  </div>
43
45
  {/each}
44
46
  {:then rails}
@@ -46,16 +48,16 @@
46
48
  {@const heading = headings[getLanguage()]}
47
49
 
48
50
  <div class="rail">
49
- <TitlesRail
51
+ <InfiniteTitlesRail
50
52
  {heading}
51
- titles={getListTitles(params)}
52
- size={windowWidth >= mobileBreakpoint ? (properties.size || 'flexible') : 'huge'}
53
- minimumLength={7}
54
53
  {...properties}
55
54
  bind:expandedTitle
56
55
  bind:expandedRailKey
57
56
  onclick={() => track(TrackingEvent.ExploreTitleRailModalView, null, { rail: headings })}
58
- onexpand={(title) => track(TrackingEvent.ExploreTitleRailExpandClick, title, { rail: headings })} />
57
+ onexpand={(title) => track(TrackingEvent.ExploreTitleRailExpandClick, title, { rail: headings })}
58
+ fetchFunction={(page) => getListTitles({ ...params, page })}
59
+ size={windowWidth >= mobileBreakpoint ? (properties.size || 'flexible') : 'huge'}
60
+ minimumLength={7} />
59
61
  </div>
60
62
  {/each}
61
63
  {:catch}
@@ -10,7 +10,7 @@
10
10
  import { trackViaPixel } from '@playpilot/retargeting-tracking'
11
11
  import Popover from './Popover.svelte'
12
12
  import Title from './Title.svelte'
13
- import Participant from './Participants/Participant.svelte'
13
+ import Participant from './Participant.svelte'
14
14
  import TopScroll from './Ads/TopScroll.svelte'
15
15
  import Display from './Ads/Display.svelte'
16
16
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  <script lang="ts">
3
3
  import Modal from './Modal.svelte'
4
- import Participant from '../Participants/Participant.svelte'
4
+ import Participant from '../Participant.svelte'
5
5
  import type { ParticipantData } from '$lib/types/participant'
6
6
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
7
7
  import { track } from '$lib/tracking'
@@ -5,12 +5,11 @@
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 ListTitle from './ListTitle.svelte'
8
9
  import { t } from '$lib/localization'
9
10
  import { hasConsentedTo } from '$lib/consent'
10
11
  import { trackViaPixel } from '@playpilot/retargeting-tracking'
11
12
  import { MetaEvent } from '$lib/enums/TrackingEvent'
12
- import ParticipantImage from './ParticipantImage.svelte'
13
- import ListTitle from '../ListTitle.svelte'
14
13
 
15
14
  interface Props {
16
15
  participant: ParticipantData
@@ -55,17 +54,13 @@
55
54
  </script>
56
55
 
57
56
  <div class="header" class:small>
58
- <ParticipantImage {participant} />
57
+ <div class="heading" use:heading={2} id="heading">{name}</div>
59
58
 
60
- <div>
61
- <div class="heading" use:heading={2} id="heading">{name}</div>
62
-
63
- {#if birth_date}
64
- <p class="dates">
65
- {t('Born On')} <strong>{formatDate(birth_date)}</strong>{#if death_date}, {t('Died On')} <strong>{formatDate(death_date)}</strong>{/if}
66
- </p>
67
- {/if}
68
- </div>
59
+ {#if birth_date}
60
+ <p class="dates">
61
+ {t('Born On')} <strong>{formatDate(birth_date)}</strong>{#if death_date}, {t('Died On')} <strong>{formatDate(death_date)}</strong>{/if}
62
+ </p>
63
+ {/if}
69
64
  </div>
70
65
 
71
66
  <div class="content">
@@ -90,22 +85,13 @@
90
85
 
91
86
  <style lang="scss">
92
87
  .header {
93
- --participant-image-size: #{margin(6)};
94
- display: grid;
95
- grid-template-columns: calc(var(--participant-image-size)) auto;
96
- align-items: center;
97
- gap: margin(1);
98
- padding: margin(3) margin(1) margin(2);
88
+ padding: margin(4) margin(1) margin(2);
99
89
  font-family: theme(detail-font-family, font-family);
100
90
  font-weight: theme(detail-font-weight, normal);
101
91
  font-size: theme(detail-font-size, font-size-base);
102
92
  line-height: theme(participant-description-line-height, normal);
103
93
  color: theme(detail-text-color, text-color);
104
94
 
105
- @include desktop {
106
- --participant-image-size: #{margin(8)};
107
- }
108
-
109
95
  &.small {
110
96
  padding: margin(1);
111
97
  }
@@ -115,7 +101,7 @@
115
101
  margin: 0;
116
102
  font-family: theme(detail-title-font-family, font-family);
117
103
  font-weight: theme(detail-title-font-weight, lighter);
118
- font-size: clamp(1.25em, 5vw, 1.5em);
104
+ font-size: theme(detail-title-font-size, margin(1.5));
119
105
  line-height: normal;
120
106
  font-style: theme(detail-title-font-style, normal);
121
107
 
@@ -123,10 +109,6 @@
123
109
  margin: 0 0 margin(0.5);
124
110
  font-size: theme(detail-title-small-font-size, margin(1.25));
125
111
  }
126
-
127
- .small & {
128
- font-size: 1em;
129
- }
130
112
  }
131
113
 
132
114
  .dates {
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import type { APIPaginatedResult } from '$lib/types/api'
3
+ import type { TitleData } from '$lib/types/title'
4
+ import TitlesRail from './TitlesRail.svelte'
5
+ import { onMount, type ComponentProps } from 'svelte'
6
+
7
+ interface Props extends Omit<ComponentProps<typeof TitlesRail>, 'titles'> {
8
+ fetchFunction: (page: number) => Promise<APIPaginatedResult<TitleData>>
9
+ maxPage?: number
10
+ }
11
+
12
+ const {
13
+ fetchFunction,
14
+ maxPage = 5,
15
+ expandedTitle = $bindable(null),
16
+ expandedRailKey = $bindable(null),
17
+ ...rest
18
+ }: Props = $props()
19
+
20
+ const emptyPromise: Promise<TitleData[]> = new Promise(() => {})
21
+
22
+ let titles: TitleData[] = $state([])
23
+ let shown: number[] = $state([])
24
+ let page = $state(1)
25
+ let hasMorePages = $state(false)
26
+ let loading = $state(true)
27
+
28
+ $effect(() => {
29
+ if (loading) return
30
+ if (!hasMorePages) return
31
+ if (page >= maxPage) return
32
+ if (titles.length - shown.length > 5) return
33
+
34
+ page++
35
+ fetchTitles()
36
+ })
37
+
38
+ onMount(fetchTitles)
39
+
40
+ async function fetchTitles(): Promise<void> {
41
+ loading = true
42
+ hasMorePages = false
43
+
44
+ try {
45
+ const { next, results } = await fetchFunction(page)
46
+
47
+ titles = [...titles, ...results]
48
+ hasMorePages = !!next
49
+ } finally {
50
+ loading = false
51
+ }
52
+ }
53
+ </script>
54
+
55
+ {#if !titles.length && loading}
56
+ <TitlesRail titles={emptyPromise} {...rest} />
57
+ {:else}
58
+ <TitlesRail {titles} {...rest} bind:shown />
59
+ {/if}
@@ -8,7 +8,6 @@
8
8
  import { track } from '$lib/tracking'
9
9
  import type { ParticipantData } from '$lib/types/participant'
10
10
  import type { TitleData } from '$lib/types/title'
11
- import ParticipantImage from '../Participants/ParticipantImage.svelte'
12
11
  import Rail from './Rail.svelte'
13
12
 
14
13
  interface Props {
@@ -36,6 +35,8 @@
36
35
  participantsByOccurrence[participant.sid] = { participant, count }
37
36
  }
38
37
 
38
+ // Sort all participants by their occurrences. If a participant did not occur their positioned will be retained
39
+ // relative to where it was before
39
40
  const participantsSortedByCount = Object.values(participantsByOccurrence).sort((a, b) => b.count - a.count)
40
41
 
41
42
  return participantsSortedByCount.map(({ participant }) => participant)
@@ -50,10 +51,7 @@
50
51
  {#await fetchParticipantsForTitle(title)}
51
52
  <Rail {heading}>
52
53
  {#each { length: 5 }}
53
- <div class="participant">
54
- <ParticipantImage />
55
- <div class="name"></div>
56
- </div>
54
+ <div class="participant"></div>
57
55
  {/each}
58
56
  </Rail>
59
57
  {:then participants}
@@ -61,9 +59,11 @@
61
59
  <Rail {heading}>
62
60
  {#each orderParticipantsByPageTextOccurrence(participants).slice(0, 15) as participant}
63
61
  <button class="participant" data-testid="participant" onclick={event => onclick(event, participant)}>
64
- <ParticipantImage {participant} />
62
+ <span class="truncate">{participant.name}</span>
65
63
 
66
- <div class="name">{participant.name}</div>
64
+ {#if participant.character}
65
+ <div class="character truncate">{participant.character}</div>
66
+ {/if}
67
67
  </button>
68
68
  {/each}
69
69
  </Rail>
@@ -71,25 +71,22 @@
71
71
  {/await}
72
72
 
73
73
  <style lang="scss">
74
- $item-width: #{theme(rail-size-default, margin(6))};
75
-
76
74
  .participant {
77
- --participant-image-size: #{$item-width};
78
75
  display: flex;
79
76
  flex-direction: column;
80
- flex: 0 0 $item-width;
81
- gap: margin(0.5);
82
- width: $item-width;
83
- padding: 0;
77
+ flex: 0 0 10rem;
78
+ width: 10rem;
79
+ min-height: margin(3.375); // Matches 54 pixels, the height of a card with both name and character
80
+ padding: margin(0.5);
81
+ border: theme(cast-border, 0);
84
82
  border-radius: theme(cast-border-radius, border-radius);
85
- border: 0;
86
- background: transparent;
83
+ background: theme(cast-background, lighter);
87
84
  cursor: pointer;
88
- color: inherit;
89
85
  font-family: inherit;
86
+ text-align: left;
87
+ color: inherit;
90
88
  font-size: theme(cast-font-size, font-size-small);
91
89
  line-height: normal;
92
- text-align: center;
93
90
  white-space: nowrap;
94
91
 
95
92
  &:is(button):hover,
@@ -99,7 +96,12 @@
99
96
  }
100
97
  }
101
98
 
102
- .name {
99
+ .character {
100
+ color: theme(cast-character-text-color, text-color-alt);
101
+ font-style: italic;
102
+ }
103
+
104
+ .truncate {
103
105
  overflow: hidden;
104
106
  text-overflow: ellipsis;
105
107
  white-space: nowrap;
@@ -9,6 +9,7 @@
9
9
  onchange?: (index: number) => void
10
10
  onresize?: () => void
11
11
  slider?: ReturnType<typeof TinySlider> | null,
12
+ shown?: number[],
12
13
  children: Snippet
13
14
  }
14
15
 
@@ -17,6 +18,7 @@
17
18
  onchange = () => null,
18
19
  onresize = () => null,
19
20
  slider = $bindable(),
21
+ shown = $bindable([]),
20
22
  children,
21
23
  }: Props = $props()
22
24
  </script>
@@ -26,7 +28,7 @@
26
28
  {/if}
27
29
 
28
30
  <div class="rail">
29
- <TinySlider bind:this={slider} {onchange} {onresize} allowWheel>
31
+ <TinySlider bind:this={slider} {onchange} {onresize} bind:shown allowWheel>
30
32
  {@render children()}
31
33
 
32
34
  {#snippet controls({ setIndex, currentIndex, reachedEnd })}
@@ -18,10 +18,11 @@
18
18
  heading?: string,
19
19
  size?: 'small' | 'large' | 'huge' | 'flexible'
20
20
  aside?: boolean,
21
- expandable?: boolean,
22
21
  navigateToExplore?: boolean,
22
+ expandable?: boolean,
23
23
  expandedTitle?: TitleData | null,
24
24
  expandedRailKey?: string | null,
25
+ shown?: number[],
25
26
  onclick?: (title: TitleData) => void,
26
27
  onexpand?: (title: TitleData) => void
27
28
  }
@@ -32,10 +33,11 @@
32
33
  heading = '',
33
34
  size = 'small',
34
35
  aside = false,
35
- expandable = false,
36
36
  navigateToExplore = false,
37
+ expandable = false,
37
38
  expandedTitle = $bindable(null),
38
39
  expandedRailKey = $bindable(null),
40
+ shown = $bindable([]),
39
41
  onclick = () => null,
40
42
  onexpand = () => null,
41
43
  }: Props = $props()
@@ -169,6 +171,7 @@
169
171
  {#if titles?.length >= minimumLength}
170
172
  <Rail
171
173
  bind:slider
174
+ bind:shown
172
175
  {heading}
173
176
  onchange={(index) => onchange(titles, index)}
174
177
  onresize={() => slider?.reposition()}>
@@ -3,7 +3,7 @@
3
3
  import { title, linkInjections, campaign, participants } from '$lib/fakeData'
4
4
  import { openModal } from '$lib/modal'
5
5
  import { insertExplore, insertExploreIntoNavigation } from '$lib/explore'
6
- import { fetchSimilarTitles } from '$lib/api/titles'
6
+ import { fetchSimilarTitles, fetchTitles } from '$lib/api/titles'
7
7
  import Disclaimer from '../components/Ads/Disclaimer.svelte'
8
8
  import Display from '../components/Ads/Display.svelte'
9
9
  import TopScroll from '../components/Ads/TopScroll.svelte'
@@ -21,6 +21,7 @@
21
21
  import ExploreRouter from '../components/Explore/ExploreRouter.svelte'
22
22
  import TitlesRail from '../components/Rails/TitlesRail.svelte'
23
23
  import ParticipantsRail from '../components/Rails/ParticipantsRail.svelte'
24
+ import InfiniteTitlesRail from '../components/Rails/InfiniteTitlesRail.svelte'
24
25
 
25
26
  if (browser) {
26
27
  // @ts-ignore
@@ -151,6 +152,10 @@
151
152
  <TitlesRail titles={fetchSimilarTitles(title)} expandable />
152
153
  <br>
153
154
  <TitlesRail titles={fetchSimilarTitles(title)} aside size="large" />
155
+ <br>
156
+ Loads more pages:
157
+ <br>
158
+ <InfiniteTitlesRail fetchFunction={(page: number) => fetchTitles({ page })} />
154
159
  {/key}
155
160
 
156
161
  <h2>TitlesRailModal</h2>
@@ -1,7 +1,7 @@
1
1
  import { fireEvent, render, waitFor } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi, beforeEach } from 'vitest'
3
3
 
4
- import Participant from '../../../../routes/components/Participants/Participant.svelte'
4
+ import Participant from '../../../routes/components/Participant.svelte'
5
5
  import { participants, title } from '$lib/fakeData'
6
6
  import { fetchTitlesForParticipant } from '$lib/api/participants'
7
7
 
@@ -80,23 +80,4 @@ describe('Participant.svelte', () => {
80
80
  expect(queryByText('Credits')).not.toBeTruthy()
81
81
  expect(container.querySelector('.small')).toBeTruthy()
82
82
  })
83
-
84
- it('Should render image if given', async () => {
85
- const participantWithImage = { ...participants[0], image: 'some-image.jpg' }
86
-
87
- const { getByRole } = render(Participant, { participant: participantWithImage })
88
-
89
- await waitFor(() => {
90
- expect(getByRole('presentation')).toBeTruthy()
91
- })
92
- })
93
-
94
- it('Should not render image if not given', async () => {
95
-
96
- const { queryByRole } = render(Participant, { participant: participants[0] })
97
-
98
- await waitFor(() => {
99
- expect(queryByRole('presentation')).not.toBeTruthy()
100
- })
101
- })
102
83
  })
@@ -36,19 +36,6 @@ describe('ParticipantsRail.svelte', () => {
36
36
  })
37
37
  })
38
38
 
39
- it('Should render image only for participants with image', async () => {
40
- const participantsWithImage = [...participants]
41
- participantsWithImage[0].image = 'some-image.jpg'
42
-
43
- vi.mocked(fetchParticipantsForTitle).mockResolvedValueOnce(participantsWithImage)
44
-
45
- const { getAllByRole } = render(ParticipantsRail, { title })
46
-
47
- await waitFor(() => {
48
- expect(getAllByRole('presentation')).toHaveLength(1)
49
- })
50
- })
51
-
52
39
  it('Should order participants by their number of occurrences on the page', async () => {
53
40
  document.body.innerHTML = `<main>${participants[1].name}. ${participants[2].name}, ${participants[2].name}</main>`
54
41
  vi.mocked(fetchParticipantsForTitle).mockResolvedValueOnce(participants)
@@ -1,56 +0,0 @@
1
- <script lang="ts">
2
- import type { ParticipantData } from '$lib/types/participant'
3
-
4
- interface Props {
5
- participant?: ParticipantData | null
6
- }
7
-
8
- const { participant = null }: Props = $props()
9
- </script>
10
-
11
- <div class="image">
12
- {#if participant?.image}
13
- <img loading="lazy" src={participant.image} alt="" />
14
- {/if}
15
- </div>
16
-
17
- <style lang="scss">
18
- $item-width: #{var(--participant-image-size, margin(6))};
19
-
20
- .image {
21
- position: relative;
22
- display: block;
23
- width: $item-width;
24
- height: $item-width;
25
- border-radius: 50%;
26
- overflow: hidden;
27
- background: theme(cast-background, lighter);
28
-
29
- &::before,
30
- &::after {
31
- content: "";
32
- display: block;
33
- position: absolute;
34
- width: 50%;
35
- height: 50%;
36
- top: 50%;
37
- left: 50%;
38
- border-radius: 50%;
39
- transform: translateX(-50%) translateY(-50%);
40
- background: currentColor;
41
- opacity: 0.1;
42
- }
43
-
44
- &::after {
45
- top: 100%;
46
- width: 75%;
47
- }
48
-
49
- img {
50
- z-index: 1;
51
- position: relative;
52
- width: 100%;
53
- height: auto;
54
- }
55
- }
56
- </style>