@playpilot/tpi 8.41.0-beta.1 → 8.41.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.41.0-beta.1",
3
+ "version": "8.41.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -4,9 +4,4 @@ export const SplitTest = {
4
4
  numberOfVariants: 2,
5
5
  variantNames: ['Separated', 'Inline'] as string[],
6
6
  },
7
- DisplayAdFormat: {
8
- key: 'display_ad_format',
9
- numberOfVariants: 3,
10
- variantNames: ['Default', 'Tall', 'Inline'] as string[],
11
- },
12
7
  } as const
@@ -172,10 +172,10 @@ export const campaign: Campaign = {
172
172
  campaign_region: 'nl',
173
173
  content: {
174
174
  header: 'Content header',
175
- header_logo: 'https://placehold.co/50?text=content.header_logo',
175
+ header_logo: null,
176
176
  header_logo_uuid: null,
177
177
  subheader: 'Content subheader',
178
- image: 'https://placehold.co/320x180/333/666?text=content.image',
178
+ image: null,
179
179
  image_uuid: null,
180
180
  video: null,
181
181
  format: null,
@@ -10,6 +10,7 @@ import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
10
10
  import { encodeHtmlEntities } from './html'
11
11
  import type { ParticipantData } from './types/participant'
12
12
  import { getConfigValue } from './api/config'
13
+ import { isParticipantsUrlExcludedViaConfig } from './url'
13
14
 
14
15
  export const keyDataAttribute = 'data-playpilot-injection-key'
15
16
  export const keySelector = `[${keyDataAttribute}]`
@@ -29,6 +30,8 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
29
30
  // This is to filter out injections that can't be injected anyway.
30
31
  const fullText = cleanPhrase(elements.map(element => element.innerText).join(' '))
31
32
 
33
+ const excludeParticipantsByUrl = isParticipantsUrlExcludedViaConfig(window.PlayPilotLinkInjections.config || {})
34
+
32
35
  const validInjections = filterInvalidInTextInjections(mergedInjections)
33
36
  const foundInjections = validInjections.filter(i => fullText.includes(cleanPhrase(i.sentence)))
34
37
  const { injections: limitedInjections, failedMessages } = limitInjectionsByCount(foundInjections)
@@ -37,9 +40,16 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
37
40
  for (const injection of sortInjections(limitedInjections)) {
38
41
  injectionIndex++
39
42
 
40
- if (!injection.manual && injection.type === 'participant' && !doesParticipantMeetThreshold(mergedInjections, injection.participant_details!)) {
41
- failedMessages[injection.key] = 'Participant does not meet popularity threshold.'
42
- continue
43
+ if (!injection.manual && injection.type === 'participant') {
44
+ if (excludeParticipantsByUrl) {
45
+ failedMessages[injection.key] = 'Participants are excluded from this URL.'
46
+ continue
47
+ }
48
+
49
+ if (!doesParticipantMeetThreshold(mergedInjections, injection.participant_details!)) {
50
+ failedMessages[injection.key] = 'Participant does not meet popularity threshold.'
51
+ continue
52
+ }
43
53
  }
44
54
 
45
55
  const elementIndex = elements.findIndex(element => cleanPhrase(element.innerText).includes(cleanPhrase(injection.sentence)))
@@ -7,6 +7,11 @@ export type ConfigResponse = {
7
7
  */
8
8
  exclude_urls_pattern?: string
9
9
 
10
+ /**
11
+ * Similar to `exclude_urls_pattern` but limited to participant injections. Other injections will continue to function.
12
+ */
13
+ exclude_participants_urls_pattern?: string
14
+
10
15
  /**
11
16
  * Used to inject custom styling for the links, modal, and popover. All styling is done via CSS variables. The passed
12
17
  * string should not contain a selector.
package/src/lib/url.ts CHANGED
@@ -27,3 +27,7 @@ export function paramsToString(params: Record<string, any>): string {
27
27
  export function isUrlExcludedViaConfig(config: ConfigResponse): boolean {
28
28
  return !!(config.exclude_urls_pattern && getFullUrlPath().match(new RegExp(config.exclude_urls_pattern)))
29
29
  }
30
+
31
+ export function isParticipantsUrlExcludedViaConfig(config: ConfigResponse): boolean {
32
+ return !!(config.exclude_participants_urls_pattern && getFullUrlPath().match(new RegExp(config.exclude_participants_urls_pattern)))
33
+ }
@@ -6,23 +6,18 @@
6
6
  import { onDestroy } from 'svelte'
7
7
  import Tooltip from '../Tooltip.svelte'
8
8
  import Disclaimer from './Disclaimer.svelte'
9
- import { getSplitTestVariantName } from '$lib/splitTest'
10
- import { SplitTest } from '$lib/enums/SplitTest'
11
9
 
12
10
  interface Props {
13
11
  campaign: Campaign
14
12
  compact?: boolean
15
- slim?: boolean
16
13
  }
17
14
 
18
- const { campaign, compact = false, slim = false }: Props = $props()
15
+ const { campaign, compact = false }: Props = $props()
19
16
 
20
17
  const { disclaimer, content, cta, campaign_type } = $derived(campaign)
21
18
  const { header, subheader, header_logo: logo, image, video } = $derived(content)
22
19
  const { header: buttonLabel, url: href, subheader: info } = $derived(cta)
23
20
 
24
- const tall = slim && getSplitTestVariantName(SplitTest.DisplayAdFormat) === 'Tall'
25
-
26
21
  let contentHeight = $state(0)
27
22
  let videoElement: HTMLVideoElement | null = $state(null)
28
23
 
@@ -39,8 +34,8 @@
39
34
  })
40
35
  </script>
41
36
 
42
- <a {href} target="_blank" class="display" class:compact class:slim class:tall rel="sponsored" {onclick} use:middlemouse={{ onclick }}>
43
- {#if info && !slim}
37
+ <a {href} target="_blank" class="display" class:compact rel="sponsored" {onclick} use:middlemouse={{ onclick }}>
38
+ {#if info}
44
39
  <div class="info">
45
40
  {#if compact}
46
41
  <Tooltip>
@@ -67,7 +62,7 @@
67
62
  <span class="name">{header}</span>
68
63
  {/if}
69
64
 
70
- {#if subheader && !slim}
65
+ {#if subheader}
71
66
  <div class="subheader">{subheader}</div>
72
67
  {/if}
73
68
  </div>
@@ -95,8 +90,6 @@
95
90
  </a>
96
91
 
97
92
  <style lang="scss">
98
- $background-color: rgb(110, 110, 110);
99
-
100
93
  .display {
101
94
  display: block;
102
95
  position: relative;
@@ -109,16 +102,6 @@
109
102
  font-family: theme(detail-font-family, font-family);
110
103
  line-height: 1.2;
111
104
  font-style: normal !important;
112
-
113
- &.slim {
114
- aspect-ratio: unset;
115
- height: margin(5.5);
116
- background: $background-color;
117
-
118
- &.tall {
119
- height: margin(8);
120
- }
121
- }
122
105
  }
123
106
 
124
107
  .info {
@@ -148,7 +131,7 @@
148
131
  bottom: 0;
149
132
  left: 0;
150
133
  padding: margin(0.5);
151
- background: linear-gradient(to right, $background-color, rgba(85, 85, 85, 0.3));
134
+ background: linear-gradient(to right, rgba(110, 110, 110, 1), rgba(85, 85, 85, 0.3));
152
135
  font-size: margin(0.875);
153
136
  text-decoration: none;
154
137
 
@@ -159,25 +142,10 @@
159
142
  padding: margin(0.75);
160
143
  }
161
144
  }
162
-
163
- .slim & {
164
- background: transparent;
165
- padding-left: margin(10.5);
166
- height: 100%;
167
- font-size: theme(font-size-small);
168
- }
169
145
  }
170
146
 
171
147
  .logo {
172
148
  border-radius: theme(display-ad-border-radius, border-radius);
173
-
174
- .slim & {
175
- position: absolute;
176
- width: margin(1.5);
177
- height: margin(1.5);
178
- bottom: margin(0.5);
179
- left: margin(0.5);
180
- }
181
149
  }
182
150
 
183
151
  .name {
@@ -208,26 +176,11 @@
208
176
  display: block;
209
177
  width: 100%;
210
178
  height: auto;
211
-
212
- .slim & {
213
- width: auto;
214
- height: 100%;
215
- aspect-ratio: 16 / 9;
216
- background: black;
217
- }
218
-
219
- .tall & {
220
- mask-image: linear-gradient(to right, white 50%, transparent);
221
- }
222
179
  }
223
180
 
224
181
  .disclaimer {
225
182
  position: absolute;
226
183
  bottom: calc(var(--offset) + margin(0.25));
227
184
  right: margin(0.25);
228
-
229
- .slim & {
230
- bottom: margin(0.25);
231
- }
232
185
  }
233
186
  </style>
@@ -9,8 +9,6 @@
9
9
  import IconArrow from '../Icons/IconArrow.svelte'
10
10
  import ModalCloseOverlay from './ModalCloseOverlay.svelte'
11
11
  import Display from '../Ads/Display.svelte'
12
- import { SplitTest } from '$lib/enums/SplitTest'
13
- import { getSplitTestVariantName, trackSplitTestView } from '$lib/splitTest'
14
12
 
15
13
  interface Props {
16
14
  items: Record<string, any>[]
@@ -29,8 +27,6 @@
29
27
  let element: HTMLElement | null = $state(null)
30
28
  let displayAd = $state(getFirstAdOfType('card'))
31
29
 
32
- if (!!displayAd) trackSplitTestView(SplitTest.DisplayAdFormat)
33
-
34
30
  onMount(() => {
35
31
  setAvailableHeight()
36
32
 
@@ -64,9 +60,9 @@
64
60
  <svelte:window onresize={setAvailableHeight} onupdateads={() => displayAd = getFirstAdOfType('card')} />
65
61
 
66
62
  {#snippet prepend()}
67
- {#if displayAd && getSplitTestVariantName(SplitTest.DisplayAdFormat) !== 'Inline'}
63
+ {#if displayAd}
68
64
  <div class="ad">
69
- <Display campaign={displayAd} slim />
65
+ <Display campaign={displayAd} />
70
66
  </div>
71
67
  {/if}
72
68
  {/snippet}
@@ -1,7 +1,5 @@
1
1
  <script lang="ts">
2
- import { SplitTest } from '$lib/enums/SplitTest'
3
2
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
4
- import { getSplitTestVariantName } from '$lib/splitTest'
5
3
  import { track } from '$lib/tracking'
6
4
  import type { TitleData } from '$lib/types/title'
7
5
  import Title from '../Title.svelte'
@@ -18,6 +16,6 @@
18
16
 
19
17
  <RailModal items={titles} {initialIndex} {onclose} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })} {...restProps}>
20
18
  {#snippet each(title, currentIndex)}
21
- <Title {title} useVideoBackground={title.sid === titles[currentIndex]?.sid} useDisplayAdSplitTest={getSplitTestVariantName(SplitTest.DisplayAdFormat) === 'Inline'} />
19
+ <Title {title} useVideoBackground={title.sid === titles[currentIndex]?.sid} />
22
20
  {/snippet}
23
21
  </RailModal>
@@ -7,14 +7,11 @@
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
- import { getContext, type Snippet } from 'svelte'
10
+ import { getContext } from 'svelte'
11
11
  import Playlink from './Playlink.svelte'
12
12
  import Button from '../Button.svelte'
13
13
  import PlaylinksDisclaimer from './PlaylinksDisclaimer.svelte'
14
14
  import { getConfigValue } from '$lib/api/config'
15
- import { getSplitTestVariantName } from '$lib/splitTest'
16
- import { SplitTest } from '$lib/enums/SplitTest'
17
- import Display from '../Ads/Display.svelte'
18
15
 
19
16
  type Category = PlaylinkCategory | '' | 'Other'
20
17
  type CategorizedPlaylinks = Partial<Record<Category, PlaylinkData[]>>
@@ -24,10 +21,9 @@
24
21
  playlinks: PlaylinkData[]
25
22
  title: TitleData
26
23
  limit?: number
27
- children?: Snippet
28
24
  }
29
25
 
30
- const { playlinks, title, limit = 3, children }: Props = $props()
26
+ const { playlinks, title, limit = 3 }: Props = $props()
31
27
 
32
28
  const isModal = getContext('scope') === 'modal'
33
29
  const type = getContext('type')
@@ -98,11 +94,7 @@
98
94
 
99
95
  <!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
100
96
  {#if displayAd && (index === 0)}
101
- {#if children}
102
- {@render children()}
103
- {:else}
104
- <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
105
- {/if}
97
+ <Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
106
98
  {/if}
107
99
  {/each}
108
100
  </div>
@@ -14,18 +14,14 @@
14
14
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
15
15
  import TitleBackground from './TitleBackground.svelte'
16
16
  import TitleInfo from './TitleInfo.svelte'
17
- import Display from './Ads/Display.svelte'
18
- import type { Campaign } from '$lib/types/campaign'
19
- import { getFirstAdOfType } from '$lib/api/ads'
20
17
 
21
18
  interface Props {
22
19
  title: TitleData
23
20
  small?: boolean
24
21
  useVideoBackground?: boolean
25
- useDisplayAdSplitTest?: boolean // NOTE: Temporary split test, remove me after the DisplayAdFormat split test is gone
26
22
  }
27
23
 
28
- const { title, small = false, useVideoBackground = true, useDisplayAdSplitTest = false }: Props = $props()
24
+ const { title, small = false, useVideoBackground = true }: Props = $props()
29
25
 
30
26
  const noAffiliate = !!window.PlayPilotLinkInjections?.no_affiliate
31
27
 
@@ -62,11 +58,7 @@
62
58
  {/if}
63
59
 
64
60
  {#if !noAffiliate}
65
- <Playlinks playlinks={title.providers} {title}>
66
- {#if useDisplayAdSplitTest && getFirstAdOfType('card')}
67
- <Display campaign={getFirstAdOfType('card')!} />
68
- {/if}
69
- </Playlinks>
61
+ <Playlinks playlinks={title.providers} {title} />
70
62
  {/if}
71
63
 
72
64
  <ParticipantsRail {title} />
@@ -39,21 +39,10 @@
39
39
 
40
40
  // Used to re-render some elements with a key
41
41
  let renderKey = Math.random()
42
-
43
- function insertMockAd(): void {
44
- // @ts-ignore
45
- window.PlayPilotLinkInjections.consents = {
46
- ads: true
47
- }
48
-
49
- window.PlayPilotLinkInjections.ads = [campaign]
50
- }
51
42
  </script>
52
43
 
53
44
  <h1>Elements</h1>
54
45
 
55
- <button onclick={insertMockAd}>Insert mock ad</button>
56
-
57
46
  <p>This page lays out various elements used in TPI. It does not currently include editor elements.</p>
58
47
 
59
48
  <h2>Youtube Embed</h2>
@@ -184,15 +173,7 @@
184
173
  <div class="group">
185
174
  <div>
186
175
  <h3>Ads/Display.svelte</h3>
187
- <div class="item">
188
- <Display {campaign} />
189
-
190
- Compact (Used in popover)
191
- <Display {campaign} compact />
192
-
193
- Slim (Used in rail modals)
194
- <Display {campaign} slim />
195
- </div>
176
+ <div class="item"><Display {campaign} /></div>
196
177
  </div>
197
178
 
198
179
  <div>
@@ -1054,6 +1054,34 @@ describe('injection.ts', () => {
1054
1054
  expect(/** @type {HTMLElement} */ (document.querySelectorAll('[data-playpilot-injection-key]')[0]).dataset.playpilotInjectionType).toBe('participant')
1055
1055
  expect(/** @type {HTMLElement} */ (document.querySelectorAll('[data-playpilot-injection-key]')[1]).dataset.playpilotInjectionType).toBe('title')
1056
1056
  })
1057
+
1058
+ it('Should ignore participants with exclude_participants_url_pattern is true', () => {
1059
+ window.PlayPilotLinkInjections.config = {
1060
+ exclude_participants_urls_pattern: '/',
1061
+ }
1062
+
1063
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
1064
+
1065
+ const elements = Array.from(document.body.querySelectorAll('p'))
1066
+
1067
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
1068
+ const aiInjections = [
1069
+ generateInjection('This is a sentence with an injection.', 'is a'),
1070
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', participant_details: participants[0] },
1071
+ ]
1072
+
1073
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
1074
+ const manualInjections = [
1075
+ { ...generateInjection('This is a sentence with an injection.', 'sentence with'), type: 'participant', manual: true, participant_details: participants[0] },
1076
+ ]
1077
+
1078
+ // @ts-ignore
1079
+ injectLinksInDocument(elements, { aiInjections, manualInjections })
1080
+
1081
+ expect(document.querySelectorAll('[data-playpilot-injection-key]')).toHaveLength(2)
1082
+ expect(document.querySelectorAll('[data-playpilot-injection-key]')[0].getAttribute('data-playpilot-injection-key')).toBe(aiInjections[0].key)
1083
+ expect(document.querySelectorAll('[data-playpilot-injection-key]')[1].getAttribute('data-playpilot-injection-key')).toBe(manualInjections[0].key)
1084
+ })
1057
1085
  })
1058
1086
 
1059
1087
  describe('config.open_tpi_links_in_explore', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import { getFullUrlPath, getUrlParams, isUrlExcludedViaConfig, paramsToString } from '$lib/url'
2
+ import { getFullUrlPath, getUrlParams, isUrlExcludedViaConfig, isParticipantsUrlExcludedViaConfig, paramsToString } from '$lib/url'
3
3
 
4
4
  describe('url.ts', () => {
5
5
  describe('getFullUrlPath', () => {
@@ -113,4 +113,27 @@ describe('url.ts', () => {
113
113
  expect(isUrlExcludedViaConfig({ exclude_urls_pattern: '/some-page(/.*)?$' })).toBe(true)
114
114
  })
115
115
  })
116
+
117
+ describe('isParticipantsUrlExcludedViaConfig', () => {
118
+ it('Should return false if no key is given in config object', () => {
119
+ expect(isParticipantsUrlExcludedViaConfig({})).toBe(false)
120
+ })
121
+
122
+ it('Should return false if no key is given but is empty', () => {
123
+ expect(isParticipantsUrlExcludedViaConfig({ exclude_participants_urls_pattern: '' })).toBe(false)
124
+ })
125
+
126
+ it('Should return false if pattern is given but does not match url', () => {
127
+ window.location.pathname = '/path'
128
+ expect(isParticipantsUrlExcludedViaConfig({ exclude_participants_urls_pattern: '/not-path' })).toBe(false)
129
+ })
130
+
131
+ it('Should return true if pattern is given and does matches url', () => {
132
+ window.location.pathname = '/path'
133
+ expect(isParticipantsUrlExcludedViaConfig({ exclude_participants_urls_pattern: '/path' })).toBe(true)
134
+
135
+ window.location.pathname = '/some-page/subpath'
136
+ expect(isParticipantsUrlExcludedViaConfig({ exclude_participants_urls_pattern: '/some-page(/.*)?$' })).toBe(true)
137
+ })
138
+ })
116
139
  })