@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/dist/link-injections.js +9 -9
- package/package.json +1 -1
- package/src/lib/data/translations.ts +25 -0
- package/src/lib/types/config.d.ts +5 -0
- package/src/lib/types/playlink.d.ts +1 -1
- package/src/routes/components/Playlinks/Playlink.svelte +1 -0
- package/src/routes/components/Playlinks/Playlinks.svelte +62 -20
- package/src/tests/routes/components/Playlinks/Playlinks.test.js +52 -1
package/package.json
CHANGED
|
@@ -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.
|
|
@@ -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
|
-
|
|
48
|
-
{#
|
|
49
|
-
<
|
|
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
|
-
|
|
52
|
-
{#
|
|
53
|
-
<Playlink playlink
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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',
|
|
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
|
})
|