@playpilot/tpi 1.0.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.
Files changed (101) hide show
  1. package/.github/workflows/tests.yml +22 -0
  2. package/.prettierignore +4 -0
  3. package/.prettierrc +16 -0
  4. package/README.md +38 -0
  5. package/dist/link-injections.js +7 -0
  6. package/eslint.config.js +33 -0
  7. package/index.html +11 -0
  8. package/jsconfig.json +19 -0
  9. package/package.json +35 -0
  10. package/src/app.d.ts +13 -0
  11. package/src/app.html +12 -0
  12. package/src/demo.spec.js +7 -0
  13. package/src/lib/api.js +160 -0
  14. package/src/lib/array.js +15 -0
  15. package/src/lib/auth.js +84 -0
  16. package/src/lib/constants.js +2 -0
  17. package/src/lib/enums/TrackingEvent.js +15 -0
  18. package/src/lib/fakeData.js +140 -0
  19. package/src/lib/genres.json +420 -0
  20. package/src/lib/global.css +37 -0
  21. package/src/lib/hash.js +15 -0
  22. package/src/lib/html.js +21 -0
  23. package/src/lib/index.js +1 -0
  24. package/src/lib/linkInjection.js +275 -0
  25. package/src/lib/search.js +24 -0
  26. package/src/lib/text.js +61 -0
  27. package/src/lib/tracking.js +32 -0
  28. package/src/lib/variables.css +16 -0
  29. package/src/main.js +45 -0
  30. package/src/routes/+layout.svelte +54 -0
  31. package/src/routes/+page.svelte +96 -0
  32. package/src/routes/components/AfterArticlePlaylinks.svelte +90 -0
  33. package/src/routes/components/ContextMenu.svelte +67 -0
  34. package/src/routes/components/Description.svelte +47 -0
  35. package/src/routes/components/Editorial/Alert.svelte +18 -0
  36. package/src/routes/components/Editorial/DragHandle.svelte +134 -0
  37. package/src/routes/components/Editorial/Editor.svelte +277 -0
  38. package/src/routes/components/Editorial/EditorItem.svelte +260 -0
  39. package/src/routes/components/Editorial/ManualInjection.svelte +192 -0
  40. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +132 -0
  41. package/src/routes/components/Editorial/Search/TitleSearch.svelte +176 -0
  42. package/src/routes/components/Editorial/Switch.svelte +76 -0
  43. package/src/routes/components/Editorial/TextInput.svelte +29 -0
  44. package/src/routes/components/Genres.svelte +41 -0
  45. package/src/routes/components/Icons/IconAlign.svelte +12 -0
  46. package/src/routes/components/Icons/IconBack.svelte +3 -0
  47. package/src/routes/components/Icons/IconBookmark.svelte +3 -0
  48. package/src/routes/components/Icons/IconChevron.svelte +18 -0
  49. package/src/routes/components/Icons/IconClose.svelte +3 -0
  50. package/src/routes/components/Icons/IconContinue.svelte +3 -0
  51. package/src/routes/components/Icons/IconDots.svelte +5 -0
  52. package/src/routes/components/Icons/IconEnlarge.svelte +12 -0
  53. package/src/routes/components/Icons/IconIMDb.svelte +3 -0
  54. package/src/routes/components/Icons/IconNewTab.svelte +3 -0
  55. package/src/routes/components/Modal.svelte +106 -0
  56. package/src/routes/components/Participants.svelte +44 -0
  57. package/src/routes/components/Playlinks.svelte +155 -0
  58. package/src/routes/components/Popover.svelte +95 -0
  59. package/src/routes/components/RoundButton.svelte +38 -0
  60. package/src/routes/components/SkeletonText.svelte +33 -0
  61. package/src/routes/components/Title.svelte +180 -0
  62. package/src/routes/components/TitleModal.svelte +24 -0
  63. package/src/routes/components/TitlePopover.svelte +17 -0
  64. package/src/tests/helpers.js +18 -0
  65. package/src/tests/lib/api.test.js +162 -0
  66. package/src/tests/lib/array.test.js +14 -0
  67. package/src/tests/lib/auth.test.js +115 -0
  68. package/src/tests/lib/hash.test.js +28 -0
  69. package/src/tests/lib/html.test.js +16 -0
  70. package/src/tests/lib/linkInjection.test.js +754 -0
  71. package/src/tests/lib/search.test.js +42 -0
  72. package/src/tests/lib/text.test.js +94 -0
  73. package/src/tests/lib/tracking.test.js +71 -0
  74. package/src/tests/routes/+page.test.js +109 -0
  75. package/src/tests/routes/components/AfterArticlePlaylinks.test.js +115 -0
  76. package/src/tests/routes/components/ContextMenu.test.js +37 -0
  77. package/src/tests/routes/components/Description.test.js +58 -0
  78. package/src/tests/routes/components/Editorial/Alert.test.js +17 -0
  79. package/src/tests/routes/components/Editorial/DragHandle.test.js +55 -0
  80. package/src/tests/routes/components/Editorial/Editor.test.js +64 -0
  81. package/src/tests/routes/components/Editorial/EditorItem.test.js +142 -0
  82. package/src/tests/routes/components/Editorial/ManualInjection.test.js +114 -0
  83. package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +63 -0
  84. package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +58 -0
  85. package/src/tests/routes/components/Editorial/Switch.test.js +60 -0
  86. package/src/tests/routes/components/Editorial/TextInput.test.js +30 -0
  87. package/src/tests/routes/components/Genres.test.js +37 -0
  88. package/src/tests/routes/components/Modal.test.js +84 -0
  89. package/src/tests/routes/components/Participants.test.js +33 -0
  90. package/src/tests/routes/components/Playlinks.test.js +101 -0
  91. package/src/tests/routes/components/Popover.test.js +66 -0
  92. package/src/tests/routes/components/RoundButton.test.js +35 -0
  93. package/src/tests/routes/components/SkeletonText.test.js +12 -0
  94. package/src/tests/routes/components/Title.test.js +82 -0
  95. package/src/tests/routes/components/TitleModal.test.js +33 -0
  96. package/src/tests/routes/components/TitlePopover.test.js +23 -0
  97. package/src/tests/setup.js +53 -0
  98. package/src/typedefs.js +72 -0
  99. package/static/favicon.png +0 -0
  100. package/svelte.config.js +13 -0
  101. package/vite.config.js +61 -0
@@ -0,0 +1,155 @@
1
+ <script>
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { track } from '$lib/tracking'
4
+ import IconContinue from './Icons/IconContinue.svelte'
5
+ import { getContext } from 'svelte'
6
+
7
+ /** @type {{ playlinks: PlaylinkData[], title: TitleData, list?: boolean }} */
8
+ const { playlinks, title, list = false } = $props()
9
+
10
+ const isModal = getContext('scope') === 'modal'
11
+
12
+ // Remove any playlinks without logos, as these are likely sub providers.
13
+ let filteredPlaylinks = $derived(playlinks.filter(playlink => !!playlink.logo_url))
14
+
15
+ const categoryStrings = {
16
+ SVOD: 'Stream',
17
+ BUY: 'Buy',
18
+ RENT: 'Rent',
19
+ }
20
+
21
+ /**
22
+ * @param {string} playlink Name of the clicked playlink
23
+ */
24
+ function onclick(playlink) {
25
+ track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.TitlePopoverPlaylinkClick, title, { playlink })
26
+ }
27
+ </script>
28
+
29
+ <h2>Where to stream online</h2>
30
+
31
+ <div class="playlinks" class:list>
32
+ {#each filteredPlaylinks as { name, url, logo_url, extra_info: { category } }}
33
+ <a href={url} target="_blank" class="playlink" onclick={() => onclick(name)} data-playlink={name} rel="sponsored">
34
+ <img src={logo_url} alt="" height="32" width="32" />
35
+
36
+ <div>
37
+ <span class="name">{name}</span>
38
+ <div class="category">{categoryStrings[category] || 'Stream'}</div>
39
+ </div>
40
+
41
+ {#if list}
42
+ <div class="arrow">
43
+ <IconContinue />
44
+ </div>
45
+ {/if}
46
+ </a>
47
+ {/each}
48
+
49
+ {#if !playlinks.length}
50
+ <div class="playlink empty">
51
+ This title is not currently available to stream. Save it and we'll notify you when it's available.
52
+ </div>
53
+ {/if}
54
+ </div>
55
+
56
+ <style>
57
+ h2 {
58
+ margin: 0;
59
+ color: var(--playpilot-playlinks-title-color, var(--playpilot-text-color-alt));
60
+ font-family: var(--playpilot-playlinks-title-font-family, inherit);
61
+ font-weight: var(--playpilot-playlinks-title-font-weight, lighter);
62
+ font-size: var(--playpilot-playlinks-title-font-size, 14px);
63
+ }
64
+
65
+ img {
66
+ border-radius: 0.5rem;
67
+ background: rgba(0, 0, 0, 0.25);
68
+ }
69
+
70
+ @media (min-width: 640px) {
71
+ img {
72
+ height: 2.5rem;
73
+ width: 2.5rem;
74
+ }
75
+
76
+ .list img {
77
+ height: 2rem;
78
+ width: 2rem;
79
+ }
80
+ }
81
+
82
+ .playlinks {
83
+ box-sizing: border-box;
84
+ display: flex;
85
+ gap: 0.5rem;
86
+ width: calc(100% + 2rem);
87
+ padding: 0.5rem 1rem;
88
+ margin: 0 -1rem;
89
+ overflow: auto;
90
+ scrollbar-width: none;
91
+ }
92
+
93
+ .playlinks::-webkit-scrollbar {
94
+ display: none;
95
+ }
96
+
97
+ .list {
98
+ display: grid;
99
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
100
+ }
101
+
102
+ .playlink {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 0.75rem;
106
+ padding: 0.75rem;
107
+ background: var(--playpilot-playlink-background, var(--playpilot-lighter));
108
+ box-shadow: var(--playpilot-playlink-shadow, var(--playpilot-shadow));
109
+ border-radius: var(--playpilot-playlink-border-radius, 0.5rem);
110
+ color: var(--playpilot-playlink-text-color, var(--playpilot-text-color-alt)) !important;
111
+ text-decoration: none !important;
112
+ white-space: nowrap;
113
+ font-size: 0.75rem;
114
+ line-height: 1;
115
+ }
116
+
117
+ .playlink img {
118
+ margin: 0;
119
+ }
120
+
121
+ .list .playlink {
122
+ padding: 0.5rem;
123
+ gap: 0.5rem;
124
+ }
125
+
126
+ .playlink:hover,
127
+ .playlink:active {
128
+ filter: var(--playpilot-playlink-hover-filter, brightness(1.1));
129
+ background: var(--playpilot-playlink-hover-background, var(--playpilot-playlink-background, var(--playpilot-lighter)));
130
+ text-decoration: none !important;
131
+ }
132
+
133
+ .name {
134
+ font-weight: var(--playpilot-playlink-font-weight, inherit);
135
+ font-family: var(--playpilot-playlink-font-family, inherit);
136
+ }
137
+
138
+ .category {
139
+ margin-top: 0.5rem;
140
+ font-size: 0.625rem;
141
+ color: var(--playpilot-playlink-category-text-color, var(--playpilot-text-color));
142
+ font-weight: var(--playpilot-playlink-category-font-weight, inherit);
143
+ font-family: var(--playpilot-playlink-category-font-family, inherit);
144
+ }
145
+
146
+ .empty {
147
+ white-space: initial;
148
+ line-height: 1.35;
149
+ }
150
+
151
+ .arrow {
152
+ margin-left: auto;
153
+ padding: 0 0.5rem;
154
+ }
155
+ </style>
@@ -0,0 +1,95 @@
1
+ <script>
2
+ import { onMount, setContext, tick } from 'svelte'
3
+ import { fly } from 'svelte/transition'
4
+
5
+ /** @type {{ children: import('svelte').Snippet, maxHeight?: number }} */
6
+ let { children, maxHeight = $bindable() } = $props()
7
+
8
+ setContext('scope', 'popover')
9
+
10
+ /** @type {HTMLElement | null} */
11
+ let element = $state(null)
12
+ let flip = $state(false)
13
+
14
+ onMount(async () => {
15
+ await tick()
16
+ positionElement()
17
+
18
+ await tick()
19
+ setMaxHeight()
20
+ })
21
+
22
+ /**
23
+ * Flip the element vertically if it doesn't fit on screen above the link.
24
+ * Move it to the left if it doesn't fit from the right side of the screen.
25
+ * Vertically it is always above or below, but horizontally is a gradual.
26
+ * @return {void}
27
+ */
28
+ function positionElement() {
29
+ if (!element) return
30
+
31
+ const { top, right } = element.getBoundingClientRect()
32
+ const offset = getOffset()
33
+
34
+ flip = top - offset < 0
35
+
36
+ const fromRight = window.innerWidth - right - offset
37
+ if (right + offset > window.innerWidth) element.style.left = `${fromRight}px`
38
+ }
39
+
40
+ /**
41
+ * Set the max height of the dialog so that it always fits on screen, even after flipping.
42
+ * @return {void}
43
+ */
44
+ function setMaxHeight() {
45
+ if (!element) return
46
+
47
+ const { top } = element.getBoundingClientRect()
48
+ const offset = getOffset()
49
+
50
+ maxHeight = Math.min(window.innerHeight - top - offset, window.innerHeight * 0.8)
51
+ }
52
+
53
+ function getOffset() {
54
+ return parseFloat(getComputedStyle(document.documentElement).fontSize) // 1 rem
55
+ }
56
+ </script>
57
+
58
+ <div class="popover" class:flip bind:this={element} style:--max-height={maxHeight ? maxHeight + 'px' : null} tabindex="-1" aria-hidden="true">
59
+ <div class="dialog" transition:fly={{ duration: 100, y: 10 }}>
60
+ {@render children()}
61
+ </div>
62
+ </div>
63
+
64
+ <style>
65
+ .popover {
66
+ --offset: 0.5rem;
67
+ position: absolute;
68
+ top: calc((var(--offset) - 1px) * -1); /* Add 1 pixel to account for rounding errors */
69
+ left: 0;
70
+ width: calc(100vw - 2rem);
71
+ max-width: 20rem;
72
+ padding: var(--offset) 0;
73
+ transform: translateY(calc(-100% + var(--offset)));
74
+ z-index: 999;
75
+ }
76
+
77
+ .popover.flip {
78
+ top: auto;
79
+ bottom: calc(var(--offset) + 1px); /* Add 1 pixel to account for rounding errors */
80
+ transform: translateY(calc(100% + var(--offset)));
81
+ }
82
+
83
+ /* This is a separate element so that the parent can have an offset while still keeping the mouse inside */
84
+ .dialog {
85
+ cursor: default;
86
+ position: relative;
87
+ border-radius: var(--playpilot-popover-border-radius, 1rem);
88
+ background: var(--playpilot-popover-background, var(--playpilot-light));
89
+ box-shadow: var(--playpilot-popover-shadow, var(--playpilot-shadow-large));
90
+ max-height: min(var(--max-height), 35rem);
91
+ scrollbar-width: thin;
92
+ overflow-y: overlay;
93
+ overflow-x: hidden;
94
+ }
95
+ </style>
@@ -0,0 +1,38 @@
1
+ <script>
2
+ /**
3
+ * @typedef {Object} Props
4
+ * @property {string} [size]
5
+ * @property {import('svelte').Snippet | null} [children]
6
+ * @property {() => void} [onclick]
7
+ */
8
+ /** @type {Props & { [key: string]: any }} */
9
+ const { children = null, size = '2rem', onclick = () => null, ...rest } = $props()
10
+ </script>
11
+
12
+ <svelte:element this={rest.href ? 'a' : 'button'} role={rest.href ? 'link' : 'button'} {onclick} class="button" style:--size={size} {...rest}>
13
+ {@render children?.()}
14
+ </svelte:element>
15
+
16
+ <style>
17
+ .button {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ flex: 0 0 var(--size);
22
+ width: var(--size);
23
+ height: var(--size);
24
+ border-radius: 50%;
25
+ border: 0;
26
+ padding: 0;
27
+ background: var(--playpilot-button-background, var(--playpilot-content));
28
+ box-shadow: var(--playpilot-button-shadow, var(--playpilot-shadow));
29
+ color: var(--playpilot-button-text-color, var(--playpilot-text-color-alt));
30
+ cursor: pointer;
31
+ transition: transform 50ms;
32
+ }
33
+
34
+ .button:hover {
35
+ filter: brightness(1.2);
36
+ transform: scale(1.05);
37
+ }
38
+ </style>
@@ -0,0 +1,33 @@
1
+ <script>
2
+ /** @type {{ lines?: number, max?: number, min?: any }} */
3
+ const { lines = 4, max = 100, min = (max / 100) * 70 } = $props()
4
+ </script>
5
+
6
+ <!-- eslint-disable-next-line no-unused-vars -->
7
+ {#each { length: lines } as _}
8
+ <div class="skeleton" style:width="{Math.floor(Math.random() * (max - min) + min)}%">&nbsp;</div>
9
+ {/each}
10
+
11
+ <style>
12
+ .skeleton {
13
+ position: relative;
14
+ font-size: inherit;
15
+ line-height: inherit;
16
+ }
17
+
18
+ .skeleton:last-child {
19
+ margin-bottom: 0;
20
+ }
21
+
22
+ .skeleton::before {
23
+ display: block;
24
+ position: absolute;
25
+ top: 0;
26
+ left: 0;
27
+ width: 100%;
28
+ height: calc(100% - 0.5rem);
29
+ border-radius: var(--playpilot-skeleton-border-radius, 2em);
30
+ background: var(--playpilot-lighter);
31
+ content: '';
32
+ }
33
+ </style>
@@ -0,0 +1,180 @@
1
+ <script>
2
+ import Genres from './Genres.svelte'
3
+ import Playlinks from './Playlinks.svelte'
4
+ import Description from './Description.svelte'
5
+ import Participants from './Participants.svelte'
6
+ import IconIMDb from './Icons/IconIMDb.svelte'
7
+
8
+ /** @type {{ title: TitleData, small?: boolean, compact?: boolean }} */
9
+ const { title, small = false, compact = false } = $props()
10
+ </script>
11
+
12
+ <div class="content" class:small class:compact data-playpilot-link-injections-title data-playpilot-original-title={title.original_title}>
13
+ <header class="header">
14
+ {#if !compact}
15
+ <div class="top">
16
+ <img class="poster" src={title.standing_poster} alt="Movie poster for '{title.title}'" />
17
+ </div>
18
+ {/if}
19
+
20
+ <h1>{title.title}</h1>
21
+
22
+ <div class="info">
23
+ <div class="imdb">
24
+ <IconIMDb />
25
+ {title.imdb_score}
26
+ </div>
27
+
28
+ <Genres genres={title.genres} />
29
+
30
+ <div>{title.year}</div>
31
+ <div class="capitalize">{title.type}</div>
32
+
33
+ {#if title.length}
34
+ <div>{title.length} minutes</div>
35
+ {/if}
36
+ </div>
37
+ </header>
38
+
39
+ <main class="main">
40
+ <Playlinks playlinks={title.providers} {title} list />
41
+
42
+ {#if !small}
43
+ <Description text={title.description} blurb={title.blurb} />
44
+
45
+ {#if title.participants?.length}
46
+ <Participants participants={title.participants} />
47
+ {/if}
48
+ {/if}
49
+
50
+ </main>
51
+ </div>
52
+
53
+ <div class="background" class:faded={compact}>
54
+ <img src={title.medium_poster} alt="" />
55
+ </div>
56
+
57
+ <style>
58
+ h1 {
59
+ margin: 0.5rem 0;
60
+ font-family: var(--playpilot-detail-title-font-family, inherit);
61
+ font-weight: var(--playpilot-detail-title-font-weight, lighter);
62
+ font-size: var(--playpilot-detail-title-font-size, 1.5rem);
63
+ color: var(--playpilot-detail-title-text-color, #fff);
64
+ }
65
+
66
+ .small h1 {
67
+ font-size: var(--playpilot-detail-title-font-size-small, 1.125rem);
68
+ }
69
+
70
+ .compact h1 {
71
+ margin-top: 0;
72
+ }
73
+
74
+ .content {
75
+ z-index: 1;
76
+ position: relative;
77
+ padding: 1rem;
78
+ color: var(--playpilot-detail-text-color, var(--playpilot-text-color));
79
+ font-family: var(--playpilot-detail-font-family, var(--playpilot-font-family));
80
+ font-size: var(--playpilot-detail-font-size, 14px);
81
+ }
82
+
83
+ .content.small {
84
+ font-size: var(--playpilot-detail-font-size, 12px);
85
+ line-height: 1.45;
86
+ padding-bottom: 0.5rem;
87
+ }
88
+
89
+ .header {
90
+ padding: 2rem 0 1rem;
91
+ }
92
+
93
+ .small .header {
94
+ padding-top: 1rem;
95
+ }
96
+
97
+ .compact .header {
98
+ padding: 0 0 1rem;
99
+ }
100
+
101
+ .top {
102
+ display: flex;
103
+ justify-content: space-between;
104
+ align-items: flex-end;
105
+ }
106
+
107
+ .poster {
108
+ display: block;
109
+ width: 4.5rem;
110
+ margin: 0;
111
+ aspect-ratio: 2 / 3;
112
+ border-radius: var(--playpilot-detail-image-border-radius, 0.5rem);
113
+ background: var(--playpilot-detail-image-background, var(--playpilot-content));
114
+ box-shadow: var(--playpilot-detail-image-shadow, var(--playpilot-shadow));
115
+ }
116
+
117
+ .info {
118
+ display: flex;
119
+ flex-wrap: wrap;
120
+ align-items: center;
121
+ gap: 0.5rem 1rem;
122
+ }
123
+
124
+ .small .info {
125
+ gap: 0.5rem 0.75rem;
126
+ }
127
+
128
+ .imdb {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 0.25rem;
132
+ }
133
+
134
+ .imdb :global(svg) {
135
+ margin-top: -0.125rem;
136
+ }
137
+
138
+ .small .imdb :global(svg) {
139
+ margin-top: 0;
140
+ width: 0.75rem;
141
+ height: 0.75rem;
142
+ }
143
+
144
+ .background {
145
+ position: absolute;
146
+ top: 0;
147
+ left: 0;
148
+ width: 100%;
149
+ height: 12rem;
150
+ overflow: hidden;
151
+ background: var(--playpilot-detail-background, var(--playpilot-lighter));
152
+ }
153
+
154
+ .background::before {
155
+ content: "";
156
+ display: block;
157
+ position: absolute;
158
+ top: 0;
159
+ right: 0;
160
+ bottom: 0;
161
+ left: 0;
162
+ background: linear-gradient(to top, var(--playpilot-detail-background, var(--playpilot-light)), transparent 40%);
163
+ }
164
+
165
+ .background img {
166
+ width: 100%;
167
+ height: 100%;
168
+ object-fit: cover;
169
+ object-position: center;
170
+ margin: 0;
171
+ }
172
+
173
+ .faded {
174
+ opacity: 0.15;
175
+ }
176
+
177
+ .capitalize {
178
+ text-transform: capitalize;
179
+ }
180
+ </style>
@@ -0,0 +1,24 @@
1
+ <script>
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { track } from '$lib/tracking'
4
+ import Modal from './Modal.svelte'
5
+ import Title from './Title.svelte'
6
+
7
+ /** @type {{ onclose: () => void, title: TitleData }} */
8
+ const { onclose, title } = $props()
9
+
10
+ track(TrackingEvent.TitleModalView, title)
11
+
12
+ let hasTrackedScrolling = false
13
+
14
+ function onscroll() {
15
+ if (hasTrackedScrolling) return
16
+
17
+ track(TrackingEvent.TitleModalScroll, title)
18
+ hasTrackedScrolling = true
19
+ }
20
+ </script>
21
+
22
+ <Modal {onclose} {onscroll}>
23
+ <Title {title} />
24
+ </Modal>
@@ -0,0 +1,17 @@
1
+ <script>
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { track } from '$lib/tracking'
4
+ import Popover from './Popover.svelte'
5
+ import Title from './Title.svelte'
6
+
7
+ /** @type {{ title: TitleData }} */
8
+ const { title } = $props()
9
+
10
+ let maxHeight = $state(0)
11
+
12
+ track(TrackingEvent.TitlePopoverView, title)
13
+ </script>
14
+
15
+ <Popover bind:maxHeight>
16
+ <Title {title} small compact={maxHeight < 250} />
17
+ </Popover>
@@ -0,0 +1,18 @@
1
+ import { vi } from 'vitest'
2
+
3
+ /**
4
+ * @param {Object} [options]
5
+ * @param {any} [options.response]
6
+ * @param {number} [options.status]
7
+ * @param {boolean} [options.ok]
8
+ */
9
+ export function fakeFetch({ response = '', status = 200, ok = true } = {}) {
10
+ // @ts-ignore
11
+ global.fetch = vi.fn(() =>
12
+ Promise.resolve({
13
+ ok,
14
+ status,
15
+ json: () => response,
16
+ }),
17
+ )
18
+ }