@playpilot/tpi 5.32.0 → 5.33.0-beta.explore.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 +10 -11
- package/package.json +1 -1
- package/src/lib/api/titles.ts +13 -1
- package/src/lib/data/translations.ts +5 -0
- package/src/lib/explore.ts +26 -0
- package/src/lib/fakeData.ts +1 -0
- package/src/lib/images/titles-list.webp +0 -0
- package/src/lib/modal.ts +7 -6
- package/src/lib/trailer.ts +22 -0
- package/src/lib/types/api.d.ts +6 -0
- package/src/lib/types/title.d.ts +4 -1
- package/src/routes/+page.svelte +5 -1
- package/src/routes/components/Ads/TopScroll.svelte +4 -18
- package/src/routes/components/Button.svelte +73 -0
- package/src/routes/components/Explore/Explore.svelte +178 -0
- package/src/routes/components/Explore/ExploreCallToAction.svelte +58 -0
- package/src/routes/components/Explore/ExploreModal.svelte +15 -0
- package/src/routes/components/Explore/Filter.svelte +3 -0
- package/src/routes/components/Explore/Search.svelte +54 -0
- package/src/routes/components/Icons/IconClose.svelte +9 -1
- package/src/routes/components/Icons/IconPlay.svelte +3 -0
- package/src/routes/components/Icons/IconSearch.svelte +3 -0
- package/src/routes/components/ListTitle.svelte +3 -5
- package/src/routes/components/ListTitleSkeleton.svelte +42 -0
- package/src/routes/components/Modal.svelte +5 -23
- package/src/routes/components/ParticipantModal.svelte +1 -11
- package/src/routes/components/Share.svelte +5 -23
- package/src/routes/components/Title.svelte +22 -22
- package/src/routes/components/TitleModal.svelte +4 -1
- package/src/routes/components/Trailer.svelte +18 -0
- package/src/routes/components/YouTubeEmbedOverlay.svelte +96 -0
- package/src/routes/elements/+page.svelte +28 -1
- package/src/tests/lib/api/ads.test.js +0 -1
- package/src/tests/lib/api/titles.test.js +55 -0
- package/src/tests/lib/explore.test.js +49 -0
- package/src/tests/lib/trailer.test.js +56 -0
- package/src/tests/routes/components/Button.test.js +28 -0
- package/src/tests/routes/components/Explore/Explore.test.js +133 -0
- package/src/tests/routes/components/Explore/Search.test.js +26 -0
- package/src/tests/routes/components/Share.test.js +12 -12
- package/src/tests/routes/components/Title.test.js +13 -0
- package/src/tests/routes/components/Trailer.test.js +20 -0
- package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +31 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="18" height="18" viewBox="0 0 19 19">
|
|
2
|
+
<path fill="currentColor" d="M16.593,2.407a8.155,8.155,0,0,0-11.569,0A8.166,8.166,0,0,0,4.18,12.983L.38,16.8a1.3,1.3,0,0,0,0,1.837A1.372,1.372,0,0,0,1.288,19a1.3,1.3,0,0,0,.908-.38L6.017,14.8A8.172,8.172,0,0,0,16.593,2.407ZM15.327,12.688A6.4,6.4,0,1,1,17.184,8.17,6.324,6.324,0,0,1,15.327,12.688Z" />
|
|
3
|
+
</svg>
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
align-items: center;
|
|
91
91
|
width: 100%;
|
|
92
92
|
background: theme(list-item-background, lighter);
|
|
93
|
-
padding: margin(0.5);
|
|
93
|
+
padding: theme(list-item-padding, margin(0.5));
|
|
94
94
|
border: 0;
|
|
95
95
|
border-radius: theme(list-item-border-radius, border-radius);
|
|
96
96
|
text-decoration: none;
|
|
@@ -98,10 +98,8 @@
|
|
|
98
98
|
|
|
99
99
|
&:hover {
|
|
100
100
|
filter: brightness(theme(list-item-hover-filter-brightness, hover-filter-brightness));
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
&:last-child {
|
|
104
|
-
border-bottom: 0;
|
|
101
|
+
background: theme(list-item-hover-background, lighter);
|
|
102
|
+
box-shadow: theme(list-item-hover-shadow, none);
|
|
105
103
|
}
|
|
106
104
|
}
|
|
107
105
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<div class="skeleton" data-testid="skeleton">
|
|
2
|
+
<div class="poster"></div>
|
|
3
|
+
|
|
4
|
+
<div class="lines">
|
|
5
|
+
<div class="line" style:width="40%"></div>
|
|
6
|
+
<div class="line" style:width="60%"></div>
|
|
7
|
+
<div class="line" style:width="80%"></div>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<style lang="scss">
|
|
12
|
+
.skeleton {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
width: 100%;
|
|
16
|
+
gap: margin(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.poster {
|
|
20
|
+
flex: 0 0 auto;
|
|
21
|
+
height: auto;
|
|
22
|
+
align-self: start;
|
|
23
|
+
width: margin(4.125);
|
|
24
|
+
aspect-ratio: 2/3;
|
|
25
|
+
border-radius: theme(detail-image-border-radius, border-radius);
|
|
26
|
+
background: theme(content);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.lines {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
gap: margin(0.5);
|
|
33
|
+
width: 100%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.line {
|
|
37
|
+
width: 60%;
|
|
38
|
+
height: theme(font-size-base);
|
|
39
|
+
background: theme(content);
|
|
40
|
+
border-radius: 5rem;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
import SwipeHandle from './SwipeHandle.svelte'
|
|
7
7
|
import { onMount, setContext, type Snippet } from 'svelte'
|
|
8
8
|
import { prefersReducedMotion } from 'svelte/motion'
|
|
9
|
-
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
10
|
-
import { SplitTest } from '$lib/enums/SplitTest'
|
|
11
9
|
import { mobileBreakpoint } from '$lib/constants'
|
|
12
10
|
import { destroyAllModals, getPreviousModal, goBackToPreviousModal } from '$lib/modal'
|
|
13
11
|
import { focustrap } from '$lib/actions/focustrap'
|
|
@@ -34,7 +32,6 @@
|
|
|
34
32
|
onclose = () => null,
|
|
35
33
|
}: Props = $props()
|
|
36
34
|
|
|
37
|
-
const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
38
35
|
const historyHash = '#playpilot'
|
|
39
36
|
|
|
40
37
|
let windowWidth = $state(0)
|
|
@@ -95,7 +92,7 @@
|
|
|
95
92
|
onhashchange={close}
|
|
96
93
|
bind:innerWidth={windowWidth} />
|
|
97
94
|
|
|
98
|
-
<div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble
|
|
95
|
+
<div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} use:focustrap class:has-bubble={!!bubble} class:has-prepend={!!prepend}>
|
|
99
96
|
{#if prepend}
|
|
100
97
|
<div class="prepend" transition:scaleOrFly|global={{ y: -10 }} data-view-transition-new="playpilot-title-extra">
|
|
101
98
|
{@render prepend()}
|
|
@@ -103,7 +100,7 @@
|
|
|
103
100
|
{/if}
|
|
104
101
|
|
|
105
102
|
{#if bubble}
|
|
106
|
-
<div class="bubble"
|
|
103
|
+
<div class="bubble" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new="playpilot-title-extra">
|
|
107
104
|
{@render bubble()}
|
|
108
105
|
</div>
|
|
109
106
|
{/if}
|
|
@@ -285,28 +282,13 @@
|
|
|
285
282
|
.bubble {
|
|
286
283
|
z-index: 1;
|
|
287
284
|
position: relative;
|
|
288
|
-
width:
|
|
285
|
+
width: 100%;
|
|
289
286
|
max-width: $max-width;
|
|
290
|
-
margin:
|
|
287
|
+
margin: auto 0 0;
|
|
291
288
|
|
|
292
289
|
@include desktop() {
|
|
293
290
|
width: 100%;
|
|
294
|
-
margin: 0
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
&.inline {
|
|
298
|
-
width: 100%;
|
|
299
|
-
margin: auto 0 0;
|
|
300
|
-
|
|
301
|
-
@include desktop() {
|
|
302
|
-
margin-top: 0;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
.prepend + & {
|
|
307
|
-
&:not(.inline) {
|
|
308
|
-
margin-top: 0;
|
|
309
|
-
}
|
|
291
|
+
margin-top: 0;
|
|
310
292
|
}
|
|
311
293
|
}
|
|
312
294
|
</style>
|
|
@@ -12,17 +12,7 @@
|
|
|
12
12
|
initialScrollPosition?: number
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const { participant =
|
|
16
|
-
sid: 'pr5C5W',
|
|
17
|
-
name: 'James Franco',
|
|
18
|
-
birth_date: '1978-04-19',
|
|
19
|
-
death_date: null,
|
|
20
|
-
jobs: ['actor'],
|
|
21
|
-
image: 'https://hips.hearstapps.com/hmg-prod/images/gettyimages-161098947-square.jpg',
|
|
22
|
-
image_uuid: '08ed2fac357011eb87470aff12c0f5c9',
|
|
23
|
-
gender: 'Male',
|
|
24
|
-
bio: 'Aenean feugiat nec odio at venenatis. Integer porta neque metus, a sollicitudin dolor dapibus et. In sollicitudin nulla eget ultricies porttitor. Nulla facilisi. Sed turpis orci, facilisis efficitur neque in, ultrices ultricies purus. Mauris nec augue a nisi imperdiet semper ut nec tellus. Donec at tristique odio. Etiam luctus eget metus non mattis. Integer imperdiet in elit eu varius. Donec ornare, nibh vitae accumsan consequat, lacus nulla elementum sapien, a scelerisque tellus augue ac erat. Aenean finibus fringilla magna, ac laoreet nisl convallis vel. Proin laoreet ex ac augue maximus, nec gravida tortor pharetra.\nCurabitur maximus dui sed risus placerat pharetra vitae ut orci. Proin sodales enim a elit euismod, a varius sem suscipit. Vivamus eu magna cursus, fringilla est in, mollis nunc. Mauris fringilla eleifend nibh, eget auctor lectus bibendum non. Praesent sed elit ipsum. Donec nunc dolor, sagittis hendrerit gravida et, lacinia sed metus. Morbi tempus mi massa. In hac habitasse platea dictumst. Suspendisse aliquet tincidunt lectus ut elementum.',
|
|
25
|
-
}, initialScrollPosition = 0 }: Props = $props()
|
|
15
|
+
const { participant, initialScrollPosition = 0 }: Props = $props()
|
|
26
16
|
|
|
27
17
|
let windowWidth = $state(0)
|
|
28
18
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import IconShare from './Icons/IconShare.svelte'
|
|
10
10
|
import IconLink from './Icons/IconLink.svelte'
|
|
11
11
|
import IconEmail from './Icons/IconEmail.svelte'
|
|
12
|
+
import Button from './Button.svelte'
|
|
12
13
|
import { onMount } from 'svelte'
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
@@ -55,9 +56,9 @@
|
|
|
55
56
|
<svelte:window onclick={() => showContextMenu = false} />
|
|
56
57
|
|
|
57
58
|
<div class="share">
|
|
58
|
-
<
|
|
59
|
-
<IconShare />
|
|
60
|
-
</
|
|
59
|
+
<Button onclick={toggle}>
|
|
60
|
+
<IconShare /> Share
|
|
61
|
+
</Button>
|
|
61
62
|
|
|
62
63
|
{#if showContextMenu}
|
|
63
64
|
<div class="context-menu" transition:scale={{ duration: 50, start: 0.85 }}>
|
|
@@ -79,30 +80,11 @@
|
|
|
79
80
|
position: relative;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
.button {
|
|
83
|
-
display: flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
justify-content: center;
|
|
86
|
-
cursor: pointer;
|
|
87
|
-
appearance: none;
|
|
88
|
-
border: 0;
|
|
89
|
-
border-radius: margin(3);
|
|
90
|
-
aspect-ratio: 1 / 1;
|
|
91
|
-
background: transparent;
|
|
92
|
-
color: theme(detail-text-color-alt, text-color-alt);
|
|
93
|
-
|
|
94
|
-
&:hover {
|
|
95
|
-
color: theme(detail-text-color, text-color);
|
|
96
|
-
background: theme(share-button-hover-background, content);
|
|
97
|
-
box-shadow: 0 0 0 2px theme(share-button-hover-background, content);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
83
|
.context-menu {
|
|
102
84
|
z-index: 10;
|
|
103
85
|
position: absolute;
|
|
104
86
|
bottom: calc(100% + margin(0.5));
|
|
105
|
-
|
|
87
|
+
left: 0;
|
|
106
88
|
max-width: margin(15);
|
|
107
89
|
border-radius: $border-radius;
|
|
108
90
|
background: theme(detail-background, lighter);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import SimilarRail from './Rails/SimilarRail.svelte'
|
|
8
8
|
import TitlePoster from './TitlePoster.svelte'
|
|
9
9
|
import Share from './Share.svelte'
|
|
10
|
+
import Trailer from './Trailer.svelte'
|
|
10
11
|
import { t } from '$lib/localization'
|
|
11
12
|
import type { TitleData } from '$lib/types/title'
|
|
12
13
|
import { heading } from '$lib/actions/heading'
|
|
@@ -36,26 +37,28 @@
|
|
|
36
37
|
|
|
37
38
|
<div class="heading" use:heading={2} class:truncate={small} id="title">{title.title}</div>
|
|
38
39
|
|
|
39
|
-
<div class="
|
|
40
|
-
<div class="
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</div>
|
|
40
|
+
<div class="info">
|
|
41
|
+
<div class="imdb">
|
|
42
|
+
<IconIMDb />
|
|
43
|
+
{title.imdb_score}
|
|
44
|
+
</div>
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
<Genres genres={title.genres} />
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
<div>{title.year}</div>
|
|
49
|
+
<div class="capitalize">{t(`Type: ${title.type}`)}</div>
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
{#if !small && title.length}
|
|
52
|
+
<div>{title.length} {t('Minutes')}</div>
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
<div class="actions">
|
|
57
|
+
<!-- !! Button is temporarily always visible while embeddable_url is not yet available -->
|
|
58
|
+
{#if true || title.embeddable_url}
|
|
59
|
+
<Trailer title={title} />
|
|
57
60
|
<Share title={title.title} url={titleUrl(title)} />
|
|
58
|
-
|
|
61
|
+
{/if}
|
|
59
62
|
</div>
|
|
60
63
|
</div>
|
|
61
64
|
|
|
@@ -146,11 +149,6 @@
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
.row {
|
|
150
|
-
display: flex;
|
|
151
|
-
align-items: flex-start;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
152
|
.info {
|
|
155
153
|
display: flex;
|
|
156
154
|
flex-wrap: wrap;
|
|
@@ -182,8 +180,10 @@
|
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
.
|
|
186
|
-
|
|
183
|
+
.actions {
|
|
184
|
+
display: flex;
|
|
185
|
+
gap: margin(0.5);
|
|
186
|
+
margin-top: margin(0.5);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
.background {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import Title from './Title.svelte'
|
|
9
9
|
import TopScroll from './Ads/TopScroll.svelte'
|
|
10
10
|
import Display from './Ads/Display.svelte'
|
|
11
|
+
import ExploreCallToAction from './Explore/ExploreCallToAction.svelte'
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
title: TitleData
|
|
@@ -40,6 +41,8 @@
|
|
|
40
41
|
{#snippet bubble()}
|
|
41
42
|
{#if topScrollAd}
|
|
42
43
|
<TopScroll campaign={topScrollAd} />
|
|
44
|
+
{:else}
|
|
45
|
+
<ExploreCallToAction />
|
|
43
46
|
{/if}
|
|
44
47
|
{/snippet}
|
|
45
48
|
|
|
@@ -49,6 +52,6 @@
|
|
|
49
52
|
{/if}
|
|
50
53
|
{/snippet}
|
|
51
54
|
|
|
52
|
-
<Modal {onscroll} {initialScrollPosition} prepend={displayAd ? prepend : null}
|
|
55
|
+
<Modal {onscroll} {initialScrollPosition} {bubble} prepend={displayAd ? prepend : null} tall>
|
|
53
56
|
<Title {title} />
|
|
54
57
|
</Modal>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '$lib/localization'
|
|
3
|
+
import { openTrailerOverlay } from '$lib/trailer'
|
|
4
|
+
import type { TitleData } from '$lib/types/title'
|
|
5
|
+
import Button from './Button.svelte'
|
|
6
|
+
import IconPlay from './Icons/IconPlay.svelte'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: TitleData
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { title }: Props = $props()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<Button onclick={() => openTrailerOverlay(title)}>
|
|
16
|
+
<IconPlay />
|
|
17
|
+
{t('Watch Trailer')}
|
|
18
|
+
</Button>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition'
|
|
3
|
+
import IconClose from './Icons/IconClose.svelte'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
embeddable_url: string,
|
|
7
|
+
onclose: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { embeddable_url, onclose }: Props = $props()
|
|
11
|
+
|
|
12
|
+
const videoId = $derived(getVideoId(embeddable_url))
|
|
13
|
+
|
|
14
|
+
// Gets the YouTube ID from a url, can be a large number of differnet formats
|
|
15
|
+
// https://stackoverflow.com/a/54200105/1665157
|
|
16
|
+
function getVideoId(url: string): string | null {
|
|
17
|
+
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
|
|
18
|
+
const match = url.match(regExp)
|
|
19
|
+
|
|
20
|
+
return match?.[7] || null
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="overlay" transition:fade={{ duration: 100 }}>
|
|
25
|
+
{#if videoId}
|
|
26
|
+
<iframe width="600" height="338" src="https://www.youtube.com/embed/{videoId}?autoplay=true" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
|
27
|
+
{:else}
|
|
28
|
+
Something went wrong
|
|
29
|
+
{/if}
|
|
30
|
+
|
|
31
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
32
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
33
|
+
<div class="backdrop" onclick={onclose} data-testid="backdrop"></div>
|
|
34
|
+
|
|
35
|
+
<button class="close" onclick={onclose} aria-label="Close">
|
|
36
|
+
<IconClose size={24} />
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<style lang="scss">
|
|
41
|
+
iframe {
|
|
42
|
+
z-index: 1;
|
|
43
|
+
position: relative;
|
|
44
|
+
display: block;
|
|
45
|
+
width: 95vmin;
|
|
46
|
+
height: auto;
|
|
47
|
+
aspect-ratio: 16/9;
|
|
48
|
+
box-shadow: 0 0 margin(4) rgba(255, 255, 255, 0.15);
|
|
49
|
+
background: black;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.overlay {
|
|
53
|
+
z-index: 2147483647; // As high as she goes
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
position: fixed;
|
|
59
|
+
top: 0;
|
|
60
|
+
right: 0;
|
|
61
|
+
bottom: 0;
|
|
62
|
+
left: 0;
|
|
63
|
+
background: theme(detail-backdrop, rgba(0, 0, 0, 0.95));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.backdrop {
|
|
67
|
+
z-index: 0;
|
|
68
|
+
position: fixed;
|
|
69
|
+
top: 0;
|
|
70
|
+
right: 0;
|
|
71
|
+
bottom: 0;
|
|
72
|
+
left: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.close {
|
|
76
|
+
appearance: none;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
position: fixed;
|
|
79
|
+
top: margin(2);
|
|
80
|
+
right: margin(2);
|
|
81
|
+
padding: 0;
|
|
82
|
+
margin: 0;
|
|
83
|
+
border: 0;
|
|
84
|
+
background: transparent;
|
|
85
|
+
color: white;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
|
|
88
|
+
&:hover {
|
|
89
|
+
transform: scale(1.1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&:active {
|
|
93
|
+
transform: scale(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { browser } from '$app/environment'
|
|
3
3
|
import { title, linkInjections, campaign, participants } from '$lib/fakeData'
|
|
4
4
|
import { openModal } from '$lib/modal'
|
|
5
|
+
import { insertExplore } from '$lib/explore'
|
|
5
6
|
import Disclaimer from '../components/Ads/Disclaimer.svelte'
|
|
6
7
|
import Display from '../components/Ads/Display.svelte'
|
|
7
8
|
import TopScroll from '../components/Ads/TopScroll.svelte'
|
|
@@ -16,9 +17,13 @@
|
|
|
16
17
|
import Title from '../components/Title.svelte'
|
|
17
18
|
import TitlePopover from '../components/TitlePopover.svelte'
|
|
18
19
|
import Tooltip from '../components/Tooltip.svelte'
|
|
20
|
+
import Explore from '../components/Explore/Explore.svelte'
|
|
19
21
|
|
|
20
22
|
// @ts-ignore
|
|
21
|
-
if (browser) window.PlayPilotLinkInjections = {}
|
|
23
|
+
if (browser) window.PlayPilotLinkInjections = { token: 'ZoAL14yqzevMyQiwckbvyetOkeIUeEDN' }
|
|
24
|
+
|
|
25
|
+
// Used to re-render some elements with a key
|
|
26
|
+
let renderKey = Math.random()
|
|
22
27
|
</script>
|
|
23
28
|
|
|
24
29
|
<h1>Elements</h1>
|
|
@@ -153,6 +158,28 @@
|
|
|
153
158
|
</div>
|
|
154
159
|
</div>
|
|
155
160
|
|
|
161
|
+
<h2>Explore</h2>
|
|
162
|
+
|
|
163
|
+
<div class="group">
|
|
164
|
+
<div class="item">
|
|
165
|
+
<button onclick={() => renderKey = Math.random()}>Rerender</button>
|
|
166
|
+
|
|
167
|
+
{#key renderKey}
|
|
168
|
+
<Explore />
|
|
169
|
+
{/key}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="item">
|
|
173
|
+
<button onclick={() => openModal({ type: 'explore' })}>Show explore modal</button>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="item">
|
|
177
|
+
<button onclick={insertExplore}>Insert explore component</button>
|
|
178
|
+
|
|
179
|
+
<div data-playpilot-explore></div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
156
183
|
<style lang="scss">
|
|
157
184
|
@import url('$lib/scss/global.scss');
|
|
158
185
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { api } from '$lib/api/api'
|
|
4
|
+
import { fetchSimilarTitles, fetchTitles } from '$lib/api/titles'
|
|
5
|
+
import { title } from '$lib/fakeData'
|
|
6
|
+
import { getApiToken } from '$lib/token'
|
|
7
|
+
|
|
8
|
+
vi.mock('$lib/token', () => ({
|
|
9
|
+
getApiToken: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('$lib/api/api', () => ({
|
|
13
|
+
api: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('$lib/api/ads', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetAllMocks()
|
|
19
|
+
vi.mocked(getApiToken).mockReturnValue('some-token')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('fetchTitles', () => {
|
|
23
|
+
it('Should call api with given parameters and return response', async () => {
|
|
24
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
25
|
+
|
|
26
|
+
const response = await fetchTitles({ some: 'thing' })
|
|
27
|
+
|
|
28
|
+
expect(api).toHaveBeenCalledWith('/titles/browse?api-token=some-token&some=thing')
|
|
29
|
+
expect(response).toEqual({ results: [title] })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('Should throw when api returns error', async () => {
|
|
33
|
+
vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
|
|
34
|
+
|
|
35
|
+
await expect(async () => await fetchTitles()).rejects.toThrow()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('fetchSimilarTitles', () => {
|
|
40
|
+
it('Should call api with with sid for given title and return array of titles', async () => {
|
|
41
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
42
|
+
|
|
43
|
+
const response = await fetchSimilarTitles(title)
|
|
44
|
+
|
|
45
|
+
expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&related_to_sid=${title.sid}`)
|
|
46
|
+
expect(response).toEqual([title])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Should throw when api returns error', async () => {
|
|
50
|
+
vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
|
|
51
|
+
|
|
52
|
+
await expect(async () => await fetchSimilarTitles(title)).rejects.toThrow()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { destroyExplore, insertExplore } from '$lib/explore'
|
|
3
|
+
import { mount, unmount } from 'svelte'
|
|
4
|
+
import { waitFor } from '@testing-library/svelte'
|
|
5
|
+
|
|
6
|
+
vi.mock('svelte', () => ({
|
|
7
|
+
mount: vi.fn(() => true),
|
|
8
|
+
unmount: vi.fn(),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
describe('explore.js', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
destroyExplore()
|
|
14
|
+
|
|
15
|
+
vi.resetAllMocks()
|
|
16
|
+
vi.mocked(mount).mockReturnValue({})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('insertExplore', () => {
|
|
20
|
+
it('Should not call mount when no target element is found', () => {
|
|
21
|
+
insertExplore()
|
|
22
|
+
|
|
23
|
+
expect(mount).not.toHaveBeenCalled()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('Should call mount when target is found', () => {
|
|
27
|
+
document.body.innerHTML = '<div data-playpilot-explore></div>'
|
|
28
|
+
|
|
29
|
+
insertExplore()
|
|
30
|
+
|
|
31
|
+
expect(mount).toHaveBeenCalled()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('destroyExplore', () => {
|
|
36
|
+
it('Should not call unmount if component was not previously mounted', () => {
|
|
37
|
+
destroyExplore()
|
|
38
|
+
|
|
39
|
+
expect(unmount).not.toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('Should call unmount if component was previously mounted', () => {
|
|
43
|
+
insertExplore()
|
|
44
|
+
destroyExplore()
|
|
45
|
+
|
|
46
|
+
expect(unmount).toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { closeCurrentModal, destroyAllModals, destroyCurrentModal, getAllModals, getPreviousModal, goBackToPreviousModal, openModal } from '$lib/modal'
|
|
3
|
+
import { linkInjections, title } from '$lib/fakeData'
|
|
4
|
+
import { mount, unmount } from 'svelte'
|
|
5
|
+
import ParticipantModal from '../../routes/components/ParticipantModal.svelte'
|
|
6
|
+
import TitleModal from '../../routes/components/TitleModal.svelte'
|
|
7
|
+
import { closeTrailerOverlay, openTrailerOverlay } from '$lib/trailer'
|
|
8
|
+
|
|
9
|
+
vi.mock('svelte', () => ({
|
|
10
|
+
mount: vi.fn(),
|
|
11
|
+
unmount: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const titleWithTrailer = { ...title, embeddable_url: 'abc' }
|
|
15
|
+
|
|
16
|
+
describe('modal.js', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('openTrailerOverlay', () => {
|
|
22
|
+
it('Should call mount with given title embeddable_url', () => {
|
|
23
|
+
openTrailerOverlay(titleWithTrailer)
|
|
24
|
+
|
|
25
|
+
expect(mount).toHaveBeenCalledWith(
|
|
26
|
+
expect.any(Function),
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
target: expect.anything(),
|
|
29
|
+
props: expect.objectContaining({
|
|
30
|
+
onclose: expect.any(Function),
|
|
31
|
+
embeddable_url: titleWithTrailer.embeddable_url,
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('closeTrailerOverlay', () => {
|
|
39
|
+
it('Should call unmount if component was previously mounted', () => {
|
|
40
|
+
vi.mocked(mount).mockReturnValueOnce({})
|
|
41
|
+
|
|
42
|
+
openTrailerOverlay(titleWithTrailer)
|
|
43
|
+
closeTrailerOverlay()
|
|
44
|
+
|
|
45
|
+
expect(unmount).toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('Should not call unmount if component was not previously mounted', () => {
|
|
49
|
+
vi.mocked(mount).mockReturnValueOnce({})
|
|
50
|
+
|
|
51
|
+
closeTrailerOverlay()
|
|
52
|
+
|
|
53
|
+
expect(unmount).not.toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import Button from '../../../routes/components/Button.svelte'
|
|
5
|
+
|
|
6
|
+
describe('Button.svelte', () => {
|
|
7
|
+
it('Should use filled class by default', () => {
|
|
8
|
+
const { getByRole } = render(Button)
|
|
9
|
+
|
|
10
|
+
expect(getByRole('button').classList).toContain('filled')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('Should use border class when border variant is given', () => {
|
|
14
|
+
const { getByRole } = render(Button, { variant: 'border' })
|
|
15
|
+
|
|
16
|
+
expect(getByRole('button').classList).not.toContain('filled')
|
|
17
|
+
expect(getByRole('button').classList).toContain('border')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('Should fire given onclick function on click', async () => {
|
|
21
|
+
const onclick = vi.fn()
|
|
22
|
+
const { getByRole } = render(Button, { onclick })
|
|
23
|
+
|
|
24
|
+
await fireEvent.click(getByRole('button'))
|
|
25
|
+
|
|
26
|
+
expect(onclick).toHaveBeenCalled()
|
|
27
|
+
})
|
|
28
|
+
})
|