@playpilot/tpi 5.32.1 → 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 -10
- 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 +6 -24
- 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
package/package.json
CHANGED
package/src/lib/api/titles.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { getApiToken } from '$lib/token'
|
|
2
|
+
import type { APIPaginatedResult } from '$lib/types/api'
|
|
2
3
|
import type { TitleData } from '../types/title'
|
|
3
4
|
import { api } from './api'
|
|
4
5
|
|
|
6
|
+
export async function fetchTitles(params: Record<string, string | number> = {}): Promise<APIPaginatedResult<TitleData>> {
|
|
7
|
+
const apiToken = getApiToken()
|
|
8
|
+
|
|
9
|
+
if (!apiToken) throw new Error('No token was provided')
|
|
10
|
+
|
|
11
|
+
const paramsAsString = Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&')
|
|
12
|
+
const response = await api<APIPaginatedResult<TitleData>>(`/titles/browse?api-token=${apiToken}&` + paramsAsString)
|
|
13
|
+
|
|
14
|
+
return response
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]> {
|
|
6
|
-
const response = await
|
|
18
|
+
const response = await fetchTitles({ related_to_sid: title.sid })
|
|
7
19
|
|
|
8
20
|
return response.results
|
|
9
21
|
}
|
|
@@ -126,6 +126,11 @@ export const translations = {
|
|
|
126
126
|
[Language.Swedish]: 'Liknande filmer och serier',
|
|
127
127
|
[Language.Danish]: 'Lignende film og serier',
|
|
128
128
|
},
|
|
129
|
+
'Watch Trailer': {
|
|
130
|
+
[Language.English]: 'Watch trailer',
|
|
131
|
+
[Language.Swedish]: 'Se trailer',
|
|
132
|
+
[Language.Danish]: 'Se trailer',
|
|
133
|
+
},
|
|
129
134
|
|
|
130
135
|
// Genres
|
|
131
136
|
'All': {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mount, unmount } from 'svelte'
|
|
2
|
+
import Explore from '../routes/components/Explore/Explore.svelte'
|
|
3
|
+
|
|
4
|
+
const exploreParentSelector = `[data-playpilot-explore]`
|
|
5
|
+
|
|
6
|
+
let exploreInsertedComponent: object | null = null
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Insert the Explore component inside of the needed selector. If no element is found, do nothing.
|
|
10
|
+
* Only one Explore component can exist per page.
|
|
11
|
+
*/
|
|
12
|
+
export function insertExplore(): void {
|
|
13
|
+
const target = document.querySelector<HTMLElement>(exploreParentSelector)
|
|
14
|
+
if (!target) return
|
|
15
|
+
|
|
16
|
+
destroyExplore()
|
|
17
|
+
|
|
18
|
+
exploreInsertedComponent = mount(Explore, { target })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function destroyExplore(): void {
|
|
22
|
+
if (!exploreInsertedComponent) return
|
|
23
|
+
|
|
24
|
+
unmount(exploreInsertedComponent)
|
|
25
|
+
exploreInsertedComponent = null
|
|
26
|
+
}
|
package/src/lib/fakeData.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const title: TitleData = {
|
|
|
38
38
|
standing_poster: 'https://img.playpilot.tech/6239ee86a58f11efb0b50a58a9feac02/src/img?optimizer=image&class=2by3x18',
|
|
39
39
|
title: 'Dune: Prophecy',
|
|
40
40
|
original_title: 'Dune: Prophecy',
|
|
41
|
+
embeddable_url: null,
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export const linkInjections: LinkInjection[] = [{
|
|
Binary file
|
package/src/lib/modal.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { mount, unmount } from "svelte"
|
|
2
2
|
import { isHoldingSpecialKey } from "./event"
|
|
3
|
-
import TitleModal from "../routes/components/TitleModal.svelte"
|
|
4
3
|
import type { LinkInjection } from "./types/injection"
|
|
5
|
-
import ParticipantModal from "../routes/components/ParticipantModal.svelte"
|
|
6
4
|
import type { TitleData } from "./types/title"
|
|
7
5
|
import type { ParticipantData } from "./types/participant"
|
|
8
6
|
import { mobileBreakpoint } from "./constants"
|
|
9
7
|
import { getPlayPilotWrapperElement } from "./injection"
|
|
8
|
+
import TitleModal from "../routes/components/TitleModal.svelte"
|
|
9
|
+
import ParticipantModal from "../routes/components/ParticipantModal.svelte"
|
|
10
|
+
import ExploreModal from "../routes/components/Explore/ExploreModal.svelte"
|
|
10
11
|
|
|
11
|
-
type ModalType = 'title' | 'participant'
|
|
12
|
+
type ModalType = 'title' | 'participant' | 'explore'
|
|
12
13
|
|
|
13
14
|
type Modal = {
|
|
14
15
|
injection?: LinkInjection | null
|
|
@@ -46,9 +47,9 @@ export function openModal(
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
function getModalComponentByType({ type = 'title', target, data, props = {} }: { type: ModalType, target: Element, data: TitleData | ParticipantData | null, props?: Record<string, any> }) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (type === 'participant') return mount(ParticipantModal, { target, props: { participant: data as ParticipantData, ...props } })
|
|
51
|
+
if (type === 'explore') return mount(ExploreModal, { target, props: { ...props } })
|
|
52
|
+
return mount(TitleModal, { target, props: { title: data as TitleData, ...props } })
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function addModalToList({ type = 'title', injection = null, data, scrollPosition = 0, component }: Modal) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte"
|
|
2
|
+
import { getPlayPilotWrapperElement } from "./injection"
|
|
3
|
+
import type { TitleData } from "./types/title"
|
|
4
|
+
import YouTubeEmbedOverlay from "../routes/components/YouTubeEmbedOverlay.svelte"
|
|
5
|
+
|
|
6
|
+
let currentTrailerComponent: object | null = {}
|
|
7
|
+
|
|
8
|
+
export function openTrailerOverlay(title: TitleData) {
|
|
9
|
+
const target = getPlayPilotWrapperElement()
|
|
10
|
+
// !! Temporarily falls back to a placeholder is while embeddable_url is not yet present
|
|
11
|
+
const props = { onclose: closeTrailerOverlay, embeddable_url: title.embeddable_url || 'https://www.youtube.com/watch?v=xGTq0blCPVQ' }
|
|
12
|
+
|
|
13
|
+
currentTrailerComponent = mount(YouTubeEmbedOverlay, { target, props })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function closeTrailerOverlay(): void {
|
|
17
|
+
if (!currentTrailerComponent) return
|
|
18
|
+
|
|
19
|
+
unmount(currentTrailerComponent, { outro: true })
|
|
20
|
+
|
|
21
|
+
currentTrailerComponent = null
|
|
22
|
+
}
|
package/src/lib/types/title.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ParticipantData } from './participant'
|
|
2
2
|
import type { PlaylinkData } from './playlink'
|
|
3
3
|
|
|
4
|
+
export type ContentType = 'movie' | 'series'
|
|
5
|
+
|
|
4
6
|
export type TitleData = {
|
|
5
7
|
sid: string
|
|
6
8
|
slug: string
|
|
@@ -9,7 +11,7 @@ export type TitleData = {
|
|
|
9
11
|
genres: string[]
|
|
10
12
|
year: number
|
|
11
13
|
imdb_score: number
|
|
12
|
-
type:
|
|
14
|
+
type: ContentType
|
|
13
15
|
providers: PlaylinkData[]
|
|
14
16
|
description: string | null
|
|
15
17
|
small_poster: string
|
|
@@ -17,6 +19,7 @@ export type TitleData = {
|
|
|
17
19
|
standing_poster: string
|
|
18
20
|
title: string
|
|
19
21
|
original_title: string
|
|
22
|
+
embeddable_url: string | null
|
|
20
23
|
length?: number
|
|
21
24
|
blurb?: string
|
|
22
25
|
participants?: ParticipantData[]
|
package/src/routes/+page.svelte
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import Consent from './components/Consent.svelte'
|
|
18
18
|
import Debugger from './components/Debugger.svelte'
|
|
19
19
|
import UserJourney from './components/UserJourney.svelte'
|
|
20
|
+
import { insertExplore } from '$lib/explore';
|
|
20
21
|
|
|
21
22
|
let parentElement: HTMLElement | null = $state(null)
|
|
22
23
|
let elements: HTMLElement[] = $state([])
|
|
@@ -42,6 +43,8 @@
|
|
|
42
43
|
if (isEditorialMode && !loading) rerender()
|
|
43
44
|
})
|
|
44
45
|
|
|
46
|
+
insertExplore()
|
|
47
|
+
|
|
45
48
|
onDestroy(clearLinkInjections)
|
|
46
49
|
|
|
47
50
|
// This function is called when a user has properly consented via tcfapi or if no consent is required.
|
|
@@ -242,7 +245,8 @@
|
|
|
242
245
|
<style lang="scss">
|
|
243
246
|
@import url('$lib/scss/global.scss');
|
|
244
247
|
|
|
245
|
-
.playpilot-link-injections
|
|
248
|
+
.playpilot-link-injections,
|
|
249
|
+
:global([data-playpilot-explore]) {
|
|
246
250
|
:global(*) {
|
|
247
251
|
box-sizing: border-box;
|
|
248
252
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { SplitTest } from '$lib/enums/SplitTest'
|
|
5
5
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
6
6
|
import { imageFromUUID } from '$lib/image'
|
|
7
|
-
import {
|
|
7
|
+
import { trackSplitTestView } from '$lib/splitTest'
|
|
8
8
|
import { track } from '$lib/tracking'
|
|
9
9
|
import type { Campaign } from '$lib/types/campaign'
|
|
10
10
|
import Disclaimer from './Disclaimer.svelte'
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
const { format, header, header_logo: logo, image_uuid: backgroundImageUUID } = $derived(content)
|
|
23
23
|
const { header: buttonLabel, url: href } = $derived(cta)
|
|
24
24
|
|
|
25
|
-
const inline = isInSplitTestVariant(SplitTest.TopScrollFormat, 1)
|
|
26
25
|
const simple = $derived(format === 'large')
|
|
27
26
|
|
|
28
27
|
const backgroundImage = $derived(imageFromUUID(backgroundImageUUID, ImageDimensions.TopScrollBackground))
|
|
@@ -43,7 +42,6 @@
|
|
|
43
42
|
target="_blank"
|
|
44
43
|
class="top-scroll"
|
|
45
44
|
class:simple
|
|
46
|
-
class:inline
|
|
47
45
|
tabindex="-1"
|
|
48
46
|
rel="sponsored"
|
|
49
47
|
style:--width="{clientWidth}px">
|
|
@@ -81,17 +79,13 @@
|
|
|
81
79
|
position: relative;
|
|
82
80
|
display: block;
|
|
83
81
|
width: 100%;
|
|
84
|
-
border-radius: $border-radius-size;
|
|
82
|
+
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
85
83
|
background: black;
|
|
86
84
|
color: theme(top-scroll-text-color, white);
|
|
87
85
|
font-family: theme(top-scroll-font-family, font-family);
|
|
88
86
|
font-size: theme(top-scroll-font-size, font-size-base);
|
|
89
87
|
text-decoration: none;
|
|
90
88
|
line-height: 1.35;
|
|
91
|
-
|
|
92
|
-
&.inline {
|
|
93
|
-
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
94
|
-
}
|
|
95
89
|
}
|
|
96
90
|
|
|
97
91
|
.content {
|
|
@@ -152,7 +146,7 @@
|
|
|
152
146
|
right: 0;
|
|
153
147
|
bottom: 0;
|
|
154
148
|
left: 0;
|
|
155
|
-
border-radius: $border-radius-size;
|
|
149
|
+
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
156
150
|
background-image: var(--background);
|
|
157
151
|
background-position: center;
|
|
158
152
|
background-size: cover;
|
|
@@ -161,10 +155,6 @@
|
|
|
161
155
|
.top-scroll:hover & {
|
|
162
156
|
filter: brightness(1.15);
|
|
163
157
|
}
|
|
164
|
-
|
|
165
|
-
.inline & {
|
|
166
|
-
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
167
|
-
}
|
|
168
158
|
}
|
|
169
159
|
|
|
170
160
|
.content-image {
|
|
@@ -172,14 +162,10 @@
|
|
|
172
162
|
max-width: 100%;
|
|
173
163
|
height: auto;
|
|
174
164
|
background: black;
|
|
175
|
-
border-radius: $border-radius-size;
|
|
165
|
+
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
176
166
|
|
|
177
167
|
.top-scroll:hover & {
|
|
178
168
|
filter: brightness(1.15);
|
|
179
169
|
}
|
|
180
|
-
|
|
181
|
-
.inline & {
|
|
182
|
-
border-radius: $border-radius-size $border-radius-size 0 0;
|
|
183
|
-
}
|
|
184
170
|
}
|
|
185
171
|
</style>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
variant?: 'filled' | 'border'
|
|
6
|
+
size?: 'base' | 'large'
|
|
7
|
+
active?: boolean
|
|
8
|
+
// eslint-disable-next-line no-unused-vars
|
|
9
|
+
onclick?: (event: MouseEvent) => void
|
|
10
|
+
children?: Snippet
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { variant = 'filled', size = 'base', active = false, onclick, children }: Props = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<button class="button {variant} {size}" class:active {onclick}>
|
|
17
|
+
{@render children?.()}
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<style lang="scss">
|
|
21
|
+
.button {
|
|
22
|
+
appearance: none;
|
|
23
|
+
display: flex;
|
|
24
|
+
height: 100%;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
gap: margin(0.25);
|
|
28
|
+
border: 0;
|
|
29
|
+
padding: 0.25em 0.5em;
|
|
30
|
+
background: transparent;
|
|
31
|
+
border-radius: theme(button-border-radius, border-radius);
|
|
32
|
+
color: theme(button-text-color, text-color-alt);
|
|
33
|
+
font-size: inherit;
|
|
34
|
+
font-family: inherit;
|
|
35
|
+
font-weight: theme(button-font-weight, normal);
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
|
|
38
|
+
:global(svg) {
|
|
39
|
+
width: 1.5em;
|
|
40
|
+
height: 1.5em;
|
|
41
|
+
opacity: 0.75;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.filled {
|
|
46
|
+
background: theme(button-filled-background, content);
|
|
47
|
+
|
|
48
|
+
&:hover,
|
|
49
|
+
&:active {
|
|
50
|
+
background: theme(button-filled-hover-background, content-light);
|
|
51
|
+
color: theme(button-filled-hover-text-color, text-color);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&.active {
|
|
55
|
+
box-shadow: inset 0 0 0 1px currentColor;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.border {
|
|
60
|
+
box-shadow: inset 0 0 0 1px theme(button-border-color, content-light);
|
|
61
|
+
|
|
62
|
+
&:hover,
|
|
63
|
+
&:active {
|
|
64
|
+
background: theme(button-border-color, content);
|
|
65
|
+
color: theme(button-hover-text-color, text-color);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.large {
|
|
70
|
+
padding: 0.5em 1.5em;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
73
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { heading } from '$lib/actions/heading'
|
|
3
|
+
import { searchTitles } from '$lib/api/search'
|
|
4
|
+
import { fetchTitles } from '$lib/api/titles'
|
|
5
|
+
import type { APIPaginatedResult } from '$lib/types/api'
|
|
6
|
+
import type { ContentType, TitleData } from '$lib/types/title'
|
|
7
|
+
import Button from '../Button.svelte'
|
|
8
|
+
import ListTitle from '../ListTitle.svelte'
|
|
9
|
+
import ListTitleSkeleton from '../ListTitleSkeleton.svelte'
|
|
10
|
+
import Filter from './Filter.svelte'
|
|
11
|
+
import Search from './Search.svelte'
|
|
12
|
+
|
|
13
|
+
type ExploreFilter = {
|
|
14
|
+
content_type?: ContentType
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const filter: ExploreFilter = $state({})
|
|
18
|
+
|
|
19
|
+
let titles: TitleData[] = $state([])
|
|
20
|
+
let page = 1
|
|
21
|
+
let searchQuery = ''
|
|
22
|
+
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
23
|
+
let promise = $state(getTitlesForFilter())
|
|
24
|
+
|
|
25
|
+
async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
|
|
26
|
+
let response
|
|
27
|
+
|
|
28
|
+
// If a search query is given we use searchTitles with just the query.
|
|
29
|
+
// If not, we use fetchTitles with the given params for the filter.
|
|
30
|
+
// This is because the backend does not support filters for search results yet.
|
|
31
|
+
// In the future we will likely merge these two, either by adding filters in the backend
|
|
32
|
+
// or by filtering results in the frontend (this seems like the less good option).
|
|
33
|
+
if (searchQuery) {
|
|
34
|
+
const results: TitleData[] = await searchTitles(searchQuery)
|
|
35
|
+
response = { results, next: null, previous: null }
|
|
36
|
+
|
|
37
|
+
titles = results
|
|
38
|
+
} else {
|
|
39
|
+
const params = { page_size: 24, page, ...filter }
|
|
40
|
+
|
|
41
|
+
response = await fetchTitles(params)
|
|
42
|
+
if (!response?.results) throw new Error('Something went wrong when fetching titles in Explore')
|
|
43
|
+
|
|
44
|
+
titles = [...titles, ...response.results]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return response
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function search(query: string): Promise<void> {
|
|
51
|
+
searchQuery = query
|
|
52
|
+
|
|
53
|
+
if (debounce) clearTimeout(debounce)
|
|
54
|
+
|
|
55
|
+
debounce = setTimeout(() => {
|
|
56
|
+
resetTitles()
|
|
57
|
+
promise = getTitlesForFilter()
|
|
58
|
+
}, 100)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setFilter(key: keyof ExploreFilter, value: any): void {
|
|
62
|
+
// If the value was previous present in the filter and a falsey value is given we remove it from
|
|
63
|
+
// the filter entirely so it will no longer be used as a param
|
|
64
|
+
if (!value && (key in filter)) {
|
|
65
|
+
delete filter[key]
|
|
66
|
+
} else if (value) {
|
|
67
|
+
filter[key] = value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
resetTitles()
|
|
71
|
+
promise = getTitlesForFilter()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resetTitles(): void {
|
|
75
|
+
page = 1
|
|
76
|
+
titles = []
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fetchMoreTitles(): void {
|
|
80
|
+
page++
|
|
81
|
+
promise = getTitlesForFilter()
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<div class="explore">
|
|
86
|
+
<div class="header" role="banner">
|
|
87
|
+
<div class="heading" use:heading>
|
|
88
|
+
<strong>Site Name</strong>
|
|
89
|
+
<div class="divider"></div>
|
|
90
|
+
Streaming Guide
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="categories" role="navigation">
|
|
94
|
+
<Button size="large" active={!filter.content_type} onclick={() => setFilter('content_type', '')}>All</Button>
|
|
95
|
+
<Button size="large" active={filter.content_type === 'movie'} onclick={() => setFilter('content_type', 'movie')}>Movies</Button>
|
|
96
|
+
<Button size="large" active={filter.content_type === 'series'} onclick={() => setFilter('content_type', 'series')}>Shows</Button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<Search oninput={search} />
|
|
100
|
+
<Filter />
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="titles" role="main">
|
|
104
|
+
{#each titles as title}
|
|
105
|
+
<ListTitle {title} />
|
|
106
|
+
{/each}
|
|
107
|
+
|
|
108
|
+
{#await promise}
|
|
109
|
+
{#each { length: 12 } as _}
|
|
110
|
+
<ListTitleSkeleton />
|
|
111
|
+
{/each}
|
|
112
|
+
{:then { next }}
|
|
113
|
+
{#if next}
|
|
114
|
+
<div class="show-more">
|
|
115
|
+
<Button size="large" onclick={fetchMoreTitles}>Show more</Button>
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
118
|
+
{:catch}
|
|
119
|
+
Something went wrong
|
|
120
|
+
{/await}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<style lang="scss">
|
|
125
|
+
.explore {
|
|
126
|
+
background: theme(explore-background, light);
|
|
127
|
+
border-radius: theme(border-radius-large);
|
|
128
|
+
padding: margin(1);
|
|
129
|
+
font-family: theme(font-family);
|
|
130
|
+
font-family: theme(detail-font-family, font-family);
|
|
131
|
+
font-weight: theme(detail-font-weight, normal);
|
|
132
|
+
font-size: theme(detail-font-size, font-size-base);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.header {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: margin(0.5);
|
|
139
|
+
margin-bottom: margin(2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.heading {
|
|
143
|
+
display: flex;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
gap: margin(0.5);
|
|
146
|
+
margin: margin(1) 0;
|
|
147
|
+
color: theme(text-color);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.divider {
|
|
151
|
+
width: 2px;
|
|
152
|
+
height: 0.5lh;
|
|
153
|
+
margin-top: 0.25lh;
|
|
154
|
+
background: currentColor;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.categories {
|
|
158
|
+
display: grid;
|
|
159
|
+
grid-template-columns: repeat(3, 1fr);
|
|
160
|
+
gap: margin(0.5);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.titles {
|
|
164
|
+
--playpilot-list-item-padding: 0;
|
|
165
|
+
--playpilot-list-item-background: transparent;
|
|
166
|
+
--playpilot-list-item-hover-shadow: 0 0 0 #{margin(0.25)} #{theme(lighter)};
|
|
167
|
+
--playpilot-detail-image-background: #{theme(content)};
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
gap: margin(0.5);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.show-more {
|
|
174
|
+
flex: 0 0 100%;
|
|
175
|
+
display: grid;
|
|
176
|
+
margin-top: margin(0.5);
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconArrow from '../Icons/IconArrow.svelte'
|
|
3
|
+
import ImageTitlesList from '$lib/images/titles-list.webp'
|
|
4
|
+
import { openModal } from '$lib/modal'
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<button class="call-to-action" onclick={() => openModal({ type: 'explore' })}>
|
|
8
|
+
<img src={ImageTitlesList} alt="" width="70" height="57" />
|
|
9
|
+
|
|
10
|
+
<div>
|
|
11
|
+
<strong>Looking for something else?</strong>
|
|
12
|
+
<div>Use our streamingguide</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="arrow">
|
|
16
|
+
<IconArrow />
|
|
17
|
+
</div>
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<style lang="scss">
|
|
21
|
+
strong {
|
|
22
|
+
color: theme(text-color);
|
|
23
|
+
font-size: theme(font-size-base);
|
|
24
|
+
font-weight: normal;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.call-to-action {
|
|
28
|
+
appearance: none;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: margin(1);
|
|
32
|
+
width: 100%;
|
|
33
|
+
padding: margin(1);
|
|
34
|
+
border: 0;
|
|
35
|
+
border-radius: theme(border-radius-large) theme(border-radius-large) 0 0;
|
|
36
|
+
background: theme(light);
|
|
37
|
+
font-family: theme(font-family);
|
|
38
|
+
color: theme(text-color-alt);
|
|
39
|
+
font-size: theme(font-size-small);
|
|
40
|
+
line-height: 1.5;
|
|
41
|
+
text-align: left;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
|
|
44
|
+
&:hover {
|
|
45
|
+
filter: brightness(theme(hover-filter-brightness));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.arrow {
|
|
50
|
+
margin-left: auto;
|
|
51
|
+
|
|
52
|
+
:global(svg) {
|
|
53
|
+
display: block;
|
|
54
|
+
height: margin(1.25);
|
|
55
|
+
width: auto;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import Modal from '../Modal.svelte'
|
|
4
|
+
import Explore from './Explore.svelte'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
initialScrollPosition?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { initialScrollPosition = 0 }: Props = $props()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<Modal {initialScrollPosition} closeButtonStyle="flat">
|
|
14
|
+
<Explore />
|
|
15
|
+
</Modal>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconSearch from '../Icons/IconSearch.svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
oninput: (query: string) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { oninput }: Props = $props()
|
|
10
|
+
|
|
11
|
+
let query = $state('')
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class="search" role="search">
|
|
15
|
+
<div class="icon">
|
|
16
|
+
<IconSearch />
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<input bind:value={query} oninput={() => oninput(query)} class="input" type="search" placeholder="Search movies and shows" />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<style lang="scss">
|
|
23
|
+
.search {
|
|
24
|
+
position: relative;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.icon {
|
|
28
|
+
position: absolute;
|
|
29
|
+
top: margin(0.6);
|
|
30
|
+
left: margin(1);
|
|
31
|
+
color: theme(text-color-alt);
|
|
32
|
+
opacity: 0.75;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.input {
|
|
36
|
+
width: 100%;
|
|
37
|
+
padding: margin(0.75) margin(1) margin(0.75) margin(3);
|
|
38
|
+
border: 0;
|
|
39
|
+
border-radius: theme(border-radius);
|
|
40
|
+
background: theme(content);
|
|
41
|
+
color: theme(text-color-alt);
|
|
42
|
+
font-size: theme(font-size-base);
|
|
43
|
+
font-family: theme(font-family);
|
|
44
|
+
|
|
45
|
+
&:focus {
|
|
46
|
+
outline: 1px solid white;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&::placeholder {
|
|
50
|
+
color: theme(text-color-alt);
|
|
51
|
+
opacity: 0.75;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
<
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { size = 16 }: Props = $props()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
2
10
|
<path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
11
|
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24px" height="24px" viewBox="0 -960 960 960">
|
|
2
|
+
<path fill="currentColor" d="m380-300 280-180-280-180v360ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
|
|
3
|
+
</svg>
|