@playpilot/tpi 6.10.6 → 6.11.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": "6.10.6",
3
+ "version": "6.11.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -101,6 +101,31 @@ export const translations = {
101
101
  [Language.Swedish]: 'Titta nu',
102
102
  [Language.Danish]: 'Se nu',
103
103
  },
104
+ 'Category: AVOD': {
105
+ [Language.English]: 'Stream for free',
106
+ [Language.Swedish]: 'Streama gratis',
107
+ [Language.Danish]: 'Stream gratis',
108
+ },
109
+ 'Category: SVOD': {
110
+ [Language.English]: 'Streaming services',
111
+ [Language.Swedish]: 'Streamingtjänster',
112
+ [Language.Danish]: 'Streamingtjenester',
113
+ },
114
+ 'Category: RENT': {
115
+ [Language.English]: 'Rent it',
116
+ [Language.Swedish]: 'Hyr den',
117
+ [Language.Danish]: 'Lej den',
118
+ },
119
+ 'Category: BUY': {
120
+ [Language.English]: 'Buy it',
121
+ [Language.Swedish]: 'Köp den',
122
+ [Language.Danish]: 'Køb den',
123
+ },
124
+ 'Category: Other': {
125
+ [Language.English]: 'Other',
126
+ [Language.Swedish]: 'Annat',
127
+ [Language.Danish]: 'Andet',
128
+ },
104
129
  'Share': {
105
130
  [Language.English]: 'Share',
106
131
  [Language.Swedish]: 'Dela',
@@ -59,6 +59,11 @@ export type ConfigResponse = {
59
59
  */
60
60
  disable_scroll_reveal_posters?: boolean
61
61
 
62
+ /**
63
+ * Playlinks are merged by default, when this option is enabled playlinks will instead be shown categorized.
64
+ */
65
+ categorize_playlinks?: boolean
66
+
62
67
  /**
63
68
  * The following options are all relevant for in text disclaimers, which renders as a disclaimer text within the article,
64
69
  * rather than only inside of title cards.
@@ -14,4 +14,4 @@ export type PlaylinkData = {
14
14
  }
15
15
  }
16
16
 
17
- export type PlaylinkCategory = 'SVOD' | 'BUY' | 'RENT' | 'TVOD'
17
+ export type PlaylinkCategory = 'SVOD' | 'BUY' | 'RENT' | 'TVOD' | 'AVOD'
@@ -23,6 +23,7 @@
23
23
  const usePixel = $derived(isPixelAllowed() && !highlighted)
24
24
 
25
25
  const categoryStrings = {
26
+ AVOD: t('Stream'),
26
27
  SVOD: t('Stream'),
27
28
  BUY: t('Buy'),
28
29
  RENT: t('Rent'),
@@ -3,13 +3,17 @@
3
3
  import { t } from '$lib/localization'
4
4
  import { mergePlaylinks } from '$lib/playlink'
5
5
  import { track } from '$lib/tracking'
6
- import type { PlaylinkData } from '$lib/types/playlink'
6
+ import type { PlaylinkCategory, PlaylinkData } from '$lib/types/playlink'
7
7
  import type { TitleData } from '$lib/types/title'
8
8
  import { heading } from '$lib/actions/heading'
9
9
  import { campaignToPlaylink, getFirstAdOfType } from '$lib/api/ads'
10
10
  import { getContext } from 'svelte'
11
11
  import Playlink from './Playlink.svelte'
12
12
 
13
+ type Category = PlaylinkCategory | '' | 'Other'
14
+ type CategorizedPlaylinks = Partial<Record<Category, PlaylinkData[]>>
15
+ type SortedPlaylinks = [Category, PlaylinkData[]][]
16
+
13
17
  interface Props {
14
18
  playlinks: PlaylinkData[]
15
19
  title: TitleData
@@ -19,10 +23,13 @@
19
23
 
20
24
  const isModal = getContext('scope') === 'modal'
21
25
  const displayAd = getFirstAdOfType('card')
26
+ const categorize = !!window.PlayPilotLinkInjections?.config?.categorize_playlinks
22
27
 
23
28
  let outerWidth = $state(0)
24
29
 
25
30
  const mergedPlaylinks = $derived(mergePlaylinks(playlinks))
31
+ const categorizedPlaylinks = $derived(categorizePlaylinks(playlinks))
32
+ const shownPlaylinks: SortedPlaylinks = $derived(categorize ? sortCategories(categorizedPlaylinks) : [['' as Category, mergedPlaylinks]])
26
33
 
27
34
  // Grid turns into a list when the playlinks container is small enough
28
35
  // It is also a list by default if a display ad is present, as that would
@@ -32,6 +39,37 @@
32
39
  function onclick(playlink: string): void {
33
40
  track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.TitlePopoverPlaylinkClick, title, { playlink })
34
41
  }
42
+
43
+ function categorizePlaylinks(playlinks: PlaylinkData[]): CategorizedPlaylinks {
44
+ const categories: CategorizedPlaylinks = {}
45
+
46
+ for (const playlink of playlinks) {
47
+ const { category } = playlink.extra_info
48
+
49
+ if (!(category in categories)) categories[category] = []
50
+ categories[category]!.push(playlink)
51
+ }
52
+
53
+ return categories
54
+ }
55
+
56
+ function sortCategories(categorizedPlaylinks: CategorizedPlaylinks): SortedPlaylinks {
57
+ const order: Partial<PlaylinkCategory>[] = ['AVOD', 'SVOD', 'RENT', 'BUY']
58
+ const sortedCategories: SortedPlaylinks = []
59
+
60
+ for (const category of order) {
61
+ if (categorizedPlaylinks[category]) sortedCategories.push([category, categorizedPlaylinks[category]])
62
+ }
63
+
64
+ const restPlaylinks: PlaylinkData[] = []
65
+ for (const playlinks of Object.values(categorizedPlaylinks)) {
66
+ for (const playlink of playlinks) {
67
+ if (!order.includes(playlink.extra_info.category)) restPlaylinks.push(playlink)
68
+ }
69
+ }
70
+
71
+ return [...sortedCategories, ['Other', restPlaylinks]]
72
+ }
35
73
  </script>
36
74
 
37
75
 
@@ -42,24 +80,28 @@
42
80
  {window?.PlayPilotLinkInjections?.config?.playlinks_disclaimer_text || t('Commission Disclaimer')}
43
81
  <a href="https://playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>
44
82
  </div>
83
+ {:else}
84
+ <div class="empty" data-testid="playlinks-empty">
85
+ {t('Title Unavailable')}
86
+ </div>
45
87
  {/if}
46
88
 
47
- <div class="playlinks" class:list bind:clientWidth={outerWidth}>
48
- {#each mergedPlaylinks as playlink, index}
49
- <Playlink {playlink} onclick={() => onclick(playlink.name)} />
89
+ {#each shownPlaylinks as [category, playlinks]}
90
+ {#if category && playlinks.length}
91
+ <div class="heading category" use:heading={4}>{t(`Category: ${category}`)}</div>
92
+ {/if}
50
93
 
51
- <!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
52
- {#if displayAd && (index === 0)}
53
- <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
54
- {/if}
55
- {/each}
94
+ <div class="playlinks" data-testid="category-{category}" class:list bind:clientWidth={outerWidth}>
95
+ {#each playlinks as playlink, index}
96
+ <Playlink {playlink} onclick={() => onclick(playlink.name)} />
56
97
 
57
- {#if !mergedPlaylinks.length}
58
- <div class="empty" data-testid="playlinks-empty">
59
- {t('Title Unavailable')}
60
- </div>
61
- {/if}
62
- </div>
98
+ <!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
99
+ {#if displayAd && (index === 0)}
100
+ <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
101
+ {/if}
102
+ {/each}
103
+ </div>
104
+ {/each}
63
105
 
64
106
  <style lang="scss">
65
107
  .heading {
@@ -71,6 +113,11 @@
71
113
  line-height: normal;
72
114
  }
73
115
 
116
+ .category {
117
+ margin: margin(0.5) 0 margin(0.25);
118
+ font-size: theme(playlinks-category-font-size, font-size-small);
119
+ }
120
+
74
121
  .playlinks {
75
122
  box-sizing: border-box;
76
123
  display: grid;
@@ -105,7 +152,6 @@
105
152
  }
106
153
 
107
154
  .empty {
108
- grid-column: span 2;
109
155
  padding: margin(0.75);
110
156
  margin-top: margin(0.5);
111
157
  background: theme(playlink-background, lighter);
@@ -113,9 +159,5 @@
113
159
  border-radius: theme(playlink-border-radius, border-radius);
114
160
  white-space: initial;
115
161
  line-height: 1.35;
116
-
117
- .list & {
118
- grid-column: 1;
119
- }
120
162
  }
121
163
  </style>
@@ -112,7 +112,7 @@ describe('Playlinks.svelte', () => {
112
112
  expect(getAllByText('Some playlink')).toHaveLength(1)
113
113
  })
114
114
 
115
- it('Should render as list when only one playlink is present', async () => {
115
+ it('Should render as list when only one playlink is present', () => {
116
116
  const playlinks = [
117
117
  { name: 'Some playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
118
118
  ]
@@ -122,4 +122,55 @@ describe('Playlinks.svelte', () => {
122
122
  // @ts-ignore
123
123
  expect(container.querySelector('.playlinks').classList).toContain('list')
124
124
  })
125
+
126
+ it('Should categorize and sort playlinks if config option is given', () => {
127
+ // @ts-ignore
128
+ window.PlayPilotLinkInjections = {
129
+ config: { categorize_playlinks: true },
130
+ }
131
+
132
+ const playlinks = [
133
+ { name: 'Some svod playlink', logo_url: 'logo', extra_info: { category: 'SVOD' } },
134
+ { name: 'Some rent playlink', extra_info: { category: 'RENT' } },
135
+ { name: 'Some other rent playlink', logo_url: '', extra_info: { category: 'RENT' } },
136
+ { name: 'Some free playlink', logo_url: null, extra_info: { category: 'AVOD' } },
137
+ ]
138
+
139
+ // @ts-ignore
140
+ const { container, getByText } = render(Playlinks, { playlinks, title })
141
+
142
+ const elements = /** @type {HTMLElement[]} */ (Array.from(container.querySelectorAll('[data-playlink]')))
143
+
144
+ expect(elements[0].innerText).toContain(playlinks[3].name)
145
+ expect(elements[1].innerText).toContain(playlinks[0].name)
146
+ expect(elements[2].innerText).toContain(playlinks[1].name)
147
+ expect(elements[3].innerText).toContain(playlinks[2].name)
148
+
149
+ expect(getByText('Streaming services')).toBeTruthy()
150
+ expect(getByText('Rent it')).toBeTruthy()
151
+ expect(getByText('Stream for free')).toBeTruthy()
152
+ })
153
+
154
+ it('Should list uncategorized playlinks under "other" category', () => {
155
+ // @ts-ignore
156
+ window.PlayPilotLinkInjections = {
157
+ config: { categorize_playlinks: true },
158
+ }
159
+
160
+ const playlinks = [
161
+ { name: 'Some udef playlink', logo_url: 'logo', extra_info: { category: 'UDEF' } },
162
+ { name: 'Some empty playlink', extra_info: { category: '' } },
163
+ { name: 'Some svod playlink', logo_url: '', extra_info: { category: 'SVOD' } },
164
+ { name: 'Some tvod playlink', logo_url: null, extra_info: { category: 'TVOD' } },
165
+ ]
166
+
167
+ // @ts-ignore
168
+ const { getByTestId, getByText } = render(Playlinks, { playlinks, title })
169
+
170
+ expect(getByText('Other')).toBeTruthy()
171
+ expect(getByTestId('category-Other').querySelectorAll('[data-playlink]')).toHaveLength(3)
172
+
173
+ expect(getByText('Streaming services'))
174
+ expect(getByTestId('category-SVOD').querySelectorAll('[data-playlink]')).toHaveLength(1)
175
+ })
125
176
  })