@playpilot/tpi 5.34.1 → 6.0.0-beta.explore.15
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 +25 -10
- package/package.json +1 -1
- package/src/lib/api/titles.ts +13 -1
- package/src/lib/data/countries.json +216 -0
- package/src/lib/data/translations.ts +5 -0
- package/src/lib/enums/SplitTest.ts +1 -6
- package/src/lib/explore.ts +59 -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/scss/global.scss +0 -2
- package/src/lib/trailer.ts +22 -0
- package/src/lib/types/api.d.ts +6 -0
- package/src/lib/types/config.d.ts +12 -0
- package/src/lib/types/filter.d.ts +2 -0
- package/src/lib/types/title.d.ts +4 -1
- package/src/routes/+page.svelte +6 -1
- package/src/routes/components/Ads/TopScroll.svelte +4 -18
- package/src/routes/components/Button.svelte +101 -0
- package/src/routes/components/Debugger.svelte +25 -0
- package/src/routes/components/Explore/Explore.svelte +240 -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/Dropdown.svelte +72 -0
- package/src/routes/components/Explore/Filter/Filter.svelte +99 -0
- package/src/routes/components/Explore/Filter/FilterItem.svelte +57 -0
- package/src/routes/components/Explore/Filter/FilterSorting.svelte +70 -0
- package/src/routes/components/Explore/Filter/Search.svelte +57 -0
- package/src/routes/components/Explore/Filter/TogglesWithSearch.svelte +142 -0
- package/src/routes/components/GridTitle.svelte +122 -0
- package/src/routes/components/GridTitleSkeleton.svelte +36 -0
- package/src/routes/components/Icons/IconArrow.svelte +10 -2
- package/src/routes/components/Icons/IconClose.svelte +9 -1
- package/src/routes/components/Icons/IconFilter.svelte +5 -0
- package/src/routes/components/Icons/IconPlay.svelte +3 -0
- package/src/routes/components/Icons/IconSearch.svelte +3 -0
- package/src/routes/components/ListTitle.svelte +10 -68
- package/src/routes/components/ListTitleSkeleton.svelte +42 -0
- package/src/routes/components/Modal.svelte +5 -23
- package/src/routes/components/Participant.svelte +0 -4
- package/src/routes/components/Playlinks/PlaylinkIcon.svelte +1 -1
- package/src/routes/components/Playlinks/PlaylinksCompact.svelte +62 -0
- 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 +39 -2
- package/src/routes/explore/+page.svelte +60 -0
- 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 +139 -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 +94 -0
- package/src/tests/routes/components/Explore/Filter/Dropdown.test.js +16 -0
- package/src/tests/routes/components/Explore/Filter/Filter.test.js +28 -0
- package/src/tests/routes/components/Explore/Filter/FilterItem.test.js +50 -0
- package/src/tests/routes/components/Explore/Filter/FilterSorting.test.js +34 -0
- package/src/tests/routes/components/Explore/Filter/Search.test.js +26 -0
- package/src/tests/routes/components/Explore/Filter/TogglesWithSearch.test.js +53 -0
- package/src/tests/routes/components/GridTitle.test.js +42 -0
- package/src/tests/routes/components/ListTitle.test.js +1 -1
- package/src/tests/routes/components/Playlinks/PlaylinksCompact.test.js +42 -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/src/tests/setup.js +2 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
variant?: 'filled' | 'border' | 'link'
|
|
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
|
+
&:last-child {
|
|
44
|
+
width: 0.85em;
|
|
45
|
+
height: 0.85em;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.filled {
|
|
51
|
+
background: theme(button-filled-background, content);
|
|
52
|
+
|
|
53
|
+
&:hover,
|
|
54
|
+
&:active {
|
|
55
|
+
background: theme(button-filled-hover-background, content-light);
|
|
56
|
+
color: theme(button-filled-hover-text-color, text-color);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&.active {
|
|
60
|
+
box-shadow: inset 0 0 0 1px currentColor;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.border {
|
|
65
|
+
box-shadow: inset 0 0 0 1px theme(button-border-color, content-light);
|
|
66
|
+
|
|
67
|
+
&:hover,
|
|
68
|
+
&:active,
|
|
69
|
+
&.active {
|
|
70
|
+
background: theme(button-border-color, lighter);
|
|
71
|
+
color: theme(button-hover-text-color, text-color);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&.active {
|
|
75
|
+
filter: brightness(theme(hover-filter-brightness));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.link {
|
|
80
|
+
display: inline-flex;
|
|
81
|
+
height: auto;
|
|
82
|
+
padding: 0;
|
|
83
|
+
font-size: inherit;
|
|
84
|
+
line-height: inherit;
|
|
85
|
+
|
|
86
|
+
&:hover,
|
|
87
|
+
&:active {
|
|
88
|
+
color: theme(button-hover-text-color, text-color);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
:global(svg) {
|
|
92
|
+
width: 0.85em;
|
|
93
|
+
height: 0.85em;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.large {
|
|
98
|
+
padding: 0.5em 1em;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
101
|
+
|
|
@@ -108,6 +108,27 @@
|
|
|
108
108
|
currentScriptTag.insertAdjacentElement('afterend', newScriptTag)
|
|
109
109
|
currentScriptTag.remove()
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
function replacePageWithExplore() {
|
|
113
|
+
const element = document.querySelector("main, article")
|
|
114
|
+
|
|
115
|
+
element!.innerHTML = `
|
|
116
|
+
<div data-playpilot-explore style="background: white;">
|
|
117
|
+
<div style="padding: 64px 32px; min-height: 100vh; color: black; max-width: 1264px; margin: 0 auto">
|
|
118
|
+
<div class="divider" style="width: 100%; max-width: 600px; height: 0.25rem;background-image: linear-gradient(to right,#51B3E0,#51B3E0 2.5rem,#E5ADAE 2.5rem,#E5ADAE 5rem,#E5E54F 5rem,#E5E54F 7.5rem,black 7.5rem,black);"></div>
|
|
119
|
+
|
|
120
|
+
<div class="heading" style="margin: 4px 0; font-size: clamp(24px, 5vw, 32px); line-height: 1.5; font-weight: bold; text-transform: uppercase;">
|
|
121
|
+
Streaming Guide
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div role="status" aria-live="polite">
|
|
125
|
+
<svg fill="currentColor" viewBox="0 0 24 24" width="72"><path fill="currentColor" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"><animateTransform attributeName="transform" type="rotate" dur="500ms" values="0 12 12;360 12 12" repeatCount="indefinite"/></path></svg>
|
|
126
|
+
<div style="margin: 12px 0; font-weight: bold; text-transform: uppercase;">Loading…</div>
|
|
127
|
+
<noscript>Sorry, this page requires JavaScript to be enabled.</noscript>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>`
|
|
131
|
+
}
|
|
111
132
|
</script>
|
|
112
133
|
|
|
113
134
|
<svelte:window {onkeydown} />
|
|
@@ -164,6 +185,10 @@
|
|
|
164
185
|
{:else}
|
|
165
186
|
<button onclick={replaceWithBetaScript}>Use beta script</button>
|
|
166
187
|
{/if}
|
|
188
|
+
|
|
189
|
+
<hr />
|
|
190
|
+
|
|
191
|
+
<button onclick={replacePageWithExplore}>Replace page with explore</button>
|
|
167
192
|
</div>
|
|
168
193
|
{/if}
|
|
169
194
|
|
|
@@ -0,0 +1,240 @@
|
|
|
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 { exploreParentSelector } from '$lib/explore'
|
|
6
|
+
import { openModal } from '$lib/modal'
|
|
7
|
+
import type { APIPaginatedResult } from '$lib/types/api'
|
|
8
|
+
import type { ExploreFilter } from '$lib/types/filter'
|
|
9
|
+
import type { TitleData } from '$lib/types/title'
|
|
10
|
+
import Button from '../Button.svelte'
|
|
11
|
+
import GridTitle from '../GridTitle.svelte'
|
|
12
|
+
import GridTitleSkeleton from '../GridTitleSkeleton.svelte'
|
|
13
|
+
import ListTitle from '../ListTitle.svelte'
|
|
14
|
+
import ListTitleSkeleton from '../ListTitleSkeleton.svelte'
|
|
15
|
+
import Filter from './Filter/Filter.svelte'
|
|
16
|
+
import Search from './Filter/Search.svelte'
|
|
17
|
+
|
|
18
|
+
const filter: ExploreFilter = $state({})
|
|
19
|
+
|
|
20
|
+
let element: HTMLElement | null = null
|
|
21
|
+
let titles: TitleData[] = $state([])
|
|
22
|
+
let page = 1
|
|
23
|
+
let searchQuery = ''
|
|
24
|
+
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
25
|
+
let promise = $state(getTitlesForFilter())
|
|
26
|
+
let height: string | null = $state(null)
|
|
27
|
+
let width = $state(0)
|
|
28
|
+
|
|
29
|
+
const grid = $derived(width > 500)
|
|
30
|
+
const gridColumns = $derived.by(() => {
|
|
31
|
+
if (width > 1400) return 8
|
|
32
|
+
if (width > 900) return 6
|
|
33
|
+
else if (width > 600) return 4
|
|
34
|
+
return 3
|
|
35
|
+
})
|
|
36
|
+
const TitleComponent = $derived(grid ? GridTitle : ListTitle)
|
|
37
|
+
const SkeletonComponent = $derived(grid ? GridTitleSkeleton : ListTitleSkeleton)
|
|
38
|
+
|
|
39
|
+
$effect(() => {
|
|
40
|
+
// Set the height of this element to match that of it's container. That way we can
|
|
41
|
+
// allow our element to be scrollable, rather than the parent needing to be scrollable.
|
|
42
|
+
height = element?.closest<HTMLElement>(exploreParentSelector)?.style.height || null
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
|
|
46
|
+
let response
|
|
47
|
+
|
|
48
|
+
// If a search query is given we use searchTitles with just the query.
|
|
49
|
+
// If not, we use fetchTitles with the given params for the filter.
|
|
50
|
+
// This is because the backend does not support filters for search results yet.
|
|
51
|
+
// In the future we will likely merge these two, either by adding filters in the backend
|
|
52
|
+
// or by filtering results in the frontend (this seems like the less good option).
|
|
53
|
+
if (searchQuery) {
|
|
54
|
+
const results: TitleData[] = await searchTitles(searchQuery)
|
|
55
|
+
response = { results, next: null, previous: null }
|
|
56
|
+
|
|
57
|
+
titles = results
|
|
58
|
+
} else {
|
|
59
|
+
const params = { page_size: 24, page, ...filter }
|
|
60
|
+
|
|
61
|
+
response = await fetchTitles(params)
|
|
62
|
+
|
|
63
|
+
if (!response?.results) throw new Error('Something went wrong when fetching titles in Explore')
|
|
64
|
+
|
|
65
|
+
titles = [...titles, ...response.results]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return response
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function search(query: string): Promise<void> {
|
|
72
|
+
searchQuery = query
|
|
73
|
+
|
|
74
|
+
if (debounce) clearTimeout(debounce)
|
|
75
|
+
|
|
76
|
+
debounce = setTimeout(() => {
|
|
77
|
+
resetTitles()
|
|
78
|
+
promise = getTitlesForFilter()
|
|
79
|
+
}, 100)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Hooking this up later
|
|
83
|
+
// eslint-disable-next-line no-unused-vars
|
|
84
|
+
function setFilter(key: keyof ExploreFilter, value: any): void {
|
|
85
|
+
// If the value was previous present in the filter and a falsey value is given we remove it from
|
|
86
|
+
// the filter entirely so it will no longer be used as a param
|
|
87
|
+
if (!value && (key in filter)) {
|
|
88
|
+
delete filter[key]
|
|
89
|
+
} else if (value) {
|
|
90
|
+
filter[key] = value
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
resetTitles()
|
|
94
|
+
promise = getTitlesForFilter()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resetTitles(): void {
|
|
98
|
+
page = 1
|
|
99
|
+
titles = []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function fetchMoreTitles(): void {
|
|
103
|
+
page++
|
|
104
|
+
promise = getTitlesForFilter()
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<div class="explore playpilot-styled-scrollbar" class:grid bind:this={element} bind:clientWidth={width} style:height>
|
|
109
|
+
<div class="header" role="banner">
|
|
110
|
+
<div>
|
|
111
|
+
<div class="divider"></div>
|
|
112
|
+
|
|
113
|
+
<div class="heading" use:heading>
|
|
114
|
+
Streaming Guide
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<p class="description">
|
|
118
|
+
Find where to watch movies online - the ultimate guide that helps you find the best movies and shows across streaming services.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{#key grid}
|
|
123
|
+
<Search oninput={search} />
|
|
124
|
+
<Filter {filter} limit={!grid} />
|
|
125
|
+
{/key}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="titles" role="main" style:--grid-columns={gridColumns}>
|
|
129
|
+
{#each titles as title}
|
|
130
|
+
{#key title.sid}
|
|
131
|
+
<TitleComponent {title} onclick={(event: MouseEvent) => openModal({ event, data: title })} />
|
|
132
|
+
{/key}
|
|
133
|
+
{/each}
|
|
134
|
+
|
|
135
|
+
{#await promise}
|
|
136
|
+
{#each { length: 24 } as _}
|
|
137
|
+
<SkeletonComponent />
|
|
138
|
+
{/each}
|
|
139
|
+
{:catch}
|
|
140
|
+
<!-- This is handled below -->
|
|
141
|
+
{/await}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{#await promise then { next }}
|
|
145
|
+
{#if next}
|
|
146
|
+
<div class="show-more">
|
|
147
|
+
<Button size="large" onclick={fetchMoreTitles}>Show more</Button>
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
{:catch}
|
|
151
|
+
Something went wrong
|
|
152
|
+
{/await}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<style lang="scss">
|
|
156
|
+
.explore {
|
|
157
|
+
background: theme(explore-background, light);
|
|
158
|
+
border-radius: theme(border-radius-large);
|
|
159
|
+
max-width: theme(explore-max-width, 1200px);
|
|
160
|
+
margin: 0 auto;
|
|
161
|
+
padding: theme(explore-padding, margin(1) margin(1) margin(2));
|
|
162
|
+
overflow: auto;
|
|
163
|
+
font-family: theme(font-family);
|
|
164
|
+
font-family: theme(detail-font-family, font-family);
|
|
165
|
+
font-weight: theme(detail-font-weight, normal);
|
|
166
|
+
font-size: theme(detail-font-size, font-size-base);
|
|
167
|
+
|
|
168
|
+
:global(*) {
|
|
169
|
+
box-sizing: border-box;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.header {
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: column;
|
|
176
|
+
gap: margin(0.5);
|
|
177
|
+
margin: theme(explore-header-margin, 0 0 margin(2));
|
|
178
|
+
width: 100%;
|
|
179
|
+
|
|
180
|
+
.grid & {
|
|
181
|
+
gap: margin(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.divider {
|
|
186
|
+
width: 100%;
|
|
187
|
+
height: theme(explore-divider-height, 0);
|
|
188
|
+
max-width: theme(explore-header-max-width, 600px);
|
|
189
|
+
background: theme(explore-divider-background, text-color);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.heading {
|
|
193
|
+
margin: theme(explore-heading-margin, margin(0.25) 0);
|
|
194
|
+
color: theme(text-color);
|
|
195
|
+
font-size: theme(explore-heading-size, clamp(margin(1.5), 5vw, margin(2)));
|
|
196
|
+
font-weight: theme(explore-heading-font-weight, font-bold);
|
|
197
|
+
text-transform: theme(explore-heading-text-transform, normal);
|
|
198
|
+
line-height: 1.5;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.description {
|
|
202
|
+
max-width: theme(explore-header-max-width, 600px);
|
|
203
|
+
margin: theme(explore-description-margin, 0 0 margin(1));
|
|
204
|
+
color: theme(text-color-alt);
|
|
205
|
+
font-size: theme(font-size-small);
|
|
206
|
+
line-height: 1.35;
|
|
207
|
+
|
|
208
|
+
.grid & {
|
|
209
|
+
font-size: theme(font-size-base);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.titles {
|
|
214
|
+
--playpilot-list-item-padding: 0;
|
|
215
|
+
--playpilot-list-item-background: transparent;
|
|
216
|
+
--playpilot-list-item-hover-shadow: 0 0 0 #{margin(0.25)} #{theme(lighter)};
|
|
217
|
+
--playpilot-detail-image-background: #{theme(content)};
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: margin(0.5);
|
|
221
|
+
|
|
222
|
+
.grid & {
|
|
223
|
+
display: grid;
|
|
224
|
+
grid-template-columns: repeat(var(--grid-columns), minmax(120px, 1fr));
|
|
225
|
+
gap: margin(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.show-more {
|
|
230
|
+
flex: 0 0 100%;
|
|
231
|
+
display: grid;
|
|
232
|
+
margin-top: margin(1);
|
|
233
|
+
|
|
234
|
+
.grid & {
|
|
235
|
+
margin: margin(2) auto 0;
|
|
236
|
+
max-width: 600px;
|
|
237
|
+
width: 100%;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
</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,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { scale } from 'svelte/transition'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
// eslint-disable-next-line no-unused-vars
|
|
7
|
+
button: Snippet<[{ toggle: (event: MouseEvent) => void }]>,
|
|
8
|
+
content: Snippet
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { button, content }: Props = $props()
|
|
12
|
+
|
|
13
|
+
let active = $state(false)
|
|
14
|
+
let element: HTMLElement | null = $state(null)
|
|
15
|
+
let direction: 'left' | 'right' = $state('left')
|
|
16
|
+
|
|
17
|
+
function closeOnOutsideClick(event: MouseEvent): void {
|
|
18
|
+
const target = event.target as HTMLElement
|
|
19
|
+
|
|
20
|
+
if (!target) return
|
|
21
|
+
if (target.closest('.dropdown') === element) return
|
|
22
|
+
|
|
23
|
+
active = false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toggle(event: MouseEvent): void {
|
|
27
|
+
const target = event.target as HTMLElement
|
|
28
|
+
const leftOffset = target.getBoundingClientRect().left
|
|
29
|
+
|
|
30
|
+
direction = leftOffset < window.innerWidth / 2 ? 'left' : 'right'
|
|
31
|
+
active = !active
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<svelte:window onclick={closeOnOutsideClick} />
|
|
36
|
+
|
|
37
|
+
<div class="dropdown {direction}" bind:this={element}>
|
|
38
|
+
{@render button({ toggle })}
|
|
39
|
+
|
|
40
|
+
{#if active}
|
|
41
|
+
<div class="content playpilot-styled-scrollbar" transition:scale={{ start: 0.85, duration: 100 }}>
|
|
42
|
+
{@render content()}
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<style lang="scss">
|
|
48
|
+
.dropdown {
|
|
49
|
+
position: relative;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.content {
|
|
53
|
+
z-index: 10;
|
|
54
|
+
position: absolute;
|
|
55
|
+
bottom: margin(-0.5);
|
|
56
|
+
right: 0;
|
|
57
|
+
max-height: 60vh;
|
|
58
|
+
border-radius: theme(border-radius);
|
|
59
|
+
box-shadow: theme(shadow-large);
|
|
60
|
+
transform: translateY(100%);
|
|
61
|
+
transform-origin: top right;
|
|
62
|
+
background: theme(content);
|
|
63
|
+
overflow-y: auto;
|
|
64
|
+
overflow-x: hidden;
|
|
65
|
+
|
|
66
|
+
.left & {
|
|
67
|
+
right: auto;
|
|
68
|
+
left: 0;
|
|
69
|
+
transform-origin: top left;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExploreFilter } from '$lib/types/filter'
|
|
3
|
+
import genres from '$lib/data/genres.json'
|
|
4
|
+
import countries from '$lib/data/countries.json'
|
|
5
|
+
import FilterItem from './FilterItem.svelte'
|
|
6
|
+
import FilterSorting from './FilterSorting.svelte'
|
|
7
|
+
import Button from '../../Button.svelte'
|
|
8
|
+
import IconArrow from '../../Icons/IconArrow.svelte'
|
|
9
|
+
import IconFilter from '../../Icons/IconFilter.svelte'
|
|
10
|
+
import { scale } from 'svelte/transition'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
filter: ExploreFilter
|
|
14
|
+
limit?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { filter, limit = true }: Props = $props()
|
|
18
|
+
|
|
19
|
+
const shownItemsLimit = 3
|
|
20
|
+
const items = [{
|
|
21
|
+
label: 'Services',
|
|
22
|
+
param: 'provider',
|
|
23
|
+
data: [{ label: 'Netflix', value: 'netflix' }, { label: 'Disney+', value: 'disney-plus' }],
|
|
24
|
+
}, {
|
|
25
|
+
label: 'Genres',
|
|
26
|
+
param: 'genres',
|
|
27
|
+
data: genres.map(genre => ({ label: genre.name, value: genre.slug })),
|
|
28
|
+
}, {
|
|
29
|
+
label: 'IMDb',
|
|
30
|
+
param: 'imdb',
|
|
31
|
+
}, {
|
|
32
|
+
label: 'Year',
|
|
33
|
+
param: 'year',
|
|
34
|
+
}, {
|
|
35
|
+
label: 'Length',
|
|
36
|
+
param: 'length',
|
|
37
|
+
}, {
|
|
38
|
+
label: 'Countries',
|
|
39
|
+
param: 'country',
|
|
40
|
+
data: countries,
|
|
41
|
+
}]
|
|
42
|
+
|
|
43
|
+
let limited = $state(limit)
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class="filter" role="navigation" class:limit>
|
|
47
|
+
<div class="items">
|
|
48
|
+
{#each limited ? items.slice(0, shownItemsLimit) : items as item}
|
|
49
|
+
<div transition:scale={{ start: 0.75, duration: 100 }}>
|
|
50
|
+
<FilterItem {filter} {...item} />
|
|
51
|
+
</div>
|
|
52
|
+
{/each}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="actions">
|
|
56
|
+
{#if limit}
|
|
57
|
+
<Button variant="link" onclick={() => limited = !limited}>
|
|
58
|
+
<IconFilter />
|
|
59
|
+
All filters
|
|
60
|
+
<IconArrow direction={limited ? 'down' : 'up'} />
|
|
61
|
+
</Button>
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
<div class="sorting">
|
|
65
|
+
<FilterSorting {filter} />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<style lang="scss">
|
|
71
|
+
.filter {
|
|
72
|
+
position: relative;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.items {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
gap: margin(0.5);
|
|
79
|
+
padding-right: margin(7);
|
|
80
|
+
|
|
81
|
+
.limit & {
|
|
82
|
+
padding-right: 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.actions {
|
|
87
|
+
position: absolute;
|
|
88
|
+
top: margin(0.25);
|
|
89
|
+
right: 0;
|
|
90
|
+
|
|
91
|
+
.limit & {
|
|
92
|
+
position: initial;
|
|
93
|
+
top: auto;
|
|
94
|
+
display: flex;
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
margin-top: margin(0.5);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../../Button.svelte'
|
|
3
|
+
import IconArrow from '../../Icons/IconArrow.svelte'
|
|
4
|
+
import TogglesWithSearch from './TogglesWithSearch.svelte'
|
|
5
|
+
import type { ExploreFilter } from '$lib/types/filter'
|
|
6
|
+
import Dropdown from './Dropdown.svelte'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
filter: ExploreFilter,
|
|
10
|
+
label: string,
|
|
11
|
+
param: string,
|
|
12
|
+
data?: { label: string, value: string }[] | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { filter, label, param, data = null }: Props = $props()
|
|
16
|
+
|
|
17
|
+
const active = $derived(!!filter[param])
|
|
18
|
+
|
|
19
|
+
function setFilter(value: string): void {
|
|
20
|
+
if (value) filter[param] = value
|
|
21
|
+
else delete filter[param]
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div class="filter-item" class:active data-testid="filter-item">
|
|
26
|
+
<Dropdown>
|
|
27
|
+
{#snippet button({ toggle })}
|
|
28
|
+
<Button variant="border" size="large" {active} onclick={toggle}>
|
|
29
|
+
{label}
|
|
30
|
+
<IconArrow direction="down" />
|
|
31
|
+
</Button>
|
|
32
|
+
{/snippet}
|
|
33
|
+
|
|
34
|
+
{#snippet content()}
|
|
35
|
+
<div class="content" data-testid="content">
|
|
36
|
+
{#if data}
|
|
37
|
+
{@const selected = filter[param]?.split(',') || []}
|
|
38
|
+
<TogglesWithSearch options={data} {selected} onchange={(selected) => setFilter(selected.join(','))} />
|
|
39
|
+
{:else}
|
|
40
|
+
Some range filter
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
{/snippet}
|
|
44
|
+
</Dropdown>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<style lang="scss">
|
|
48
|
+
.filter-item {
|
|
49
|
+
&.active :global(.dropdown > button) {
|
|
50
|
+
box-shadow: inset 0 0 0 1px theme(green);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.content {
|
|
55
|
+
width: margin(15);
|
|
56
|
+
}
|
|
57
|
+
</style>
|