@playpilot/tpi 8.27.1 → 8.28.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.
@@ -1,160 +1,160 @@
1
- <script lang="ts">
2
- import { onMount } from 'svelte'
3
- import { heading } from '$lib/actions/heading'
4
- import { fetchTitlesForParticipant } from '$lib/api/participants'
5
- import { openModal } from '$lib/modal'
6
- import type { ParticipantData } from '$lib/types/participant'
7
- import type { TitleData } from '$lib/types/title'
8
- import ListTitle from './ListTitle.svelte'
9
- import { t } from '$lib/localization'
10
- import { isRetargetingAllowed } from '$lib/retargeting'
11
- import { trackViaPixel } from '@playpilot/retargeting-tracking'
12
- import { MetaEvent } from '$lib/enums/TrackingEvent'
13
-
14
- interface Props {
15
- participant: ParticipantData
16
- small?: boolean
17
- }
18
-
19
- const { participant, small = false }: Props = $props()
20
-
21
- const { name, birth_date, death_date } = $derived(participant)
22
-
23
- const pageSize = 30
24
-
25
- let titles: TitleData[] = $state([])
26
- let page = $state(1)
27
- let hasMorePages = $state(true)
28
- let loading = $state(true)
29
-
30
- onMount(loadMore)
31
-
32
- if (isRetargetingAllowed()) trackViaPixel(MetaEvent.ParticipantView, { participant: participant.name })
33
-
34
- function formatDate(dateString: string): string {
35
- const date = new Date(dateString)
36
- return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
37
- }
38
-
39
- async function loadMore(): Promise<void> {
40
- loading = true
41
-
42
- try {
43
- const results = await fetchTitlesForParticipant(participant, { page })
44
-
45
- titles = [...titles, ...results]
46
- hasMorePages = results?.length >= pageSize
47
- } catch {
48
- hasMorePages = false
49
- } finally {
50
- loading = false
51
- page += 1
52
- }
53
- }
54
- </script>
55
-
56
- <div class="header" class:small>
57
- <div class="heading" use:heading={2} id="heading">{name}</div>
58
-
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}
64
- </div>
65
-
66
- <div class="content">
67
- {#if !small}
68
- <div class="heading subheading" use:heading={3} id="credits">{t('Credits')}</div>
69
- {/if}
70
-
71
- <div class="list">
72
- {#each titles as title}
73
- <ListTitle {title} compact={small} onclick={(event) => openModal({ event, data: title })} />
74
- {/each}
75
-
76
- {#if loading}
77
- {#each { length: 6 }}
78
- <div class="skeleton" data-testid="skeleton"></div>
79
- {/each}
80
- {:else if hasMorePages}
81
- <button class="more" onclick={loadMore}>{t('Show More')}</button>
82
- {/if}
83
- </div>
84
- </div>
85
-
86
- <style lang="scss">
87
- .header {
88
- padding: margin(4) margin(1) margin(2);
89
- font-family: theme(detail-font-family, font-family);
90
- font-weight: theme(detail-font-weight, normal);
91
- font-size: theme(detail-font-size, font-size-base);
92
- line-height: theme(participant-description-line-height, normal);
93
- color: theme(detail-text-color, text-color);
94
-
95
- &.small {
96
- padding: margin(1);
97
- }
98
- }
99
-
100
- .heading {
101
- margin: 0;
102
- font-family: theme(detail-title-font-family, font-family);
103
- font-weight: theme(detail-title-font-weight, lighter);
104
- font-size: theme(detail-title-font-size, margin(1.5));
105
- line-height: normal;
106
- font-style: theme(detail-title-font-style, normal);
107
-
108
- &.subheading {
109
- margin: 0 0 margin(0.5);
110
- font-size: theme(detail-title-small-font-size, margin(1.25));
111
- }
112
- }
113
-
114
- .dates {
115
- margin: margin(0.5) 0 0;
116
- }
117
-
118
- .content {
119
- padding: 0 margin(1) margin(1);
120
- color: theme(detail-text-color, text-color);
121
- font-family: theme(detail-font-family, font-family);
122
- font-weight: theme(detail-font-weight, normal);
123
- font-size: theme(detail-font-size, font-size-base);
124
- line-height: normal;
125
- font-style: normal;
126
-
127
- ::view-transition-old(content),
128
- ::view-transition-new(content) {
129
- height: 100%;
130
- }
131
- }
132
-
133
- .list {
134
- display: flex;
135
- flex-direction: column;
136
- gap: margin(0.5);
137
- }
138
-
139
- .skeleton {
140
- min-height: margin(7.25);
141
- border-radius: theme(playlink-border-radius, border-radius);
142
- background: theme(detail-background-light, lighter);
143
- }
144
-
145
- .more {
146
- cursor: pointer;
147
- appearance: none;
148
- padding: margin(0.5) margin(1);
149
- border: 0;
150
- border-radius: theme(participants-load-more-border-radius, border-radius);
151
- background: theme(participants-load-more-background, content);
152
- color: inherit;
153
- font-size: inherit;
154
- font-family: inherit;
155
-
156
- &:hover {
157
- filter: brightness(1.2);
158
- }
159
- }
160
- </style>
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import { heading } from '$lib/actions/heading'
4
+ import { fetchTitlesForParticipant } from '$lib/api/participants'
5
+ import { openModal } from '$lib/modal'
6
+ import type { ParticipantData } from '$lib/types/participant'
7
+ import type { TitleData } from '$lib/types/title'
8
+ import ListTitle from './ListTitle.svelte'
9
+ import { t } from '$lib/localization'
10
+ import { isRetargetingAllowed } from '$lib/retargeting'
11
+ import { trackViaPixel } from '@playpilot/retargeting-tracking'
12
+ import { MetaEvent } from '$lib/enums/TrackingEvent'
13
+
14
+ interface Props {
15
+ participant: ParticipantData
16
+ small?: boolean
17
+ }
18
+
19
+ const { participant, small = false }: Props = $props()
20
+
21
+ const { name, birth_date, death_date } = $derived(participant)
22
+
23
+ const pageSize = 30
24
+
25
+ let titles: TitleData[] = $state([])
26
+ let page = $state(1)
27
+ let hasMorePages = $state(true)
28
+ let loading = $state(true)
29
+
30
+ onMount(loadMore)
31
+
32
+ if (isRetargetingAllowed()) trackViaPixel(MetaEvent.ParticipantView, { participant: participant.name })
33
+
34
+ function formatDate(dateString: string): string {
35
+ const date = new Date(dateString)
36
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
37
+ }
38
+
39
+ async function loadMore(): Promise<void> {
40
+ loading = true
41
+
42
+ try {
43
+ const results = await fetchTitlesForParticipant(participant, { page })
44
+
45
+ titles = [...titles, ...results]
46
+ hasMorePages = results?.length >= pageSize
47
+ } catch {
48
+ hasMorePages = false
49
+ } finally {
50
+ loading = false
51
+ page += 1
52
+ }
53
+ }
54
+ </script>
55
+
56
+ <div class="header" class:small>
57
+ <div class="heading" use:heading={2} id="heading">{name}</div>
58
+
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}
64
+ </div>
65
+
66
+ <div class="content">
67
+ {#if !small}
68
+ <div class="heading subheading" use:heading={3} id="credits">{t('Credits')}</div>
69
+ {/if}
70
+
71
+ <div class="list">
72
+ {#each titles as title}
73
+ <ListTitle {title} compact={small} onclick={(event) => openModal({ event, data: title })} />
74
+ {/each}
75
+
76
+ {#if loading}
77
+ {#each { length: 6 }}
78
+ <div class="skeleton" data-testid="skeleton"></div>
79
+ {/each}
80
+ {:else if hasMorePages}
81
+ <button class="more" onclick={loadMore}>{t('Show More')}</button>
82
+ {/if}
83
+ </div>
84
+ </div>
85
+
86
+ <style lang="scss">
87
+ .header {
88
+ padding: margin(4) margin(1) margin(2);
89
+ font-family: theme(detail-font-family, font-family);
90
+ font-weight: theme(detail-font-weight, normal);
91
+ font-size: theme(detail-font-size, font-size-base);
92
+ line-height: theme(participant-description-line-height, normal);
93
+ color: theme(detail-text-color, text-color);
94
+
95
+ &.small {
96
+ padding: margin(1);
97
+ }
98
+ }
99
+
100
+ .heading {
101
+ margin: 0;
102
+ font-family: theme(detail-title-font-family, font-family);
103
+ font-weight: theme(detail-title-font-weight, lighter);
104
+ font-size: theme(detail-title-font-size, margin(1.5));
105
+ line-height: normal;
106
+ font-style: theme(detail-title-font-style, normal);
107
+
108
+ &.subheading {
109
+ margin: 0 0 margin(0.5);
110
+ font-size: theme(detail-title-small-font-size, margin(1.25));
111
+ }
112
+ }
113
+
114
+ .dates {
115
+ margin: margin(0.5) 0 0;
116
+ }
117
+
118
+ .content {
119
+ padding: 0 margin(1) margin(1);
120
+ color: theme(detail-text-color, text-color);
121
+ font-family: theme(detail-font-family, font-family);
122
+ font-weight: theme(detail-font-weight, normal);
123
+ font-size: theme(detail-font-size, font-size-base);
124
+ line-height: normal;
125
+ font-style: normal;
126
+
127
+ ::view-transition-old(content),
128
+ ::view-transition-new(content) {
129
+ height: 100%;
130
+ }
131
+ }
132
+
133
+ .list {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: margin(0.5);
137
+ }
138
+
139
+ .skeleton {
140
+ min-height: margin(7.25);
141
+ border-radius: theme(playlink-border-radius, border-radius);
142
+ background: theme(detail-background-light, lighter);
143
+ }
144
+
145
+ .more {
146
+ cursor: pointer;
147
+ appearance: none;
148
+ padding: margin(0.5) margin(1);
149
+ border: 0;
150
+ border-radius: theme(participants-load-more-border-radius, border-radius);
151
+ background: theme(participants-load-more-background, content);
152
+ color: inherit;
153
+ font-size: inherit;
154
+ font-family: inherit;
155
+
156
+ &:hover {
157
+ filter: brightness(1.2);
158
+ }
159
+ }
160
+ </style>
@@ -35,7 +35,7 @@
35
35
 
36
36
  <button onclick={(() => {
37
37
  destroyExplore()
38
- window.PlayPilotLinkInjections.config.explore_use_router = !useExploreRouter()
38
+ window.PlayPilotLinkInjections.config!.explore_use_router = !useExploreRouter()
39
39
  insertExplore()
40
40
  })}>Toggle new explore</button>
41
41
 
@@ -73,4 +73,3 @@
73
73
  background: theme(light);
74
74
  }
75
75
  </style>
76
-
@@ -40,7 +40,7 @@ describe('$lib/api/externalPages', () => {
40
40
 
41
41
  expect(response).toBe('Some response')
42
42
  expect(api).toHaveBeenCalledWith(
43
- '/external-pages/?api-token=a&include_title_details=true&language=en-US&url=https://some-url',
43
+ '/external-pages/?api-token=a&include_title_details=true&include_participant_details=true&language=en-US&url=https://some-url',
44
44
  expect.objectContaining({
45
45
  method: 'GET',
46
46
  }),
@@ -120,7 +120,7 @@ describe('explore.js', () => {
120
120
 
121
121
  it('Should not insert if config object is present but explore_navigation_selector is not given', () => {
122
122
  window.PlayPilotLinkInjections.config = {
123
- explore_navigation_selector: null,
123
+ explore_navigation_selector: '',
124
124
  }
125
125
 
126
126
  expect(insertExploreIntoNavigation()).toBe(false)
@@ -136,11 +136,12 @@ describe('explore.js', () => {
136
136
  expect(insertExploreIntoNavigation()).toBe(false)
137
137
  })
138
138
 
139
- it('Should insert if explore_navigation_selector is given and selector is valid', () => {
139
+ it('Should insert if explore_navigation_selector is given and selector and path are valid', () => {
140
140
  document.body.innerHTML = '<nav><a></a></nav>'
141
141
 
142
142
  window.PlayPilotLinkInjections.config = {
143
143
  explore_navigation_selector: 'a',
144
+ explore_navigation_path: '/explore',
144
145
  }
145
146
 
146
147
  expect(insertExploreIntoNavigation()).toBe(true)
@@ -165,6 +166,7 @@ describe('explore.js', () => {
165
166
  window.PlayPilotLinkInjections.config = {
166
167
  explore_navigation_selector: 'a',
167
168
  explore_navigation_label: 'Some label',
169
+ explore_navigation_path: '/explore',
168
170
  }
169
171
 
170
172
  insertExploreIntoNavigation()
@@ -984,7 +984,7 @@ describe('injection.ts', () => {
984
984
  injectLinksInDocument(elements, { aiInjections: [injection], manualInjections: [] })
985
985
 
986
986
  const link = /** @type {HTMLAnchorElement} */ (document.querySelector('a'))
987
- expect(link.href).toBe(window.PlayPilotLinkInjections.config.explore_navigation_path + `?route=modal&sid=${injection.title_details?.sid}`)
987
+ expect(link.href).toBe(window.PlayPilotLinkInjections.config?.explore_navigation_path + `?route=modal&sid=${injection.title_details?.sid}`)
988
988
  expect(link.target).not.toBeTruthy()
989
989
  })
990
990
 
@@ -26,6 +26,7 @@ describe('retargeting.ts', () => {
26
26
  describe('isPixelAllow', () => {
27
27
  it('Should return false if no config object is set', () => {
28
28
  vi.mocked(hasConsentedTo).mockImplementation(() => true)
29
+ // @ts-ignore
29
30
  window.PlayPilotLinkInjections.config = null
30
31
 
31
32
  expect(isRetargetingAllowed()).toBe(false)
@@ -33,6 +34,7 @@ describe('retargeting.ts', () => {
33
34
 
34
35
  it('Should return false if config object is without allow_retargeting_pixels', () => {
35
36
  vi.mocked(hasConsentedTo).mockImplementation(() => true)
37
+ // @ts-ignore
36
38
  window.PlayPilotLinkInjections.config = { something: true }
37
39
 
38
40
  expect(isRetargetingAllowed()).toBe(false)
@@ -118,6 +118,7 @@ describe('Playlink.svelte', () => {
118
118
  })
119
119
 
120
120
  it('Should render as link when user has not consented to affiliate but require_affiliate_consent is false', () => {
121
+ // @ts-ignore
121
122
  window.PlayPilotLinkInjections.config.require_affiliate_consent = false
122
123
 
123
124
  vi.mocked(hasConsentedTo).mockImplementation(() => false)
@@ -129,6 +130,7 @@ describe('Playlink.svelte', () => {
129
130
  })
130
131
 
131
132
  it('Should render as link when user has not consented to affiliate and require_affiliate_consent is true', () => {
133
+ // @ts-ignore
132
134
  window.PlayPilotLinkInjections.config.require_affiliate_consent = true
133
135
 
134
136
  vi.mocked(hasConsentedTo).mockImplementation(() => false)
@@ -1,108 +1,108 @@
1
- import { cleanup } from '@testing-library/svelte'
2
- import { afterEach, beforeAll, beforeEach, vi } from 'vitest'
3
- import { fakeFetch } from './helpers'
4
-
5
- vi.mock('svelte/transition')
6
-
7
- // https://github.com/jsdom/jsdom/issues/3429#issuecomment-1936128876
8
- const mockAnimations = () => {
9
- Element.prototype.animate ??= vi.fn().mockReturnValue({
10
- onfinish: null,
11
- finished: Promise.resolve(),
12
- cancel: vi.fn(),
13
- startTime: null,
14
- currentTime: null,
15
- })
16
-
17
- Element.prototype.getAnimations ??= vi.fn().mockReturnValue([])
18
- }
19
-
20
- const mockLocalStorage = () => {
21
- /** @type {Record<string, string>} */
22
- let store = {}
23
-
24
- // @ts-ignore
25
- ;(window).localStorage = {
26
- getItem: function (key) {
27
- return store[key] || null
28
- },
29
- setItem: function (key, value) {
30
- store[key] = value.toString()
31
- },
32
- removeItem: function (key) {
33
- delete store[key]
34
- },
35
- clear: function () {
36
- store = {}
37
- },
38
- }
39
- }
40
-
41
- const mockSessionStorage = () => {
42
- /** @type {Record<string, string>} */
43
- let store = {}
44
-
45
- // @ts-ignore
46
- ;(window).sessionStorage = {
47
- getItem: function (key) {
48
- return store[key] || null
49
- },
50
- setItem: function (key, value) {
51
- store[key] = value.toString()
52
- },
53
- removeItem: function (key) {
54
- delete store[key]
55
- },
56
- clear: function () {
57
- store = {}
58
- },
59
- }
60
- }
61
-
62
- export const mockIntersectionObserver = () => {
63
- // @ts-ignore
64
- global.IntersectionObserver = vi.fn((callback) => ({
65
- disconnect: vi.fn(),
66
- observe: vi.fn(() => callback([{ isIntersecting: true }])),
67
- unobserve: vi.fn(),
68
- }))
69
- }
70
-
71
- beforeAll(() => {
72
- fakeFetch()
73
- mockLocalStorage()
74
- mockSessionStorage()
75
- mockIntersectionObserver()
76
- })
77
-
78
- beforeEach(() => {
79
- // @ts-ignore
80
- window.PlayPilotLinkInjections = { token: 'some-token', require_consent: false }
81
-
82
- // Enable all consent
83
- // @ts-ignore
84
- window.__tcfapi = (command, _version, callback) => {
85
- if (command !== 'addEventListener') return
86
-
87
- setTimeout(() => callback({
88
- purpose: { consents: { 1: true, 2: true, 7: true, 8: true, 9: true, 10: true } },
89
- eventStatus: 'tcloaded',
90
- }, true), 0)
91
-
92
- return 1
93
- }
94
-
95
- // Reset cookies
96
- document.cookie.split(';').forEach((cookie) => document.cookie = cookie.replace(/^ +/, '').replace(/=.*/, '=;'))
97
-
98
- window.HTMLElement.prototype.scrollIntoView = vi.fn()
99
-
100
- localStorage.clear()
101
- sessionStorage.clear()
102
-
103
- mockAnimations()
104
- })
105
-
106
- afterEach(() => {
107
- cleanup()
108
- })
1
+ import { cleanup } from '@testing-library/svelte'
2
+ import { afterEach, beforeAll, beforeEach, vi } from 'vitest'
3
+ import { fakeFetch } from './helpers'
4
+
5
+ vi.mock('svelte/transition')
6
+
7
+ // https://github.com/jsdom/jsdom/issues/3429#issuecomment-1936128876
8
+ const mockAnimations = () => {
9
+ Element.prototype.animate ??= vi.fn().mockReturnValue({
10
+ onfinish: null,
11
+ finished: Promise.resolve(),
12
+ cancel: vi.fn(),
13
+ startTime: null,
14
+ currentTime: null,
15
+ })
16
+
17
+ Element.prototype.getAnimations ??= vi.fn().mockReturnValue([])
18
+ }
19
+
20
+ const mockLocalStorage = () => {
21
+ /** @type {Record<string, string>} */
22
+ let store = {}
23
+
24
+ // @ts-ignore
25
+ ;(window).localStorage = {
26
+ getItem: function (key) {
27
+ return store[key] || null
28
+ },
29
+ setItem: function (key, value) {
30
+ store[key] = value.toString()
31
+ },
32
+ removeItem: function (key) {
33
+ delete store[key]
34
+ },
35
+ clear: function () {
36
+ store = {}
37
+ },
38
+ }
39
+ }
40
+
41
+ const mockSessionStorage = () => {
42
+ /** @type {Record<string, string>} */
43
+ let store = {}
44
+
45
+ // @ts-ignore
46
+ ;(window).sessionStorage = {
47
+ getItem: function (key) {
48
+ return store[key] || null
49
+ },
50
+ setItem: function (key, value) {
51
+ store[key] = value.toString()
52
+ },
53
+ removeItem: function (key) {
54
+ delete store[key]
55
+ },
56
+ clear: function () {
57
+ store = {}
58
+ },
59
+ }
60
+ }
61
+
62
+ export const mockIntersectionObserver = () => {
63
+ // @ts-ignore
64
+ global.IntersectionObserver = vi.fn((callback) => ({
65
+ disconnect: vi.fn(),
66
+ observe: vi.fn(() => callback([{ isIntersecting: true }])),
67
+ unobserve: vi.fn(),
68
+ }))
69
+ }
70
+
71
+ beforeAll(() => {
72
+ fakeFetch()
73
+ mockLocalStorage()
74
+ mockSessionStorage()
75
+ mockIntersectionObserver()
76
+ })
77
+
78
+ beforeEach(() => {
79
+ // @ts-ignore
80
+ window.PlayPilotLinkInjections = { token: 'some-token', require_consent: false }
81
+
82
+ // Enable all consent
83
+ // @ts-ignore
84
+ window.__tcfapi = (command, _version, callback) => {
85
+ if (command !== 'addEventListener') return
86
+
87
+ setTimeout(() => callback({
88
+ purpose: { consents: { 1: true, 2: true, 7: true, 8: true, 9: true, 10: true } },
89
+ eventStatus: 'tcloaded',
90
+ }, true), 0)
91
+
92
+ return 1
93
+ }
94
+
95
+ // Reset cookies
96
+ document.cookie.split(';').forEach((cookie) => document.cookie = cookie.replace(/^ +/, '').replace(/=.*/, '=;'))
97
+
98
+ window.HTMLElement.prototype.scrollIntoView = vi.fn()
99
+
100
+ localStorage.clear()
101
+ sessionStorage.clear()
102
+
103
+ mockAnimations()
104
+ })
105
+
106
+ afterEach(() => {
107
+ cleanup()
108
+ })