@playpilot/tpi 3.1.0-beta.5 → 3.2.0-beta.1
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 +8 -7
- package/package.json +1 -1
- package/src/lib/api.ts +1 -3
- package/src/lib/auth.ts +19 -3
- package/src/lib/constants.ts +2 -0
- package/src/lib/enums/TrackingEvent.ts +2 -0
- package/src/lib/linkInjection.ts +83 -43
- package/src/lib/scss/global.scss +6 -6
- package/src/lib/scss/variables.scss +2 -0
- package/src/lib/stores/organization.ts +4 -0
- package/src/lib/tracking.ts +14 -1
- package/src/lib/types/global.d.ts +2 -0
- package/src/lib/types/injection.d.ts +3 -0
- package/src/main.js +5 -1
- package/src/routes/+layout.svelte +13 -1
- package/src/routes/+page.svelte +39 -8
- package/src/routes/components/AfterArticlePlaylinks.svelte +4 -0
- package/src/routes/components/Editorial/AIIndicator.svelte +12 -4
- package/src/routes/components/Editorial/Alert.svelte +12 -2
- package/src/routes/components/Editorial/Editor.svelte +51 -24
- package/src/routes/components/Editorial/EditorItem.svelte +32 -7
- package/src/routes/components/Editorial/EditorTrigger.svelte +72 -0
- package/src/routes/components/Editorial/ManualInjection.svelte +13 -10
- package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +14 -0
- package/src/routes/components/Editorial/ResizeHandle.svelte +5 -5
- package/src/routes/components/Editorial/Search/TitleSearch.svelte +0 -2
- package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +6 -4
- package/src/routes/components/Icons/IconPlayPilot.svelte +8 -0
- package/src/routes/components/Icons/IconWarning.svelte +5 -0
- package/src/routes/components/TitlePopover.svelte +1 -1
- package/src/tests/lib/auth.test.js +41 -1
- package/src/tests/lib/linkInjection.test.js +199 -41
- package/src/tests/lib/tracking.test.js +61 -1
- package/src/tests/routes/+page.test.js +100 -6
- package/src/tests/routes/components/Editorial/AiIndicator.test.js +12 -5
- package/src/tests/routes/components/Editorial/Alert.test.js +10 -3
- package/src/tests/routes/components/Editorial/Editor.test.js +19 -4
- package/src/tests/routes/components/Editorial/EditorItem.test.js +32 -7
- package/src/tests/routes/components/Editorial/EditorTrigger.test.js +24 -0
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +65 -21
- package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +13 -1
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
+
type?: 'error' | 'warning'
|
|
5
6
|
children: Snippet
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
const { children }: Props = $props()
|
|
9
|
+
const { type = 'error', children }: Props = $props()
|
|
9
10
|
</script>
|
|
10
11
|
|
|
11
|
-
<div class="alert">
|
|
12
|
+
<div class="alert {type}">
|
|
12
13
|
{@render children()}
|
|
13
14
|
</div>
|
|
14
15
|
|
|
@@ -19,5 +20,14 @@
|
|
|
19
20
|
border: 1px solid var(--playpilot-error);
|
|
20
21
|
background: var(--playpilot-error-dark);
|
|
21
22
|
font-size: margin(0.75);
|
|
23
|
+
|
|
24
|
+
&.warning {
|
|
25
|
+
border-color: var(--playpilot-warning);
|
|
26
|
+
background: var(--playpilot-warning-dark);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:global(a) {
|
|
30
|
+
color: currentColor;
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
33
|
</style>
|
|
@@ -19,10 +19,19 @@
|
|
|
19
19
|
linkInjections: LinkInjection[],
|
|
20
20
|
htmlString?: string,
|
|
21
21
|
loading?: boolean,
|
|
22
|
-
aiRunning?: boolean
|
|
22
|
+
aiRunning?: boolean,
|
|
23
|
+
injectionsEnabled?: boolean,
|
|
24
|
+
automationEnabled?: boolean,
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
let {
|
|
27
|
+
let {
|
|
28
|
+
linkInjections = $bindable(),
|
|
29
|
+
htmlString = '',
|
|
30
|
+
loading = false,
|
|
31
|
+
aiRunning = false,
|
|
32
|
+
injectionsEnabled = false,
|
|
33
|
+
automationEnabled = false,
|
|
34
|
+
}: Props = $props()
|
|
26
35
|
|
|
27
36
|
const editorPositionKey = 'editor-position'
|
|
28
37
|
const editorHeightKey = 'editor-height'
|
|
@@ -38,7 +47,7 @@
|
|
|
38
47
|
let aIIndicator = $state()
|
|
39
48
|
|
|
40
49
|
const linkInjectionsString = $derived(JSON.stringify(linkInjections))
|
|
41
|
-
const hasChanged = $derived(initialStateString !== linkInjectionsString)
|
|
50
|
+
const hasChanged = $derived(initialStateString && initialStateString !== linkInjectionsString)
|
|
42
51
|
// Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
|
|
43
52
|
const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
|
|
44
53
|
const sortedInjections = $derived(sortInjections(filteredInjections))
|
|
@@ -47,7 +56,7 @@
|
|
|
47
56
|
if (loading) return
|
|
48
57
|
|
|
49
58
|
untrack(() => {
|
|
50
|
-
initialStateString = linkInjectionsString
|
|
59
|
+
requestAnimationFrame(() => initialStateString = linkInjectionsString)
|
|
51
60
|
trackInjectionsCount()
|
|
52
61
|
})
|
|
53
62
|
})
|
|
@@ -112,10 +121,10 @@
|
|
|
112
121
|
|
|
113
122
|
function trackInjectionsCount() {
|
|
114
123
|
const payload = {
|
|
115
|
-
total: linkInjections.length
|
|
116
|
-
failed_automatic: linkInjections.filter(i => i.failed && !i.manual).length
|
|
117
|
-
failed_manual: linkInjections.filter(i => i.failed && i.manual).length
|
|
118
|
-
final_injected: filteredInjections.length
|
|
124
|
+
total: linkInjections.length,
|
|
125
|
+
failed_automatic: linkInjections.filter(i => i.failed && !i.manual).length,
|
|
126
|
+
failed_manual: linkInjections.filter(i => i.failed && i.manual).length,
|
|
127
|
+
final_injected: filteredInjections.length,
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
track(TrackingEvent.TotalInjectionsCount, null, payload)
|
|
@@ -137,19 +146,19 @@
|
|
|
137
146
|
</script>
|
|
138
147
|
|
|
139
148
|
<section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
</div>
|
|
146
|
-
{/if}
|
|
149
|
+
{#if editorElement && !loading}
|
|
150
|
+
<div class="handles">
|
|
151
|
+
<div class="handle">
|
|
152
|
+
<ResizeHandle element={editorElement} {height} onchange={(height) => saveLocalStorage(editorHeightKey, JSON.stringify(height))} />
|
|
153
|
+
</div>
|
|
147
154
|
|
|
148
155
|
<div class="handle">
|
|
149
156
|
<DragHandle element={editorElement} {position} limit={{ x: 16, y: 16 }} onchange={(position) => saveLocalStorage(editorPositionKey, JSON.stringify(position))} />
|
|
150
157
|
</div>
|
|
151
|
-
|
|
158
|
+
</div>
|
|
159
|
+
{/if}
|
|
152
160
|
|
|
161
|
+
<header class="header">
|
|
153
162
|
<h1>Playlinks</h1>
|
|
154
163
|
|
|
155
164
|
{#if loading}
|
|
@@ -165,11 +174,20 @@
|
|
|
165
174
|
{/if}
|
|
166
175
|
</header>
|
|
167
176
|
|
|
168
|
-
{#if !loading && aiRunning}
|
|
169
|
-
<AIIndicator {htmlString} bind:this={aIIndicator} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
|
|
170
|
-
{/if}
|
|
171
|
-
|
|
172
177
|
{#if !loading}
|
|
178
|
+
{#if !injectionsEnabled}
|
|
179
|
+
<div class="alert">
|
|
180
|
+
<Alert type="warning">
|
|
181
|
+
<strong>Playlinks are currently not published.</strong> Visitors to this page will not see any of the injected links.
|
|
182
|
+
Publish playlinks from the <a href="https://partner.playpilot.net">Partner Portal</a>
|
|
183
|
+
</Alert>
|
|
184
|
+
</div>
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
187
|
+
{#if aiRunning || !automationEnabled}
|
|
188
|
+
<AIIndicator {htmlString} bind:this={aIIndicator} {automationEnabled} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
|
|
189
|
+
{/if}
|
|
190
|
+
|
|
173
191
|
{#if hasError}
|
|
174
192
|
<div class="error" transition:slide|global={{ duration: 150 }}>
|
|
175
193
|
<Alert>Something went wrong, check your links below.</Alert>
|
|
@@ -225,7 +243,7 @@
|
|
|
225
243
|
right: margin(1);
|
|
226
244
|
width: 100%;
|
|
227
245
|
max-width: margin(22);
|
|
228
|
-
height: min(
|
|
246
|
+
height: min(70vh, margin(40));
|
|
229
247
|
min-height: 10rem;
|
|
230
248
|
margin: 0;
|
|
231
249
|
padding: margin(1);
|
|
@@ -254,6 +272,13 @@
|
|
|
254
272
|
font-size: margin(0.85);
|
|
255
273
|
}
|
|
256
274
|
|
|
275
|
+
.handles {
|
|
276
|
+
z-index: 20;
|
|
277
|
+
position: sticky;
|
|
278
|
+
top: margin(-1);
|
|
279
|
+
margin: margin(-1) margin(-1) 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
257
282
|
.handle {
|
|
258
283
|
opacity: 0;
|
|
259
284
|
transition: opacity 150ms;
|
|
@@ -264,10 +289,8 @@
|
|
|
264
289
|
}
|
|
265
290
|
|
|
266
291
|
.header {
|
|
292
|
+
@extend .handles;
|
|
267
293
|
z-index: 5;
|
|
268
|
-
position: sticky;
|
|
269
|
-
top: margin(-1);
|
|
270
|
-
margin: margin(-1) margin(-1) 0;
|
|
271
294
|
padding: margin(1) margin(1) margin(1) margin(1.5);
|
|
272
295
|
border: 0;
|
|
273
296
|
background: var(--playpilot-dark);
|
|
@@ -340,6 +363,10 @@
|
|
|
340
363
|
margin-top: margin(0.5);
|
|
341
364
|
}
|
|
342
365
|
|
|
366
|
+
.alert {
|
|
367
|
+
margin: 0 margin(0.5) margin(0.5);
|
|
368
|
+
}
|
|
369
|
+
|
|
343
370
|
.panel {
|
|
344
371
|
z-index: 10;
|
|
345
372
|
position: absolute;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { slide } from 'svelte/transition'
|
|
2
|
+
import { fade, slide } from 'svelte/transition'
|
|
3
3
|
import IconChevron from '../Icons/IconChevron.svelte'
|
|
4
|
+
import IconWarning from '../Icons/IconWarning.svelte'
|
|
4
5
|
import Switch from './Switch.svelte'
|
|
5
6
|
import TextInput from './TextInput.svelte'
|
|
6
7
|
import PlaylinkTypeSelect from './PlaylinkTypeSelect.svelte'
|
|
@@ -11,7 +12,9 @@
|
|
|
11
12
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
12
13
|
import type { LinkInjection } from '$lib/types/injection'
|
|
13
14
|
import type { TitleData } from '$lib/types/title'
|
|
14
|
-
import { truncateAroundPhrase } from '$lib/text'
|
|
15
|
+
import { cleanPhrase, truncateAroundPhrase } from '$lib/text'
|
|
16
|
+
import { getLinkInjectionElements, getLinkInjectionsParentElement, isValidPlaylinkType } from '$lib/linkInjection'
|
|
17
|
+
import { imagePlaceholderDataUrl } from '$lib/constants'
|
|
15
18
|
|
|
16
19
|
interface Props {
|
|
17
20
|
linkInjection: LinkInjection,
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
|
|
23
26
|
const { linkInjection = $bindable(), onremove = () => null, onhighlight = () => null }: Props = $props()
|
|
24
27
|
|
|
25
|
-
const { key, sentence, title_details, failed, inactive } = $derived(linkInjection || {})
|
|
28
|
+
const { key, sentence, title_details, failed, failed_message, inactive } = $derived(linkInjection || {})
|
|
26
29
|
|
|
27
30
|
// @ts-ignore Definitely not null
|
|
28
31
|
const title: TitleData = $derived(title_details)
|
|
@@ -42,8 +45,9 @@
|
|
|
42
45
|
*/
|
|
43
46
|
function toggleOnPageResultHighlight(state: boolean = true): void {
|
|
44
47
|
const matchingElements = getMatchingElements()
|
|
48
|
+
|
|
45
49
|
matchingElements.forEach(element => {
|
|
46
|
-
element.classList.toggle('injection-highlight', state)
|
|
50
|
+
element.classList.toggle('playpilot-injection-highlight', state)
|
|
47
51
|
})
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -76,7 +80,14 @@
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
function getMatchingElements(): Element[] {
|
|
79
|
-
|
|
83
|
+
const injectedElements = Array.from(document.querySelectorAll(`[data-playpilot-injection-key="${key}"]`))
|
|
84
|
+
if (injectedElements.length) return injectedElements
|
|
85
|
+
|
|
86
|
+
// No matching injection was found, so we try and get the element the sentence might be in.
|
|
87
|
+
// Could not be entirely accurate. Could also result in nothing depending on why the injection failed.
|
|
88
|
+
return getLinkInjectionElements((getLinkInjectionsParentElement())).filter((element) => {
|
|
89
|
+
return cleanPhrase(element.innerText).includes(cleanPhrase(linkInjection.sentence))
|
|
90
|
+
}) || []
|
|
80
91
|
}
|
|
81
92
|
</script>
|
|
82
93
|
|
|
@@ -94,7 +105,7 @@
|
|
|
94
105
|
bind:this={element}
|
|
95
106
|
out:slide|global={{ duration: 200 }}>
|
|
96
107
|
<div class="header">
|
|
97
|
-
<img class="poster" src={title.standing_poster} alt="" width="32" height="48" />
|
|
108
|
+
<img class="poster" src={title.standing_poster} alt="" width="32" height="48" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
98
109
|
|
|
99
110
|
<div class="info">
|
|
100
111
|
<div class="title">{title.title}</div>
|
|
@@ -120,13 +131,19 @@
|
|
|
120
131
|
|
|
121
132
|
<div class="content">
|
|
122
133
|
{#if failed}
|
|
123
|
-
<Alert>
|
|
134
|
+
<Alert>{failed_message}</Alert>
|
|
124
135
|
{:else}
|
|
125
136
|
<div class="actions">
|
|
126
137
|
<button class="expand" onclick={() => expanded = !expanded} aria-label="Expand" aria-expanded={expanded}>
|
|
127
138
|
<IconChevron {expanded} />
|
|
128
139
|
</button>
|
|
129
140
|
|
|
141
|
+
{#if !isValidPlaylinkType(linkInjection)}
|
|
142
|
+
<div class="warning" transition:fade={{ duration: 100 }} aria-label="Invalid playlink settings">
|
|
143
|
+
<IconWarning />
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
146
|
+
|
|
130
147
|
<Switch label={inactive ? 'Inactive' : 'Visible'} active={!linkInjection.inactive} onclick={(active) => { linkInjection.inactive = !active; linkInjection.manual = true }}>
|
|
131
148
|
{inactive ? 'Inactive' : 'Visible'}
|
|
132
149
|
</Switch>
|
|
@@ -293,5 +310,13 @@
|
|
|
293
310
|
.offset {
|
|
294
311
|
margin-top: margin(0.75);
|
|
295
312
|
}
|
|
313
|
+
|
|
314
|
+
.warning {
|
|
315
|
+
margin: 0 auto 0 margin(0.5);
|
|
316
|
+
|
|
317
|
+
:global(svg) {
|
|
318
|
+
display: block;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
296
321
|
</style>
|
|
297
322
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconClose from '../Icons/IconClose.svelte'
|
|
3
|
+
import IconPlayPilot from '../Icons/IconPlayPilot.svelte'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
onclick: () => void,
|
|
7
|
+
onclose: () => void,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { onclick, onclose }: Props = $props()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="trigger">
|
|
14
|
+
<button type="button" class="button" title="Show PlayPilot TPI editor" {onclick}>
|
|
15
|
+
<IconPlayPilot />
|
|
16
|
+
</button>
|
|
17
|
+
|
|
18
|
+
<button type="button" class="button close" title="Close PlayPilot TPI" onclick={onclose}>
|
|
19
|
+
<IconClose />
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<style lang="scss">
|
|
24
|
+
.trigger {
|
|
25
|
+
position: fixed;
|
|
26
|
+
bottom: margin(1);
|
|
27
|
+
right: margin(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.button {
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
appearance: none;
|
|
33
|
+
width: margin(3);
|
|
34
|
+
height: margin(3);
|
|
35
|
+
border: 0;
|
|
36
|
+
padding: margin(0.75);
|
|
37
|
+
border-radius: 50%;
|
|
38
|
+
background: var(--playpilot-light);
|
|
39
|
+
box-shadow: var(--playpilot-shadow-large);
|
|
40
|
+
transition: transform 50ms;
|
|
41
|
+
line-height: margin(1.5);
|
|
42
|
+
text-align: center;
|
|
43
|
+
font-size: margin(0.85);
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
filter: brightness(1.1);
|
|
47
|
+
transform: scale(1.05);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
:global(svg) {
|
|
51
|
+
width: 100%;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.close {
|
|
56
|
+
display: flex;
|
|
57
|
+
text-align: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
position: absolute;
|
|
60
|
+
top: margin(-0.5);
|
|
61
|
+
right: margin(-0.5);
|
|
62
|
+
width: margin(1.5);
|
|
63
|
+
height: margin(1.5);
|
|
64
|
+
padding: margin(0.25);
|
|
65
|
+
background: var(--playpilot-content);
|
|
66
|
+
color: white;
|
|
67
|
+
|
|
68
|
+
:global(svg) {
|
|
69
|
+
width: margin(0.75);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
function updateSelection(): void {
|
|
37
37
|
const selection = window.getSelection()
|
|
38
38
|
if (!selection?.anchorNode) return
|
|
39
|
-
|
|
40
|
-
if (!getLinkInjectionsParentElement().contains(selection.anchorNode.parentElement)) return
|
|
39
|
+
if (selection.anchorNode.parentElement && !getLinkInjectionsParentElement().contains(selection.anchorNode.parentElement)) return
|
|
41
40
|
|
|
42
41
|
const selectionText = selection.toString().trim()
|
|
43
42
|
if (!selectionText) return // No content was selected
|
|
@@ -45,9 +44,12 @@
|
|
|
45
44
|
error = ''
|
|
46
45
|
currentSelection = selectionText
|
|
47
46
|
|
|
48
|
-
// Selection spanned more than 1 element
|
|
49
|
-
if
|
|
50
|
-
|
|
47
|
+
// Selection spanned more than 1 element. This will likely result in a failed injection.
|
|
48
|
+
// Check if the selection contains more than 1 element with non-empty content.
|
|
49
|
+
const fragment = selection.getRangeAt(0).cloneContents()
|
|
50
|
+
const selectionNodesWithContent = Array.from(fragment.childNodes).filter(n => n.textContent?.trim())
|
|
51
|
+
if (selectionNodesWithContent.length > 1) {
|
|
52
|
+
error = 'Selection contains multiple items. Selection may not contain a mix of styled and non styled text. Please select the text more directly.'
|
|
51
53
|
return
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -56,7 +58,6 @@
|
|
|
56
58
|
|
|
57
59
|
const nodeContent = selection.getRangeAt(0).commonAncestorContainer.textContent
|
|
58
60
|
const documentTextContent = decodeHtmlEntities(htmlString)
|
|
59
|
-
|
|
60
61
|
if (!nodeContent || !documentTextContent.includes(nodeContent)) { // Selected content is not within the ALI selector
|
|
61
62
|
error = 'Selection was not inside of given content'
|
|
62
63
|
}
|
|
@@ -172,10 +173,12 @@
|
|
|
172
173
|
</div>
|
|
173
174
|
{/if}
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
{#if !error}
|
|
177
|
+
<label for="text">Paired title</label>
|
|
178
|
+
{#key selectionSentence + currentSelection}
|
|
179
|
+
<TitleSearch {onselect} bind:query />
|
|
180
|
+
{/key}
|
|
181
|
+
{/if}
|
|
179
182
|
|
|
180
183
|
<button class="save" onclick={save} disabled={!currentSelection || !selectedTitle}>
|
|
181
184
|
Add playlink
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import IconAlign from '../Icons/IconAlign.svelte'
|
|
4
4
|
import Switch from './Switch.svelte'
|
|
5
5
|
import type { LinkInjection } from '$lib/types/injection'
|
|
6
|
+
import { isValidPlaylinkType } from '$lib/linkInjection'
|
|
7
|
+
import Alert from './Alert.svelte'
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
linkInjection: LinkInjection
|
|
@@ -33,6 +35,14 @@
|
|
|
33
35
|
</Switch>
|
|
34
36
|
</div>
|
|
35
37
|
|
|
38
|
+
{#if !isValidPlaylinkType(linkInjection)}
|
|
39
|
+
<div class="alert" transition:slide={{ duration: 200 }}>
|
|
40
|
+
<Alert type="warning">
|
|
41
|
+
At least one layout option must be selected for the playlink to be visible.
|
|
42
|
+
</Alert>
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|
|
45
|
+
|
|
36
46
|
{#if linkInjection.after_article}
|
|
37
47
|
<div transition:slide={{ duration: 100 }}>
|
|
38
48
|
<div class="label">Bottom playlinks style</div>
|
|
@@ -133,4 +143,8 @@
|
|
|
133
143
|
font-size: margin(0.675);
|
|
134
144
|
color: var(--playpilot-text-color-alt);
|
|
135
145
|
}
|
|
146
|
+
|
|
147
|
+
.alert {
|
|
148
|
+
margin-top: margin(0.5);
|
|
149
|
+
}
|
|
136
150
|
</style>
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
ontouchmove={move}
|
|
64
64
|
onmouseup={end}
|
|
65
65
|
ontouchend={end}
|
|
66
|
-
onresize={() => setHeight(height)} />
|
|
66
|
+
onresize={() => { if (height) setHeight(height) }} />
|
|
67
67
|
|
|
68
68
|
<button class="resize-handle" onmousedown={start} ontouchstart={start} aria-label="Move editor"></button>
|
|
69
69
|
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
appearance: none;
|
|
74
74
|
position: absolute;
|
|
75
75
|
top: 0;
|
|
76
|
-
left:
|
|
77
|
-
width:
|
|
76
|
+
left: 0;
|
|
77
|
+
width: 100%;
|
|
78
78
|
height: margin(0.5);
|
|
79
79
|
background: transparent;
|
|
80
80
|
border: 0;
|
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
content: "";
|
|
100
100
|
position: absolute;
|
|
101
101
|
top: 0;
|
|
102
|
-
right:
|
|
103
|
-
left:
|
|
102
|
+
right: 20%;
|
|
103
|
+
left: 20%;
|
|
104
104
|
bottom: 80%;
|
|
105
105
|
border-radius: 0 0 margin(1) margin(1);
|
|
106
106
|
background: var(--playpilot-text-color);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { playPilotBaseUrl } from '$lib/constants'
|
|
2
|
+
import { imagePlaceholderDataUrl, playPilotBaseUrl } from '$lib/constants'
|
|
3
3
|
import type { TitleData } from '$lib/types/title'
|
|
4
4
|
import IconIMDb from '../../Icons/IconIMDb.svelte'
|
|
5
5
|
import IconNewTab from '../../Icons/IconNewTab.svelte'
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<button class="item" class:hoverable {onclick}>
|
|
17
|
-
<img class="poster" src={title.standing_poster} alt="" width="28" height="42" />
|
|
17
|
+
<img class="poster" src={title.standing_poster} alt="" width="28" height="42" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
18
18
|
|
|
19
19
|
<div class="content">
|
|
20
20
|
<div class="name">{title.title}</div>
|
|
@@ -73,10 +73,12 @@
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
.poster {
|
|
76
|
+
flex: 0 0 auto;
|
|
77
|
+
height: auto;
|
|
76
78
|
width: margin(1.75);
|
|
79
|
+
aspect-ratio: 28/42;
|
|
77
80
|
border-radius: margin(0.25);
|
|
78
|
-
|
|
79
|
-
background: var(--playpilot-dark);
|
|
81
|
+
background: var(--playpilot-content);
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
.name {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 57.48 62.96">
|
|
2
|
+
<defs>
|
|
3
|
+
<style>.playpilot-logo{fill:url(#playpilot-linear-gradient);}</style>
|
|
4
|
+
<linearGradient id="playpilot-linear-gradient" x1="-90.03" y1="-40.23" x2="-34.8" y2="-101.48" gradientTransform="translate(74.02 108.65)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#de00b9"/><stop offset="0.99" stop-color="#ff557b"/></linearGradient>
|
|
5
|
+
</defs>
|
|
6
|
+
|
|
7
|
+
<path class="playpilot-logo" d="M57.48,31.48C57.48,22.17,20.85,2.19,9.2.18,7.42-.12,3.78-.37,1.36,2.25-4.32,8.39,9.62,21.5,9.62,31.48S-4.32,54.57,1.36,60.72c2.42,2.61,6.06,2.37,7.84,2.06C20.85,60.78,57.48,40.79,57.48,31.48Z"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none">
|
|
2
|
+
<circle cx="12" cy="17" r="1" fill="var(--playpilot-warning)"/>
|
|
3
|
+
<path d="M12 10L12 14" stroke="var(--playpilot-warning)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M3.44722 18.1056L10.2111 4.57771C10.9482 3.10361 13.0518 3.10362 13.7889 4.57771L20.5528 18.1056C21.2177 19.4354 20.2507 21 18.7639 21H5.23607C3.7493 21 2.78231 19.4354 3.44722 18.1056Z" stroke="var(--playpilot-warning)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, removeAuthParamFromUrl } from '$lib/auth'
|
|
4
|
+
import { track } from '$lib/tracking'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* @param {Object} [options]
|
|
@@ -18,6 +20,10 @@ function fakeFetch({ response = '', status = 200, ok = true } = {}) {
|
|
|
18
20
|
)
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
vi.mock('$lib/tracking', () => ({
|
|
24
|
+
track: vi.fn(),
|
|
25
|
+
}))
|
|
26
|
+
|
|
21
27
|
describe('$lib/auth', () => {
|
|
22
28
|
beforeEach(() => {
|
|
23
29
|
vi.resetAllMocks()
|
|
@@ -67,6 +73,7 @@ describe('$lib/auth', () => {
|
|
|
67
73
|
let authorized = await authorize('https://example.com/some-path?articleReplacementEditToken=some-token')
|
|
68
74
|
|
|
69
75
|
expect(authorized).not.toBeTruthy()
|
|
76
|
+
expect(track).toHaveBeenCalled()
|
|
70
77
|
})
|
|
71
78
|
})
|
|
72
79
|
|
|
@@ -92,6 +99,16 @@ describe('$lib/auth', () => {
|
|
|
92
99
|
})
|
|
93
100
|
})
|
|
94
101
|
|
|
102
|
+
describe('getAuthToken', () => {
|
|
103
|
+
it('Should remove auth cookie when clicked', () => {
|
|
104
|
+
document.cookie = 'EncryptedToken=some-token=; expires=Thu, 01 Jan 2020 00:00:00 UTC; path='
|
|
105
|
+
|
|
106
|
+
removeAuthCookie()
|
|
107
|
+
|
|
108
|
+
expect(document.cookie).toBe('EncryptedToken=')
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
95
112
|
describe('isEditorialModeEnabled', () => {
|
|
96
113
|
it('Should return true if both search token and window token are given', async () => {
|
|
97
114
|
window.location.search = '?playpilot-editorial-mode=false'
|
|
@@ -112,4 +129,27 @@ describe('$lib/auth', () => {
|
|
|
112
129
|
expect(isEditorialModeEnabled()).toBe(true)
|
|
113
130
|
})
|
|
114
131
|
})
|
|
132
|
+
|
|
133
|
+
describe('removeAuthParamFromUrl', () => {
|
|
134
|
+
it('Should remove auth param from url', async () => {
|
|
135
|
+
window.location.search = '?articleReplacementEditToken=test'
|
|
136
|
+
removeAuthParamFromUrl()
|
|
137
|
+
|
|
138
|
+
expect(window.location.search).toBe('')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('Should leave other params intact', async () => {
|
|
142
|
+
window.location.search = '?articleReplacementEditToken=test&some_param=abc'
|
|
143
|
+
removeAuthParamFromUrl()
|
|
144
|
+
|
|
145
|
+
expect(window.location.search).toBe('?some_param=abc')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('Should not alter the url if the param is not present', async () => {
|
|
149
|
+
window.location.search = '?some_param=abc&some-other_param=123'
|
|
150
|
+
removeAuthParamFromUrl()
|
|
151
|
+
|
|
152
|
+
expect(window.location.search).toBe('?some_param=abc&some-other_param=123')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
115
155
|
})
|