@playpilot/tpi 3.1.0 → 3.2.0-beta.2

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.
Files changed (42) hide show
  1. package/dist/link-injections.js +8 -7
  2. package/package.json +1 -1
  3. package/src/lib/actions/heading.ts +11 -0
  4. package/src/lib/api.ts +1 -3
  5. package/src/lib/auth.ts +13 -1
  6. package/src/lib/constants.ts +2 -0
  7. package/src/lib/enums/TrackingEvent.ts +1 -0
  8. package/src/lib/linkInjection.ts +43 -32
  9. package/src/lib/scss/_mixins.scss +8 -0
  10. package/src/lib/scss/global.scss +6 -6
  11. package/src/lib/scss/variables.scss +2 -0
  12. package/src/lib/stores/organization.ts +4 -0
  13. package/src/lib/tracking.ts +14 -1
  14. package/src/lib/types/injection.d.ts +5 -0
  15. package/src/routes/+layout.svelte +6 -2
  16. package/src/routes/+page.svelte +31 -13
  17. package/src/routes/components/Description.svelte +2 -3
  18. package/src/routes/components/Editorial/AIIndicator.svelte +85 -91
  19. package/src/routes/components/Editorial/Alert.svelte +12 -2
  20. package/src/routes/components/Editorial/Editor.svelte +82 -61
  21. package/src/routes/components/Editorial/EditorItem.svelte +32 -7
  22. package/src/routes/components/Editorial/ManualInjection.svelte +10 -9
  23. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +14 -0
  24. package/src/routes/components/Editorial/ResizeHandle.svelte +1 -1
  25. package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +7 -5
  26. package/src/routes/components/Icons/IconWarning.svelte +5 -0
  27. package/src/routes/components/Modal.svelte +2 -0
  28. package/src/routes/components/Playlinks.svelte +12 -11
  29. package/src/routes/components/Popover.svelte +2 -0
  30. package/src/routes/components/Title.svelte +3 -2
  31. package/src/routes/components/TitlePopover.svelte +1 -1
  32. package/src/tests/lib/auth.test.js +31 -1
  33. package/src/tests/lib/linkInjection.test.js +87 -48
  34. package/src/tests/lib/tracking.test.js +61 -1
  35. package/src/tests/routes/+page.test.js +94 -4
  36. package/src/tests/routes/components/Editorial/AiIndicator.test.js +28 -42
  37. package/src/tests/routes/components/Editorial/Alert.test.js +10 -3
  38. package/src/tests/routes/components/Editorial/Editor.test.js +15 -0
  39. package/src/tests/routes/components/Editorial/EditorItem.test.js +32 -7
  40. package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +13 -1
  41. package/src/tests/routes/components/Title.test.js +2 -2
  42. package/svelte.config.js +1 -0
@@ -1,75 +1,62 @@
1
1
  <script lang="ts">
2
- import type { LinkInjection } from '$lib/types/injection'
3
2
  import IconAi from '../Icons/IconAi.svelte'
4
3
 
5
4
  interface Props {
6
- // eslint-disable-next-line no-unused-vars
7
- onadd: (injections: LinkInjection[]) => void
8
- /** Used to guesstimate the load times. */
9
- htmlString?: string
5
+ aiRunning?: boolean
6
+ automationEnabled?: boolean,
7
+ message?: string,
8
+ percentage?: number,
10
9
  }
11
10
 
12
- const { onadd, htmlString = '' }: Props = $props()
13
-
14
- // Guesstimate AI load times based on the given text length.
15
- // The value will always be between 1 and 10 minutes.
16
- const fakeLoadTimes = $derived(Math.min(Math.max(htmlString.length * 30, 60000), 600000))
17
-
18
- let running = $state(true)
19
- let injectionsToBeInserted: LinkInjection[] = $state([])
20
- let dismissed = $state(false)
21
-
22
- /**
23
- * This is called from the Editor component when AI links are ready.
24
- * From here we determine what to show the user. Either we show them an option
25
- * to inject new links, or we tell them no new injections were found.
26
- */
27
- export function notifyUserOfNewState(injections: LinkInjection[]) {
28
- running = false
29
- injectionsToBeInserted = injections
30
- }
11
+ const { aiRunning = false, automationEnabled = false, message = '', percentage = 0 }: Props = $props()
31
12
  </script>
32
13
 
33
- {#if !dismissed}
34
- <div class="ai-indicator" class:running>
35
- <div class="content">
36
- <div class="icon">
37
- <IconAi />
38
- </div>
39
-
40
- <div>
41
- {#if running}
42
- AI links are currently processing. This can take several minutes.<br>
43
- You can add manual links while this is ongoing.
44
- {:else if injectionsToBeInserted?.length}
45
- AI links are ready.
46
- <strong>{injectionsToBeInserted.length} New {injectionsToBeInserted.length > 1 ? 'links were' : 'link was'} found.</strong>
47
- New links will be used next time you refresh the page, or you can insert them now.
48
-
49
- <button class="button" onclick={() => { onadd(injectionsToBeInserted); dismissed = true }}>
50
- Add AI links
51
- </button>
52
- {:else}
53
- AI links finished running, but no new links were found.
54
-
55
- <button class="button" onclick={() => dismissed = true}>Dismiss</button>
56
- {/if}
57
- </div>
58
-
59
- {#if running}
60
- <div class="loading-bar" data-testid="loading-bar" style:animation-duration="{fakeLoadTimes}ms"></div>
61
- {/if}
14
+ <div class="ai-indicator" class:running={aiRunning}>
15
+ <div class="content">
16
+ <div class="icon">
17
+ <IconAi />
62
18
  </div>
63
19
 
64
- <div class="border">
65
- {#if running}
66
- <div class="animator"></div>
20
+ <div>
21
+ {#if !automationEnabled}
22
+ <strong>AI processing is disabled.</strong> Enable AI from the <a href="https://partner.playpilot.net">Partner Portal</a>
23
+ {:else if aiRunning}
24
+ AI links are currently processing. This can take several minutes. We'll automatically insert all found injections once ready.
25
+
26
+ <div class="message">
27
+ {message}
28
+
29
+ <span class="ellipses">
30
+ {#each { length: 3 }}
31
+ <span>.</span>
32
+ {/each}
33
+ </span>
34
+ </div>
35
+
36
+ <div class="loading-bar">
37
+ <div class="loading-bar-progress">
38
+ <div class="loading-bar-fill" data-testid="loading-bar" style:width="{Math.max(percentage, 3)}%"></div>
39
+ </div>
40
+
41
+ <div class="loading-bar-label">{percentage}%</div>
42
+ </div>
67
43
  {/if}
68
44
  </div>
45
+
46
+ </div>
47
+
48
+ <div class="border">
49
+ {#if aiRunning}
50
+ <div class="animator" data-test-id="animator"></div>
51
+ {/if}
69
52
  </div>
70
- {/if}
53
+ </div>
71
54
 
72
55
  <style lang="scss">
56
+ a {
57
+ color: currentColor;
58
+ }
59
+
73
60
  .ai-indicator {
74
61
  position: relative;
75
62
  margin: 0 margin(0.5);
@@ -90,6 +77,12 @@
90
77
  overflow: hidden;
91
78
  }
92
79
 
80
+ .message {
81
+ display: flex;
82
+ margin-top: margin(0.25);
83
+ font-style: italic;
84
+ }
85
+
93
86
  .icon {
94
87
  color: var(--playpilot-green);
95
88
  }
@@ -129,49 +122,50 @@
129
122
  filter: blur(5rem);
130
123
  }
131
124
 
132
- .button {
133
- display: block;
134
- appearance: none;
135
- padding: 0 margin(0.25);
136
- margin: margin(0.25) 0 0;
137
- border: 1px solid currentColor;
138
- border-radius: 0.25rem;
139
- background: transparent;
140
- color: var(--playpilot-green);
141
- font-family: var(--playpilot-font-family);
142
- cursor: pointer;
125
+ .loading-bar {
126
+ display: grid;
127
+ grid-template-columns: auto 3em;
128
+ align-items: center;
129
+ }
143
130
 
144
- &:hover {
145
- color: white;
146
- }
131
+ .loading-bar-progress {
132
+ height: 0.5em;
133
+ border-radius: 0.25rem;
134
+ background: var(--playpilot-dark);
147
135
  }
148
136
 
149
- @keyframes fake-load {
150
- 0% {
151
- width: 0%;
152
- }
137
+ .loading-bar-fill {
138
+ height: 100%;
139
+ border-radius: 0.25rem;
140
+ background: var(--playpilot-green);
141
+ transition: width 200ms;
142
+ }
153
143
 
154
- 2% {
155
- width: 20%;
156
- }
144
+ .loading-bar-label {
145
+ text-align: right;
146
+ }
157
147
 
158
- 70% {
159
- width: 85%;
148
+ @keyframes fade {
149
+ 0%,
150
+ 100% {
151
+ opacity: 0;
160
152
  }
161
153
 
162
- 100% {
163
- width: 90%;
154
+ 25%,
155
+ 75% {
156
+ opacity: 1;
164
157
  }
165
158
  }
166
159
 
167
- .loading-bar {
168
- position: absolute;
169
- bottom: 0;
170
- left: 0;
171
- height: 0.15em;
172
- border-radius: 0 0.25rem 0.25rem 0;
173
- background: linear-gradient(to right, var(--playpilot-light), var(--playpilot-green));
174
- animation: fake-load forwards;
175
- animation-duration: 20000ms;
160
+ .ellipses {
161
+ span {
162
+ animation: fade 2000ms infinite;
163
+
164
+ @for $i from 1 through 3 {
165
+ &:nth-child(#{$i}) {
166
+ animation-delay: 200ms * $i;
167
+ }
168
+ }
169
+ }
176
170
  }
177
171
  </style>
@@ -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>
@@ -9,20 +9,32 @@
9
9
  import { saveLinkInjections } from '$lib/api'
10
10
  import { untrack } from 'svelte'
11
11
  import AIIndicator from './AIIndicator.svelte'
12
- import { isEquivalentInjection, isValidInjection } from '$lib/linkInjection'
13
12
  import type { Position } from '$lib/types/position'
14
13
  import type { LinkInjection } from '$lib/types/injection'
15
14
  import { track } from '$lib/tracking'
16
15
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
16
+ import { heading } from '$lib/actions/heading'
17
17
 
18
18
  interface Props {
19
19
  linkInjections: LinkInjection[],
20
20
  htmlString?: string,
21
21
  loading?: boolean,
22
- aiRunning?: boolean
22
+ injectionsEnabled?: boolean,
23
+ aiStatus: {
24
+ automationEnabled?: boolean,
25
+ aiRunning?: boolean,
26
+ message?: string,
27
+ percentage?: number
28
+ }
23
29
  }
24
30
 
25
- let { linkInjections = $bindable(), htmlString = '', loading = false, aiRunning = false }: Props = $props()
31
+ let {
32
+ linkInjections = $bindable(),
33
+ htmlString = '',
34
+ loading = false,
35
+ injectionsEnabled = false,
36
+ aiStatus = {},
37
+ }: Props = $props()
26
38
 
27
39
  const editorPositionKey = 'editor-position'
28
40
  const editorHeightKey = 'editor-height'
@@ -35,19 +47,19 @@
35
47
  let hasError = $state(false)
36
48
  let scrollDistance = $state(0)
37
49
  let initialStateString = $state('')
38
- let aIIndicator = $state()
39
50
 
40
51
  const linkInjectionsString = $derived(JSON.stringify(linkInjections))
41
- const hasChanged = $derived(initialStateString !== linkInjectionsString)
52
+ const hasChanged = $derived(initialStateString && initialStateString !== linkInjectionsString)
42
53
  // Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
43
54
  const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
44
55
  const sortedInjections = $derived(sortInjections(filteredInjections))
56
+ const { automationEnabled = false, aiRunning = false } = $derived(aiStatus)
45
57
 
46
58
  $effect(() => {
47
59
  if (loading) return
48
60
 
49
61
  untrack(() => {
50
- initialStateString = linkInjectionsString
62
+ requestAnimationFrame(() => initialStateString = linkInjectionsString)
51
63
  trackInjectionsCount()
52
64
  })
53
65
  })
@@ -120,41 +132,27 @@
120
132
 
121
133
  track(TrackingEvent.TotalInjectionsCount, null, payload)
122
134
  }
123
-
124
- /**
125
- * This is called from outside when new AI links are ready.
126
- * We only pass on links that do not already exist.
127
- */
128
- export function requestNewAIInjections(injections: LinkInjection[]): void {
129
- const newInjections = injections.filter(injection => {
130
- if (!isValidInjection(injection)) return
131
- return !linkInjections.some((i) => isEquivalentInjection(injection, i))
132
- })
133
-
134
- // @ts-ignore
135
- aIIndicator.notifyUserOfNewState(newInjections)
136
- }
137
135
  </script>
138
136
 
139
137
  <section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
140
- <header class="header">
141
- {#if editorElement}
142
- {#if !loading}
143
- <div class="handle">
144
- <ResizeHandle element={editorElement} {height} onchange={(height) => saveLocalStorage(editorHeightKey, JSON.stringify(height))} />
145
- </div>
146
- {/if}
138
+ {#if editorElement && !loading}
139
+ <div class="handles">
140
+ <div class="handle">
141
+ <ResizeHandle element={editorElement} {height} onchange={(height) => saveLocalStorage(editorHeightKey, JSON.stringify(height))} />
142
+ </div>
147
143
 
148
144
  <div class="handle">
149
145
  <DragHandle element={editorElement} {position} limit={{ x: 16, y: 16 }} onchange={(position) => saveLocalStorage(editorPositionKey, JSON.stringify(position))} />
150
146
  </div>
151
- {/if}
147
+ </div>
148
+ {/if}
152
149
 
153
- <h1>Playlinks</h1>
150
+ <header class="header">
151
+ <div class="heading" use:heading>Playlinks</div>
154
152
 
155
153
  {#if loading}
156
154
  <div class="loading">Loading...</div>
157
- {:else}
155
+ {:else if !aiRunning}
158
156
  <div class="bubble" aria-label="{filteredInjections.length} found playlinks">
159
157
  {filteredInjections.length}
160
158
  </div>
@@ -165,32 +163,43 @@
165
163
  {/if}
166
164
  </header>
167
165
 
168
- {#if !loading && aiRunning}
169
- <AIIndicator {htmlString} bind:this={aIIndicator} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
170
- {/if}
171
-
172
166
  {#if !loading}
167
+ {#if !injectionsEnabled}
168
+ <div class="alert">
169
+ <Alert type="warning">
170
+ <strong>Playlinks are currently not published.</strong> Visitors to this page will not see any of the injected links.
171
+ Publish playlinks from the <a href="https://partner.playpilot.net">Partner Portal</a>
172
+ </Alert>
173
+ </div>
174
+ {/if}
175
+
176
+ {#if aiRunning || !automationEnabled}
177
+ <AIIndicator {...aiStatus} />
178
+ {/if}
179
+
173
180
  {#if hasError}
174
181
  <div class="error" transition:slide|global={{ duration: 150 }}>
175
182
  <Alert>Something went wrong, check your links below.</Alert>
176
183
  </div>
177
184
  {/if}
178
185
 
179
- <div class="items">
180
- {#each sortedInjections as linkInjection (linkInjection.key)}
181
- <!-- We want to bind to the original object, not the derived object, so we get the index of the injection in the original object by it's key -->
182
- {@const index = linkInjections.findIndex((i) => i.key === linkInjection.key)}
186
+ {#if !aiRunning}
187
+ <div class="items">
188
+ {#each sortedInjections as linkInjection (linkInjection.key)}
189
+ <!-- We want to bind to the original object, not the derived object, so we get the index of the injection in the original object by it's key -->
190
+ {@const index = linkInjections.findIndex((i) => i.key === linkInjection.key)}
183
191
 
184
- <EditorItem bind:linkInjection={linkInjections[index]} onremove={() => removeInjection(linkInjection.key)} onhighlight={scrollToItem} />
185
- {:else}
186
- <div class="empty">No links available. Add links manually by clicking the + button and select text to add a link to.</div>
187
- {/each}
188
- </div>
192
+ <EditorItem bind:linkInjection={linkInjections[index]} onremove={() => removeInjection(linkInjection.key)} onhighlight={scrollToItem} />
193
+ {:else}
194
+ <div class="empty">No links available. Add links manually by clicking the + button and select text to add a link to.</div>
195
+ {/each}
196
+ </div>
189
197
 
190
- {#if hasChanged && linkInjections.length}
191
- <button class="save" disabled={saving} onclick={save} in:fade={{ duration: 100 }}>
192
- {saving ? 'Saving...' : 'Save links'}
193
- </button>
198
+ {#if hasChanged && linkInjections.length}
199
+ <button class="save" disabled={saving} onclick={save} in:fade={{ duration: 100 }}>
200
+ {saving ? 'Saving...' : 'Save links'}
201
+ </button>
202
+ {/if}
194
203
  {/if}
195
204
  {/if}
196
205
 
@@ -208,14 +217,6 @@
208
217
  </section>
209
218
 
210
219
  <style lang="scss">
211
- h1 {
212
- margin: 0 margin(0.75) 0 0;
213
- color: var(--playpilot-text-color);
214
- font-size: margin(1.25);
215
- font-weight: normal;
216
- line-height: normal;
217
- }
218
-
219
220
  .editor {
220
221
  z-index: 2147483646; // 1 less than as high as she goes;
221
222
  display: flex;
@@ -225,7 +226,7 @@
225
226
  right: margin(1);
226
227
  width: 100%;
227
228
  max-width: margin(22);
228
- height: min(50vh, margin(50));
229
+ height: min(70vh, margin(40));
229
230
  min-height: 10rem;
230
231
  margin: 0;
231
232
  padding: margin(1);
@@ -238,6 +239,8 @@
238
239
  overflow-y: auto;
239
240
  overflow-x: hidden;
240
241
  line-height: normal;
242
+
243
+ @include reset-svg();
241
244
  }
242
245
 
243
246
  .panel-open {
@@ -254,6 +257,13 @@
254
257
  font-size: margin(0.85);
255
258
  }
256
259
 
260
+ .handles {
261
+ z-index: 20;
262
+ position: sticky;
263
+ top: margin(-1);
264
+ margin: margin(-1) margin(-1) 0;
265
+ }
266
+
257
267
  .handle {
258
268
  opacity: 0;
259
269
  transition: opacity 150ms;
@@ -264,21 +274,28 @@
264
274
  }
265
275
 
266
276
  .header {
277
+ @extend .handles;
267
278
  z-index: 5;
268
- position: sticky;
269
- top: margin(-1);
270
- margin: margin(-1) margin(-1) 0;
279
+ display: flex;
280
+ align-items: center;
271
281
  padding: margin(1) margin(1) margin(1) margin(1.5);
282
+ margin: 0 margin(-1) 0;
272
283
  border: 0;
273
284
  background: var(--playpilot-dark);
274
- display: flex;
275
- align-items: center;
276
285
 
277
286
  .loading & {
278
287
  margin: margin(-1);
279
288
  }
280
289
  }
281
290
 
291
+ .heading {
292
+ margin: 0 margin(0.75) 0 0;
293
+ color: var(--playpilot-text-color);
294
+ font-size: margin(1.25);
295
+ font-weight: normal;
296
+ line-height: normal;
297
+ }
298
+
282
299
  .bubble {
283
300
  appearance: none;
284
301
  width: margin(1.5);
@@ -340,6 +357,10 @@
340
357
  margin-top: margin(0.5);
341
358
  }
342
359
 
360
+ .alert {
361
+ margin: 0 margin(0.5) margin(0.5);
362
+ }
363
+
343
364
  .panel {
344
365
  z-index: 10;
345
366
  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
- return Array.from(document.querySelectorAll(`[data-playpilot-injection-key="${key}"]`))
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>A match was found, but the link could not be injected.</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
 
@@ -11,6 +11,7 @@
11
11
  import { getLinkInjectionsParentElement } from '$lib/linkInjection'
12
12
  import type { LinkInjection } from '$lib/types/injection'
13
13
  import type { TitleData } from '$lib/types/title'
14
+ import { heading } from '$lib/actions/heading'
14
15
 
15
16
  interface Props {
16
17
  htmlString: string
@@ -159,7 +160,7 @@
159
160
  <section class="layout">
160
161
  <div class="header">
161
162
  <RoundButton onclick={onclose} size="24px"><IconBack /></RoundButton>
162
- <h2>Add Playlink manually</h2>
163
+ <div class="heading" use:heading={2}>Add Playlink manually</div>
163
164
  </div>
164
165
 
165
166
  <p>Highlight the text section in your post that you want to turn into a Playlink.</p>
@@ -186,14 +187,6 @@
186
187
  </section>
187
188
 
188
189
  <style lang="scss">
189
- h2 {
190
- margin: 0;
191
- color: var(--playpilot-text-color);
192
- font-size: margin(1);
193
- line-height: normal;
194
- font-weight: normal;
195
- }
196
-
197
190
  p,
198
191
  label {
199
192
  font-size: margin(0.75);
@@ -220,6 +213,14 @@
220
213
  margin-bottom: margin(1);
221
214
  }
222
215
 
216
+ .heading {
217
+ margin: 0;
218
+ color: var(--playpilot-text-color);
219
+ font-size: margin(1);
220
+ line-height: normal;
221
+ font-weight: normal;
222
+ }
223
+
223
224
  .error {
224
225
  margin-top: margin(0.5);
225
226
  }
@@ -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>