@playpilot/tpi 5.15.0 → 5.16.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.
- package/dist/link-injections.js +11 -10
- package/package.json +2 -1
- package/release.js +3 -1
- package/src/lib/enums/SplitTest.ts +4 -0
- package/src/lib/fakeData.ts +70 -0
- package/src/lib/linkInjection.ts +11 -29
- package/src/lib/modal.ts +97 -0
- package/src/lib/playlink.ts +4 -1
- package/src/lib/types/participant.d.ts +14 -0
- package/src/lib/types/title.d.ts +3 -1
- package/src/routes/components/Description.svelte +1 -0
- package/src/routes/components/Icons/IconArrow.svelte +22 -0
- package/src/routes/components/Icons/IconClose.svelte +1 -1
- package/src/routes/components/Icons/IconIMDb.svelte +9 -1
- package/src/routes/components/ListTitle.svelte +204 -0
- package/src/routes/components/Modal.svelte +63 -13
- package/src/routes/components/Participant.svelte +92 -0
- package/src/routes/components/ParticipantModal.svelte +31 -0
- package/src/routes/components/PlaylinkIcon.svelte +41 -0
- package/src/routes/components/PlaylinkLabel.svelte +37 -0
- package/src/routes/components/Playlinks.svelte +1 -3
- package/src/routes/components/Rails/ParticipantsRail.svelte +56 -0
- package/src/routes/components/Rails/Rail.svelte +91 -0
- package/src/routes/components/Rails/SimilarRail.svelte +16 -0
- package/src/routes/components/Rails/TitlesRail.svelte +95 -0
- package/src/routes/components/Tabs.svelte +47 -0
- package/src/routes/components/Title.svelte +20 -17
- package/src/routes/components/TitleModal.svelte +3 -3
- package/src/routes/components/TitlePoster.svelte +30 -0
- package/src/tests/lib/linkInjection.test.js +10 -22
- package/src/tests/lib/modal.test.js +148 -0
- package/src/tests/lib/playlink.test.js +25 -10
- package/src/tests/routes/components/ListTitle.test.js +84 -0
- package/src/tests/routes/components/Modal.test.js +51 -19
- package/src/tests/routes/components/PlaylinkIcon.test.js +27 -0
- package/src/tests/routes/components/PlaylinkLabel.test.js +19 -0
- package/src/tests/routes/components/Rails/ParticipantsRail.test.js +41 -0
- package/src/tests/routes/components/Rails/TitleRail.test.js +38 -0
- package/src/tests/routes/components/Title.test.js +6 -0
- package/src/tests/routes/components/TitlePoster.test.js +20 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { fade, fly, scale, type TransitionConfig } from 'svelte/transition'
|
|
3
3
|
import IconClose from './Icons/IconClose.svelte'
|
|
4
|
+
import IconArrow from './Icons/IconArrow.svelte'
|
|
4
5
|
import RoundButton from './RoundButton.svelte'
|
|
5
6
|
import SwipeHandle from './SwipeHandle.svelte'
|
|
6
7
|
import { onMount, setContext, type Snippet } from 'svelte'
|
|
@@ -8,23 +9,35 @@
|
|
|
8
9
|
import { isInSplitTestVariant } from '$lib/splitTest'
|
|
9
10
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
10
11
|
import { mobileBreakpoint } from '$lib/constants'
|
|
12
|
+
import { destroyAllModals, getPreviousModal, goBackToPreviousModal } from '$lib/modal'
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
children: Snippet
|
|
14
16
|
bubble?: Snippet | null
|
|
15
17
|
prepend?: Snippet | null
|
|
16
18
|
tall?: boolean
|
|
17
|
-
|
|
19
|
+
closeButtonStyle?: 'shadow' | 'flat'
|
|
20
|
+
initialScrollPosition?: number
|
|
18
21
|
onscroll?: () => void
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
children,
|
|
26
|
+
bubble,
|
|
27
|
+
prepend,
|
|
28
|
+
tall = false,
|
|
29
|
+
closeButtonStyle = 'shadow',
|
|
30
|
+
initialScrollPosition = 0,
|
|
31
|
+
onscroll = () => null,
|
|
32
|
+
}: Props = $props()
|
|
22
33
|
|
|
23
34
|
const inlineBubble = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
24
35
|
|
|
25
36
|
let windowWidth = $state(0)
|
|
37
|
+
let modalElement: HTMLElement | null = $state(null)
|
|
26
38
|
let dialogElement: HTMLElement | null = $state(null)
|
|
27
39
|
let dialogOffset: number = $state(0)
|
|
40
|
+
let hasPreviousModal = $state(false)
|
|
28
41
|
|
|
29
42
|
const isMobile = $derived(windowWidth < mobileBreakpoint)
|
|
30
43
|
|
|
@@ -37,9 +50,18 @@
|
|
|
37
50
|
const baseOverflowStyle = document.body.style.overflowY
|
|
38
51
|
document.body.style.overflowY = 'hidden'
|
|
39
52
|
|
|
53
|
+
hasPreviousModal = !!getPreviousModal()
|
|
54
|
+
|
|
55
|
+
requestAnimationFrame(setInitialScrollPosition)
|
|
56
|
+
|
|
40
57
|
return () => document.body.style.overflowY = baseOverflowStyle || ''
|
|
41
58
|
})
|
|
42
59
|
|
|
60
|
+
function setInitialScrollPosition(): void {
|
|
61
|
+
const scrollableElement = window.innerWidth > mobileBreakpoint ? modalElement : dialogElement
|
|
62
|
+
scrollableElement?.scrollTo(0, initialScrollPosition)
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
function scaleOrFly(node: Element, options: { y: number } = { y: 0 }): TransitionConfig {
|
|
44
66
|
if (prefersReducedMotion.current) return fade(node, { duration: 0 })
|
|
45
67
|
|
|
@@ -48,9 +70,9 @@
|
|
|
48
70
|
}
|
|
49
71
|
</script>
|
|
50
72
|
|
|
51
|
-
<svelte:window onkeydown={({ key }) => { if (key === 'Escape')
|
|
73
|
+
<svelte:window onkeydown={({ key }) => { if (key === 'Escape') destroyAllModals() }} bind:innerWidth={windowWidth} />
|
|
52
74
|
|
|
53
|
-
<div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
|
|
75
|
+
<div class="modal" style:--dialog-offset="{dialogOffset}px" transition:fade|global={{ duration: 150 }} bind:this={modalElement} class:has-bubble={!!bubble && inlineBubble} class:has-prepend={!!prepend}>
|
|
54
76
|
{#if prepend}
|
|
55
77
|
<div class="prepend" transition:scaleOrFly|global={{ y: -10 }}>
|
|
56
78
|
{@render prepend()}
|
|
@@ -65,13 +87,21 @@
|
|
|
65
87
|
|
|
66
88
|
{#if isMobile}
|
|
67
89
|
<div class="swipe-handle" transition:scaleOrFly|global>
|
|
68
|
-
<SwipeHandle target={dialogElement!} onpassed={() =>
|
|
90
|
+
<SwipeHandle target={dialogElement!} onpassed={() => destroyAllModals()} />
|
|
69
91
|
</div>
|
|
70
92
|
{/if}
|
|
71
93
|
|
|
72
94
|
<div class="dialog" class:tall {onscroll} bind:this={dialogElement} role="dialog" aria-labelledby="title" transition:scaleOrFly|global={{ y: window.innerHeight }} data-view-transition-new>
|
|
73
|
-
|
|
74
|
-
<
|
|
95
|
+
{#if hasPreviousModal}
|
|
96
|
+
<div class="close back {closeButtonStyle}">
|
|
97
|
+
<RoundButton onclick={() => goBackToPreviousModal()} aria-label="Back">
|
|
98
|
+
<IconArrow direction="left" />
|
|
99
|
+
</RoundButton>
|
|
100
|
+
</div>
|
|
101
|
+
{/if}
|
|
102
|
+
|
|
103
|
+
<div class="close {closeButtonStyle}">
|
|
104
|
+
<RoundButton onclick={() => destroyAllModals()} aria-label="Close">
|
|
75
105
|
<IconClose />
|
|
76
106
|
</RoundButton>
|
|
77
107
|
</div>
|
|
@@ -81,7 +111,7 @@
|
|
|
81
111
|
|
|
82
112
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
83
113
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
84
|
-
<div class="backdrop" onclick={() =>
|
|
114
|
+
<div class="backdrop" onclick={() => destroyAllModals()}></div>
|
|
85
115
|
</div>
|
|
86
116
|
|
|
87
117
|
<style lang="scss">
|
|
@@ -98,9 +128,9 @@
|
|
|
98
128
|
align-items: center;
|
|
99
129
|
overflow: auto;
|
|
100
130
|
top: 0;
|
|
131
|
+
right: 0;
|
|
132
|
+
bottom: 0;
|
|
101
133
|
left: 0;
|
|
102
|
-
width: 100%;
|
|
103
|
-
height: 100%;
|
|
104
134
|
background: var(--playpilot-detail-backdrop, rgba(0, 0, 0, 0.65));
|
|
105
135
|
|
|
106
136
|
@media (min-width: $max-width) {
|
|
@@ -118,8 +148,9 @@
|
|
|
118
148
|
width: 100%;
|
|
119
149
|
max-width: $max-width;
|
|
120
150
|
max-height: 80vh;
|
|
121
|
-
overflow: auto;
|
|
122
151
|
margin-top: auto;
|
|
152
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
153
|
+
overflow: auto;
|
|
123
154
|
border-radius: var(--playpilot-detail-border-radius, margin(1) margin(1) 0 0);
|
|
124
155
|
background: var(--playpilot-detail-background, var(--playpilot-light));
|
|
125
156
|
transition: transform 200ms;
|
|
@@ -129,10 +160,11 @@
|
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
@media (min-width: $max-width) {
|
|
163
|
+
max-height: unset;
|
|
132
164
|
margin-top: 0;
|
|
133
|
-
|
|
165
|
+
padding-bottom: 0;
|
|
134
166
|
overflow: visible;
|
|
135
|
-
|
|
167
|
+
border-radius: var(--playpilot-detail-border-radius, margin(1));
|
|
136
168
|
}
|
|
137
169
|
|
|
138
170
|
&.tall {
|
|
@@ -193,6 +225,24 @@
|
|
|
193
225
|
&:hover {
|
|
194
226
|
filter: brightness(1.1);
|
|
195
227
|
}
|
|
228
|
+
|
|
229
|
+
&.flat {
|
|
230
|
+
--playpilot-button-background: transparent;
|
|
231
|
+
--playpilot-button-shadow: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
&.back {
|
|
235
|
+
right: auto;
|
|
236
|
+
left: margin(1);
|
|
237
|
+
|
|
238
|
+
&.flat {
|
|
239
|
+
left: margin(0.5);
|
|
240
|
+
|
|
241
|
+
:global(svg) {
|
|
242
|
+
margin: 0;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
196
246
|
}
|
|
197
247
|
|
|
198
248
|
.prepend {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { heading } from '$lib/actions/heading'
|
|
3
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
4
|
+
import { openModal } from '$lib/modal'
|
|
5
|
+
import { trackSplitTestView } from '$lib/splitTest'
|
|
6
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
7
|
+
import ListTitle from './ListTitle.svelte'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
participant: ParticipantData
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { participant }: Props = $props()
|
|
14
|
+
|
|
15
|
+
const { name, birth_date, death_date } = $derived(participant)
|
|
16
|
+
|
|
17
|
+
trackSplitTestView(SplitTest.ParticipantPlaylinkFormat)
|
|
18
|
+
|
|
19
|
+
function formatDate(dateString: string): string {
|
|
20
|
+
const date = new Date(dateString)
|
|
21
|
+
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div class="header">
|
|
26
|
+
<div class="heading" use:heading={2} id="name">{name}</div>
|
|
27
|
+
|
|
28
|
+
{#if birth_date}
|
|
29
|
+
<p class="dates">
|
|
30
|
+
Born on <strong>{formatDate(birth_date)}</strong>{#if death_date}, died on <strong>{formatDate(death_date)}</strong>{/if}
|
|
31
|
+
</p>
|
|
32
|
+
{/if}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="content">
|
|
36
|
+
<div class="heading small" use:heading={3} id="credits">Credits</div>
|
|
37
|
+
|
|
38
|
+
<div class="list">
|
|
39
|
+
{#each window.PlayPilotLinkInjections?.evaluated_link_injections?.map(i => i.title_details) || [] as title}
|
|
40
|
+
{#if title}
|
|
41
|
+
<ListTitle {title} onclick={(event) => openModal({ event, data: title })} />
|
|
42
|
+
{/if}
|
|
43
|
+
{/each}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<style lang="scss">
|
|
48
|
+
.header {
|
|
49
|
+
padding: margin(4) margin(1) margin(2);
|
|
50
|
+
background: linear-gradient(to bottom, var(--playpilot-detail-background-light, var(--playpilot-lighter)), transparent);
|
|
51
|
+
border-radius: var(--playpilot-detail-border-radius, margin(1) margin(1) 0 0);
|
|
52
|
+
font-family: var(--playpilot-detail-font-family, var(--playpilot-font-family));
|
|
53
|
+
font-weight: var(--playpilot-detail-font-weight, normal);
|
|
54
|
+
font-size: var(--playpilot-detail-font-size, 14px);
|
|
55
|
+
line-height: var(--playpilot-participant-description-line-height, normal);
|
|
56
|
+
color: var(--playpilot-detail-text-color, var(--playpilot-text-color));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.heading {
|
|
60
|
+
margin: 0;
|
|
61
|
+
font-family: var(--playpilot-detail-title-font-family, var(--playpilot-font-family));
|
|
62
|
+
font-weight: var(--playpilot-detail-title-font-weight, lighter);
|
|
63
|
+
font-size: var(--playpilot-detail-title-font-size, margin(1.5));
|
|
64
|
+
line-height: normal;
|
|
65
|
+
font-style: var(--playpilot-detail-title-font-style, normal);
|
|
66
|
+
|
|
67
|
+
&.small {
|
|
68
|
+
margin: 0 0 margin(0.5);
|
|
69
|
+
font-size: var(--playpilot-detail-title-small-font-size, margin(1.25));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.dates {
|
|
74
|
+
margin: margin(0.5) 0 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.content {
|
|
78
|
+
padding: 0 margin(1) margin(1);
|
|
79
|
+
color: var(--playpilot-detail-text-color, var(--playpilot-text-color));
|
|
80
|
+
font-family: var(--playpilot-detail-font-family, var(--playpilot-font-family));
|
|
81
|
+
font-weight: var(--playpilot-detail-font-weight, normal);
|
|
82
|
+
font-size: var(--playpilot-detail-font-size, 14px);
|
|
83
|
+
line-height: normal;
|
|
84
|
+
font-style: normal;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.list {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: margin(0.5);
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import Modal from './Modal.svelte'
|
|
4
|
+
import Participant from './Participant.svelte'
|
|
5
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
participant: ParticipantData
|
|
9
|
+
initialScrollPosition?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { participant = {
|
|
13
|
+
sid: 'pr5C5W',
|
|
14
|
+
name: 'James Franco',
|
|
15
|
+
birth_date: '1978-04-19',
|
|
16
|
+
death_date: null,
|
|
17
|
+
jobs: ['actor'],
|
|
18
|
+
image: 'https://hips.hearstapps.com/hmg-prod/images/gettyimages-161098947-square.jpg',
|
|
19
|
+
image_uuid: '08ed2fac357011eb87470aff12c0f5c9',
|
|
20
|
+
gender: 'Male',
|
|
21
|
+
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.',
|
|
22
|
+
}, initialScrollPosition = 0 }: Props = $props()
|
|
23
|
+
|
|
24
|
+
let windowWidth = $state(0)
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<svelte:window bind:innerWidth={windowWidth} />
|
|
28
|
+
|
|
29
|
+
<Modal {initialScrollPosition} closeButtonStyle="flat">
|
|
30
|
+
<Participant {participant} />
|
|
31
|
+
</Modal>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { removeImageUrlPrefix } from '$lib/image'
|
|
3
|
+
import type { PlaylinkData } from '$lib/types/playlink'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
playlink: PlaylinkData
|
|
7
|
+
size?: number
|
|
8
|
+
// eslint-disable-next-line no-unused-vars
|
|
9
|
+
onclick?: (event: MouseEvent) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { playlink, size = 30, onclick = () => null }: Props = $props()
|
|
13
|
+
|
|
14
|
+
const { name, url, logo_url } = $derived(playlink)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<a href={url} target="_blank" class="playlink" data-playlink={name} rel="sponsored" {onclick}>
|
|
18
|
+
<img src={removeImageUrlPrefix(logo_url)} alt={name} height={size} width={size} />
|
|
19
|
+
</a>
|
|
20
|
+
|
|
21
|
+
<style lang="scss">
|
|
22
|
+
.playlink {
|
|
23
|
+
display: inline-block;
|
|
24
|
+
background: var(--playpilot-playlink-background, var(--playpilot-light));
|
|
25
|
+
border-radius: var(--playpilot-playlink-border-radius, margin(0.5));
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
|
|
28
|
+
&:hover,
|
|
29
|
+
&:active {
|
|
30
|
+
filter: var(--playpilot-playlink-hover-filter, brightness(1.1));
|
|
31
|
+
background: var(--playpilot-playlink-hover-background, var(--playpilot-playlink-background, var(--playpilot-lighter))) !important;
|
|
32
|
+
text-decoration: none !important;
|
|
33
|
+
outline: 2px solid var(--playpilot-detail-text-color, var(--playpilot-text-color));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
img {
|
|
37
|
+
display: block;
|
|
38
|
+
margin: 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PlaylinkData } from '$lib/types/playlink'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
playlink: PlaylinkData
|
|
6
|
+
size?: number
|
|
7
|
+
// eslint-disable-next-line no-unused-vars
|
|
8
|
+
onclick?: (event: MouseEvent) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { playlink, onclick = () => null }: Props = $props()
|
|
12
|
+
|
|
13
|
+
const { name, url } = $derived(playlink)
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<a href={url} target="_blank" class="playlink" data-playlink={name} rel="sponsored" {onclick}>
|
|
17
|
+
{name}
|
|
18
|
+
</a>
|
|
19
|
+
|
|
20
|
+
<style lang="scss">
|
|
21
|
+
.playlink {
|
|
22
|
+
display: inline-block;
|
|
23
|
+
background: var(--playpilot-playlink-label-background, var(--playpilot-light));
|
|
24
|
+
border-radius: var(--playpilot-playlink-label-border-radius, margin(0.25));
|
|
25
|
+
padding: margin(0.25) margin(0.5);
|
|
26
|
+
font-size: var(--playpilot-playlink-label-font-size, 12px);
|
|
27
|
+
color: var(--playpilot-playlink-label-text-color, var(--playpilot-text-color-alt)) !important;
|
|
28
|
+
text-decoration: none;
|
|
29
|
+
font-style: normal !important;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
|
|
32
|
+
&:hover,
|
|
33
|
+
&:active {
|
|
34
|
+
background: var(--playpilot-playlink-label-hover-background, var(--playpilot-content-light));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
@@ -27,9 +27,7 @@
|
|
|
27
27
|
// otherwise break the layout in ways that don't make sense to fix.
|
|
28
28
|
const list = $derived(outerWidth < 500 || !!displayAd)
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
const filteredPlaylinks = $derived(playlinks.filter(playlink => !!playlink.logo_url))
|
|
32
|
-
const mergedPlaylink = $derived(mergePlaylinks(filteredPlaylinks))
|
|
30
|
+
const mergedPlaylink = $derived(mergePlaylinks(playlinks))
|
|
33
31
|
|
|
34
32
|
function onclick(playlink: string): void {
|
|
35
33
|
track(isModal ? TrackingEvent.TitleModalPlaylinkClick : TrackingEvent.TitlePopoverPlaylinkClick, title, { playlink })
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { openModal } from '$lib/modal'
|
|
3
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
4
|
+
import Rail from './Rail.svelte'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
participants: ParticipantData[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { participants }: Props = $props()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<Rail heading="Cast">
|
|
14
|
+
{#each participants.slice(0, 15) as participant}
|
|
15
|
+
<button class="participant" onclick={event => openModal({ event, type: 'participant', data: participant })}>
|
|
16
|
+
<span class="truncate">{participant.name}</span>
|
|
17
|
+
|
|
18
|
+
<div class="character truncate">{participant.character}</div>
|
|
19
|
+
</button>
|
|
20
|
+
{/each}
|
|
21
|
+
</Rail>
|
|
22
|
+
|
|
23
|
+
<style lang="scss">
|
|
24
|
+
.participant {
|
|
25
|
+
display: block;
|
|
26
|
+
flex: 0 0 10rem;
|
|
27
|
+
width: 10rem;
|
|
28
|
+
padding: margin(0.5);
|
|
29
|
+
border: 0;
|
|
30
|
+
border-radius: var(--playpilot-cast-border-radius, var(--playpilot-playlink-border-radius, margin(0.5)));
|
|
31
|
+
background: var(--playpilot-cast-background, var(--playpilot-playlink-background, var(--playpilot-lighter)));
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
font-family: inherit;
|
|
34
|
+
text-align: left;
|
|
35
|
+
color: inherit;
|
|
36
|
+
font-size: var(--playpilot-cast-font-size, var(--playpilot-playlinks-font-size, margin(0.75)));
|
|
37
|
+
white-space: nowrap;
|
|
38
|
+
|
|
39
|
+
&:hover,
|
|
40
|
+
&:active {
|
|
41
|
+
filter: var(--playpilot-cast-hover-filter, var(--playpilot-playlink-hover-filter, brightness(1.1)));
|
|
42
|
+
text-decoration: none !important;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.character {
|
|
47
|
+
color: var(--playpilot-cast-character-text-color, var(--playpilot-text-color-alt));
|
|
48
|
+
font-style: italic;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.truncate {
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import TinySlider from 'svelte-tiny-slider'
|
|
4
|
+
import IconArrow from '../Icons/IconArrow.svelte'
|
|
5
|
+
import { heading as _heading } from '$lib/actions/heading'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
heading?: string
|
|
9
|
+
children: Snippet
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { heading = '', children }: Props = $props()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if heading}
|
|
16
|
+
<div class="heading" use:_heading={2}>{heading}</div>
|
|
17
|
+
{/if}
|
|
18
|
+
|
|
19
|
+
<div class="rail">
|
|
20
|
+
<TinySlider allowWheel>
|
|
21
|
+
{@render children()}
|
|
22
|
+
|
|
23
|
+
{#snippet controls({ setIndex, currentIndex, reachedEnd })}
|
|
24
|
+
{#if currentIndex > 0}
|
|
25
|
+
<button class="arrow left" onclick={() => setIndex(currentIndex - 1)}><IconArrow direction="left" /></button>
|
|
26
|
+
{/if}
|
|
27
|
+
|
|
28
|
+
{#if !reachedEnd}
|
|
29
|
+
<button class="arrow right" onclick={() => setIndex(currentIndex + 1)}><IconArrow /></button>
|
|
30
|
+
{/if}
|
|
31
|
+
{/snippet}
|
|
32
|
+
</TinySlider>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<style lang="scss">
|
|
36
|
+
.heading {
|
|
37
|
+
margin: margin(1) 0 margin(0.5);
|
|
38
|
+
font-size: var(--playpilot-rail-title-font-size, 18px);
|
|
39
|
+
line-height: normal;
|
|
40
|
+
font-weight: inherit;
|
|
41
|
+
color: var(--playpilot-rail-title-text-color, var(--playpilot-text-color));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.rail {
|
|
45
|
+
--gap: #{margin(0.5)};
|
|
46
|
+
position: relative;
|
|
47
|
+
width: calc(100% + margin(2));
|
|
48
|
+
margin: 0 margin(-1);
|
|
49
|
+
|
|
50
|
+
:global(.slider) {
|
|
51
|
+
padding: 0 margin(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:global(.slider-content > :last-child) {
|
|
55
|
+
margin-right: margin(2);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.arrow {
|
|
60
|
+
display: none;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
position: absolute;
|
|
64
|
+
left: margin(2);
|
|
65
|
+
top: 50%;
|
|
66
|
+
width: margin(2);
|
|
67
|
+
height: margin(2);
|
|
68
|
+
padding: 0;
|
|
69
|
+
margin: 0;
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
transform: translateX(-50%) translateY(-50%);
|
|
73
|
+
background: var(--playpilot-rails-arrow-background, var(--playpilot-content-light));
|
|
74
|
+
z-index: 2;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
color: var(--playpilot-rails-arrow-color, var(--playpilot-detail-text-color, white));
|
|
77
|
+
|
|
78
|
+
@media (hover: hover) {
|
|
79
|
+
display: flex;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
filter: brightness(1.2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&.right {
|
|
87
|
+
left: auto;
|
|
88
|
+
right: 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { TitleData } from '$lib/types/title'
|
|
3
|
+
import TitlesRail from './TitlesRail.svelte'
|
|
4
|
+
|
|
5
|
+
const titles = fetchTitles()
|
|
6
|
+
|
|
7
|
+
async function fetchTitles(): Promise<TitleData[]> {
|
|
8
|
+
// This is just a fake loading state for now
|
|
9
|
+
await new Promise(res => setTimeout(res, 500))
|
|
10
|
+
|
|
11
|
+
// Imagine this being a fetch request that returns titles instead.
|
|
12
|
+
return (window.PlayPilotLinkInjections?.evaluated_link_injections?.map(i => i.title_details) || []) as TitleData[]
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<TitlesRail {titles} heading="Similar movies & shows" />
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import TitlePoster from '../TitlePoster.svelte'
|
|
3
|
+
import Rail from './Rail.svelte'
|
|
4
|
+
import { playPilotBaseUrl } from '$lib/constants'
|
|
5
|
+
import type { TitleData } from '$lib/types/title'
|
|
6
|
+
import { openModal } from '$lib/modal'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
titles: Promise<TitleData[]> | TitleData[]
|
|
10
|
+
heading?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { titles, heading = '' }: Props = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="titles">
|
|
17
|
+
<Rail {heading}>
|
|
18
|
+
{#await titles}
|
|
19
|
+
{#each { length: 8 }}
|
|
20
|
+
<div class="title" data-testid="skeleton">
|
|
21
|
+
<div class="poster"></div>
|
|
22
|
+
|
|
23
|
+
<div class="heading">
|
|
24
|
+
<span class="skeleton"> </span>
|
|
25
|
+
<span class="skeleton"> </span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{/each}
|
|
29
|
+
{:then titles}
|
|
30
|
+
{#each titles as title}
|
|
31
|
+
{@const href = `${playPilotBaseUrl}/${title.type}/${title.slug}`}
|
|
32
|
+
|
|
33
|
+
<div class="title" data-testid="title">
|
|
34
|
+
<a class="poster" {href} onclick={(event) => openModal({ event, data: title })}>
|
|
35
|
+
<TitlePoster {title} width={96} height={144} />
|
|
36
|
+
</a>
|
|
37
|
+
|
|
38
|
+
<a {href} class="heading" onclick={(event) => openModal({ event, data: title })}>
|
|
39
|
+
{title.title}
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
{/each}
|
|
43
|
+
{/await}
|
|
44
|
+
</Rail>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<style lang="scss">
|
|
48
|
+
$width: margin(6);
|
|
49
|
+
|
|
50
|
+
.title {
|
|
51
|
+
width: $width;
|
|
52
|
+
|
|
53
|
+
&:hover,
|
|
54
|
+
&:active {
|
|
55
|
+
filter: brightness(1.1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.poster {
|
|
60
|
+
display: block;
|
|
61
|
+
border-radius: var(--playpilot-rail-border-radius, var(--playpilot-playlink-border-radius, margin(0.5)));
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
width: 100%;
|
|
64
|
+
aspect-ratio: 2 / 3;
|
|
65
|
+
background: var(--playpilot-detail-background-light, var(--playpilot-lighter));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.heading {
|
|
69
|
+
display: -webkit-box;
|
|
70
|
+
padding-top: margin(0.5);
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
color: var(--playpilot-detail-text-color, var(--playpilot-text-color-alt)) !important;
|
|
74
|
+
font-size: var(--playpilot-rail-font-size, var(--playpilot-detail-font-size-small, 12px));
|
|
75
|
+
font-style: normal !important;
|
|
76
|
+
line-height: 1.2;
|
|
77
|
+
line-clamp: 2;
|
|
78
|
+
-webkit-line-clamp: 2;
|
|
79
|
+
-webkit-box-orient: vertical;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.skeleton {
|
|
84
|
+
display: block;
|
|
85
|
+
height: 1em;
|
|
86
|
+
width: 100%;
|
|
87
|
+
background: var(--playpilot-detail-background-light, var(--playpilot-lighter));
|
|
88
|
+
border-radius: margin(2);
|
|
89
|
+
|
|
90
|
+
&:last-child {
|
|
91
|
+
margin-top: margin(0.3);
|
|
92
|
+
width: 50%;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</style>
|