@playpilot/tpi 7.3.0 → 8.0.0-beta.explore-home.3
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/.env +1 -0
- package/dist/editorial.mount.js +9 -9
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +8 -8
- package/events.md +1 -5
- package/package.json +1 -1
- package/src/lib/api/youtubeAvailability.ts +26 -0
- package/src/lib/enums/TrackingEvent.ts +1 -0
- package/src/lib/explore.ts +3 -21
- package/src/lib/modal.ts +3 -3
- package/src/lib/trailer.ts +31 -0
- package/src/lib/types/config.d.ts +0 -1
- package/src/lib/types/explore.d.ts +5 -0
- package/src/routes/components/Button.svelte +2 -1
- package/src/routes/components/Explore/ExploreLayout.svelte +102 -0
- package/src/routes/components/Explore/ExploreModal.svelte +2 -2
- package/src/routes/components/Explore/ExploreRouter.svelte +35 -0
- package/src/routes/components/Explore/Routes/ExploreHome.svelte +23 -0
- package/src/routes/components/Explore/{Explore.svelte → Routes/ExploreResults.svelte} +46 -115
- package/src/routes/components/Rails/TitlesRail.svelte +125 -14
- package/src/routes/components/Title.svelte +27 -11
- package/src/routes/components/TitleModal.svelte +2 -2
- package/src/routes/components/YouTubeEmbed.svelte +36 -0
- package/src/routes/components/YouTubeEmbedBackground.svelte +34 -0
- package/src/routes/components/YouTubeEmbedOverlay.svelte +14 -29
- package/src/routes/elements/+page.svelte +12 -2
- package/src/routes/explore/+page.svelte +1 -2
- package/src/tests/lib/api/youtubeAvailability.test.js +70 -0
- package/src/tests/lib/explore.test.js +1 -38
- package/src/tests/lib/trailer.test.js +57 -1
- package/src/tests/routes/components/Explore/ExploreLayout.test.js +52 -0
- package/src/tests/routes/components/Explore/ExploreRouter.test.js +20 -0
- package/src/tests/routes/components/Explore/Routes/ExploreHome.test.js +18 -0
- package/src/tests/routes/components/Explore/{Explore.test.js → Routes/ExploreResults.test.js} +29 -22
- package/src/tests/routes/components/Rails/TitlesRail.test.js +79 -2
- package/src/tests/routes/components/Title.test.js +35 -0
- package/src/tests/routes/components/YouTubeEmbed.test.js +38 -0
- package/src/tests/routes/components/YouTubeEmbedBackground.test.js +13 -0
- package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +1 -8
- package/vite._main.config.js +0 -1
package/events.md
CHANGED
|
@@ -9,11 +9,6 @@ All events share a common payload:
|
|
|
9
9
|
- `organization_sid`: The sid for the related organization
|
|
10
10
|
- `domain_sid`: The sid for the related domain
|
|
11
11
|
- `device`: Basic device info containing the screen type, size, touch, and orientation
|
|
12
|
-
- `type`: "desktop" or "mobile"
|
|
13
|
-
- `width`: Screen width in pixels
|
|
14
|
-
- `height`: Screen height in pixels
|
|
15
|
-
- `touch`: Whether the device uses touch controls or not
|
|
16
|
-
- `orientation`: "landscape-primary", "landscape-secondary", "portrait-primary", "portrait-secondary", or "undefined"
|
|
17
12
|
|
|
18
13
|
Events related to titles share an additional set of data (referred to below as `Title`):
|
|
19
14
|
|
|
@@ -99,6 +94,7 @@ Event | Action | Info | Payload
|
|
|
99
94
|
`ali_trailer_button_click` | _Fires any time the trailer button is clicked for a title_ | | `Title`
|
|
100
95
|
`ali_expand_title_description` | _Fires any time the "show more" button is clicked for a title description_ | | `Title`
|
|
101
96
|
`ali_region_request_failed` | _Fires when requests to external service for getting the user region fails_ | | `message` (error message as returned by the request)
|
|
97
|
+
`ali_youtube_availability_request_failed` | _Fires when requests to external service for getting the youtube availability fails_ | | `message` (error message as returned by the request)
|
|
102
98
|
|
|
103
99
|
### Explore
|
|
104
100
|
Event | Action | Info | Payload
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PUBLIC_YOUTUBE_AVAILABILITY_URL } from '$env/static/public'
|
|
2
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
|
+
import { track } from '$lib/tracking'
|
|
4
|
+
|
|
5
|
+
export async function isYouTubeVideoAvailableInRegion(videoId: string): Promise<boolean> {
|
|
6
|
+
const region = window.PlayPilotLinkInjections?.region?.toUpperCase()
|
|
7
|
+
|
|
8
|
+
if (!region) return true
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(`${PUBLIC_YOUTUBE_AVAILABILITY_URL}/?video_id=${videoId}`)
|
|
12
|
+
|
|
13
|
+
if (!response.ok) throw new Error('Response was not ok')
|
|
14
|
+
|
|
15
|
+
const data = await response.json()
|
|
16
|
+
|
|
17
|
+
if (data.blocked?.[region]) return false
|
|
18
|
+
if (!data.blocked && !!data.allowed && !data.allowed?.includes(region)) return false
|
|
19
|
+
|
|
20
|
+
return true
|
|
21
|
+
} catch (error: any) {
|
|
22
|
+
track(TrackingEvent.YouTubeAvailabilityRequestFailed, null, { message: error.message })
|
|
23
|
+
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -60,6 +60,7 @@ export const TrackingEvent = {
|
|
|
60
60
|
TrailerClick: 'ali_trailer_button_click',
|
|
61
61
|
ExpandTitleDescription: 'ali_expand_title_description',
|
|
62
62
|
RegionRequestFailed: 'ali_region_request_failed',
|
|
63
|
+
YouTubeAvailabilityRequestFailed: 'ali_youtube_availability_request_failed',
|
|
63
64
|
|
|
64
65
|
// Explore
|
|
65
66
|
// These deliberately do not use the `ali_` prefix. We might want to separate this from TPI at some point
|
package/src/lib/explore.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { mount, unmount } from 'svelte'
|
|
2
|
-
import
|
|
2
|
+
import ExploreRouter from '../routes/components/Explore/ExploreRouter.svelte'
|
|
3
3
|
|
|
4
4
|
export const exploreParentSelector = '[data-playpilot-explore]'
|
|
5
5
|
export const explorePreConsentSelector = '[data-playpilot-pre-consent-explore]'
|
|
6
|
-
export const exploreCustomStyleId = 'playpilot-explore-custom-style'
|
|
7
6
|
|
|
8
7
|
let exploreInsertedComponent: object | null = null
|
|
9
8
|
|
|
@@ -19,9 +18,7 @@ export function insertExplore(): void {
|
|
|
19
18
|
document.querySelector<HTMLElement>(explorePreConsentSelector)?.remove()
|
|
20
19
|
|
|
21
20
|
target.innerHTML = ''
|
|
22
|
-
exploreInsertedComponent = mount(
|
|
23
|
-
|
|
24
|
-
insertExploreCustomStyle()
|
|
21
|
+
exploreInsertedComponent = mount(ExploreRouter, { target })
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
export function destroyExplore(): void {
|
|
@@ -29,8 +26,6 @@ export function destroyExplore(): void {
|
|
|
29
26
|
|
|
30
27
|
unmount(exploreInsertedComponent)
|
|
31
28
|
exploreInsertedComponent = null
|
|
32
|
-
|
|
33
|
-
document.querySelector('#' + exploreCustomStyleId)?.remove()
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
/**
|
|
@@ -42,7 +37,7 @@ export function insertExploreIntoNavigation(): boolean {
|
|
|
42
37
|
const config = window.PlayPilotLinkInjections.config
|
|
43
38
|
if (!config) return false
|
|
44
39
|
|
|
45
|
-
const { explore_navigation_selector: selector, explore_navigation_label: label, explore_navigation_path: path, explore_navigation_insert_position: insertPosition } = config
|
|
40
|
+
const { explore_navigation_selector: selector, explore_navigation_label: label, explore_navigation_path: path, explore_navigation_insert_position: insertPosition } = window.PlayPilotLinkInjections.config
|
|
46
41
|
if (!selector) return false
|
|
47
42
|
|
|
48
43
|
// Make sure to remove an element we created if it already exists. Should not be possible under normal circumstances.
|
|
@@ -64,16 +59,3 @@ export function insertExploreIntoNavigation(): boolean {
|
|
|
64
59
|
|
|
65
60
|
return true
|
|
66
61
|
}
|
|
67
|
-
|
|
68
|
-
function insertExploreCustomStyle(): void {
|
|
69
|
-
const customStyleString = window.PlayPilotLinkInjections.config?.explore_custom_style
|
|
70
|
-
|
|
71
|
-
if (!customStyleString) return
|
|
72
|
-
|
|
73
|
-
const styleElement = document.createElement('style')
|
|
74
|
-
|
|
75
|
-
styleElement.textContent = customStyleString
|
|
76
|
-
styleElement.id = exploreCustomStyleId
|
|
77
|
-
|
|
78
|
-
document.body.appendChild(styleElement)
|
|
79
|
-
}
|
package/src/lib/modal.ts
CHANGED
|
@@ -29,8 +29,8 @@ const modals: Modal[] = []
|
|
|
29
29
|
* Ignore clicks that used modifier keys or that were not left click.
|
|
30
30
|
*/
|
|
31
31
|
export function openModal(
|
|
32
|
-
{ type = 'title', event = null, injection = null, data = null, scrollPosition = 0, returnToTitle = null }:
|
|
33
|
-
{ type?: ModalType, event?: MouseEvent | null, injection?: LinkInjection | null, data?: TitleData | ParticipantData | null, scrollPosition?: number, returnToTitle?: TitleData | null } = {}): void {
|
|
32
|
+
{ type = 'title', event = null, injection = null, data = null, scrollPosition = 0, returnToTitle = null, props = {} }:
|
|
33
|
+
{ type?: ModalType, event?: MouseEvent | null, injection?: LinkInjection | null, data?: TitleData | ParticipantData | null, scrollPosition?: number, returnToTitle?: TitleData | null, props?: Record<string, any> } = {}): void {
|
|
34
34
|
if (event && isHoldingSpecialKey(event)) return
|
|
35
35
|
|
|
36
36
|
event?.preventDefault()
|
|
@@ -38,7 +38,7 @@ export function openModal(
|
|
|
38
38
|
if (modals?.length) closeCurrentModal()
|
|
39
39
|
|
|
40
40
|
const target = getPlayPilotWrapperElement()
|
|
41
|
-
const sharedProps = { initialScrollPosition: scrollPosition }
|
|
41
|
+
const sharedProps = { initialScrollPosition: scrollPosition, ...props }
|
|
42
42
|
const component = getModalComponentByType({ type, target, data, props: sharedProps })
|
|
43
43
|
|
|
44
44
|
// When a return title is given it is added to the list of modals but is not actually opened.
|
package/src/lib/trailer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mount, unmount } from 'svelte'
|
|
|
2
2
|
import { getPlayPilotWrapperElement } from './injection'
|
|
3
3
|
import type { TitleData } from './types/title'
|
|
4
4
|
import YouTubeEmbedOverlay from '../routes/components/YouTubeEmbedOverlay.svelte'
|
|
5
|
+
import { isYouTubeVideoAvailableInRegion } from './api/youtubeAvailability'
|
|
5
6
|
|
|
6
7
|
let currentTrailerComponent: object | null = {}
|
|
7
8
|
|
|
@@ -19,3 +20,33 @@ export function closeTrailerOverlay(): void {
|
|
|
19
20
|
|
|
20
21
|
currentTrailerComponent = null
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
// Gets the YouTube ID from a url, can be a large number of different formats
|
|
25
|
+
// https://stackoverflow.com/a/54200105/1665157
|
|
26
|
+
export function getVideoId(url: string): string | null {
|
|
27
|
+
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
|
|
28
|
+
const match = url.match(regExp)
|
|
29
|
+
|
|
30
|
+
return match?.[7] || null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getFirstTitleWithAvailableTrailer(titles: TitleData[], limit = 3): Promise<TitleData | null> {
|
|
34
|
+
let index = 0
|
|
35
|
+
let titleWithAvailableTrailer: TitleData | null = null
|
|
36
|
+
|
|
37
|
+
while (index < limit && !titleWithAvailableTrailer) {
|
|
38
|
+
const title = titles[index]
|
|
39
|
+
const videoId = title?.embeddable_url && getVideoId(title.embeddable_url)
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const isTrailerAvailable = videoId && await isYouTubeVideoAvailableInRegion(videoId)
|
|
43
|
+
if (isTrailerAvailable) titleWithAvailableTrailer = title
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
index++
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return titleWithAvailableTrailer
|
|
52
|
+
}
|
|
@@ -67,11 +67,12 @@
|
|
|
67
67
|
&:hover,
|
|
68
68
|
&:active,
|
|
69
69
|
&.active {
|
|
70
|
-
background: theme(button-border-color, lighter);
|
|
70
|
+
background: theme(button-hover-border-color, lighter);
|
|
71
71
|
color: theme(button-hover-text-color, text-color);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
&.active {
|
|
75
|
+
box-shadow: inset 0 0 0 1px theme(button-active-border-color, text-color-alt);
|
|
75
76
|
filter: brightness(theme(hover-filter-brightness));
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { heading } from '$lib/actions/heading'
|
|
3
|
+
import { exploreParentSelector } from '$lib/explore'
|
|
4
|
+
import { t } from '$lib/localization'
|
|
5
|
+
import type { ExploreRoute } from '$lib/types/explore'
|
|
6
|
+
import type { Snippet } from 'svelte'
|
|
7
|
+
import Search from './Filter/Search.svelte'
|
|
8
|
+
import Button from '../Button.svelte'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
routes: ExploreRoute[]
|
|
12
|
+
currentRoute: ExploreRoute
|
|
13
|
+
navigate?: (route: ExploreRoute) => void
|
|
14
|
+
searchQuery?: string
|
|
15
|
+
children: Snippet
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { routes, currentRoute, navigate = () => null, searchQuery = $bindable(''), children }: Props = $props()
|
|
19
|
+
|
|
20
|
+
let element: HTMLElement | null = null
|
|
21
|
+
let height: string | null = $state(null)
|
|
22
|
+
|
|
23
|
+
$effect(() => {
|
|
24
|
+
// Set the height of this element to match that of it's container. That way we can
|
|
25
|
+
// allow our element to be scrollable, rather than the parent needing to be scrollable.
|
|
26
|
+
height = element?.closest<HTMLElement>(exploreParentSelector)?.style.height || null
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="explore playpilot-styled-scrollbar" bind:this={element} style:height>
|
|
31
|
+
<div class="header" role="banner">
|
|
32
|
+
<div>
|
|
33
|
+
<div class="divider"></div>
|
|
34
|
+
|
|
35
|
+
<div class="heading" use:heading>
|
|
36
|
+
{t('Streaming Guide')}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="navigation" role="navigation">
|
|
42
|
+
{#each routes as route}
|
|
43
|
+
<Button variant="border" size="large" active={currentRoute.key === route.key} onclick={() => navigate(route)}>
|
|
44
|
+
{t(route.label)}
|
|
45
|
+
</Button>
|
|
46
|
+
{/each}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<Search oninput={(query) => searchQuery = query} />
|
|
50
|
+
|
|
51
|
+
{@render children()}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<style lang="scss">
|
|
55
|
+
.explore {
|
|
56
|
+
background: theme(explore-background, light);
|
|
57
|
+
border-radius: theme(border-radius-large);
|
|
58
|
+
max-width: theme(explore-max-width, 1200px);
|
|
59
|
+
min-height: 75vh;
|
|
60
|
+
margin: 0 auto;
|
|
61
|
+
padding: theme(explore-padding, margin(1) margin(1) margin(6));
|
|
62
|
+
overflow: auto;
|
|
63
|
+
font-family: theme(font-family);
|
|
64
|
+
font-family: theme(detail-font-family, font-family);
|
|
65
|
+
font-weight: theme(detail-font-weight, normal);
|
|
66
|
+
font-size: theme(detail-font-size, font-size-base);
|
|
67
|
+
|
|
68
|
+
:global(*) {
|
|
69
|
+
box-sizing: border-box;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.header {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: margin(0.5);
|
|
77
|
+
margin: theme(explore-header-margin, 0 0 margin(2));
|
|
78
|
+
width: 100%;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.divider {
|
|
82
|
+
width: 100%;
|
|
83
|
+
height: theme(explore-divider-height, 0);
|
|
84
|
+
max-width: theme(explore-header-max-width, 600px);
|
|
85
|
+
background: theme(explore-divider-background, text-color);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.heading {
|
|
89
|
+
margin: theme(explore-heading-margin, margin(0.25) 0);
|
|
90
|
+
color: theme(text-color);
|
|
91
|
+
font-size: theme(explore-heading-size, clamp(margin(1.5), 5vw, margin(2)));
|
|
92
|
+
font-weight: theme(explore-heading-font-weight, font-bold);
|
|
93
|
+
text-transform: theme(explore-heading-text-transform, normal);
|
|
94
|
+
line-height: theme(explore-heading-line-height, 1.5);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.navigation {
|
|
98
|
+
display: flex;
|
|
99
|
+
gap: margin(0.5);
|
|
100
|
+
margin-bottom: margin(0.5);
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import Modal from '../Modal.svelte'
|
|
4
|
-
import
|
|
4
|
+
import ExploreRouter from './ExploreRouter.svelte'
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
initialScrollPosition?: number
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<Modal {initialScrollPosition} closeButtonStyle="flat" wide>
|
|
14
14
|
<div class="content">
|
|
15
|
-
<
|
|
15
|
+
<ExploreRouter />
|
|
16
16
|
</div>
|
|
17
17
|
</Modal>
|
|
18
18
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ExploreHome from './Routes/ExploreHome.svelte'
|
|
3
|
+
import ExploreResults from './Routes/ExploreResults.svelte'
|
|
4
|
+
import type { ExploreRoute } from '$lib/types/explore'
|
|
5
|
+
import ExploreLayout from './ExploreLayout.svelte'
|
|
6
|
+
|
|
7
|
+
const routes: ExploreRoute[] = [
|
|
8
|
+
{
|
|
9
|
+
key: 'home',
|
|
10
|
+
label: 'Home',
|
|
11
|
+
component: ExploreHome,
|
|
12
|
+
}, {
|
|
13
|
+
key: 'results',
|
|
14
|
+
label: 'Explore',
|
|
15
|
+
component: ExploreResults,
|
|
16
|
+
},
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
let currentRoute: ExploreRoute = $state(routes[0])
|
|
20
|
+
let searchQuery: string = $state('')
|
|
21
|
+
|
|
22
|
+
const CurrentRouteComponent = $derived(currentRoute.component)
|
|
23
|
+
|
|
24
|
+
$effect(() => {
|
|
25
|
+
if (searchQuery) currentRoute = routes.find(route => route.key === 'results')!
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function navigate(route: ExploreRoute): void {
|
|
29
|
+
currentRoute = route
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<ExploreLayout {routes} {currentRoute} {navigate} bind:searchQuery>
|
|
34
|
+
<CurrentRouteComponent {searchQuery} />
|
|
35
|
+
</ExploreLayout>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { fetchSimilarTitles } from '$lib/api/titles'
|
|
3
|
+
import { title } from '$lib/fakeData'
|
|
4
|
+
import TitlesRail from '../../Rails/TitlesRail.svelte'
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div data-testid="explore-home"></div>
|
|
8
|
+
|
|
9
|
+
{#await fetchSimilarTitles(title)}
|
|
10
|
+
Loading...
|
|
11
|
+
{:then titles}
|
|
12
|
+
{#if titles}
|
|
13
|
+
<TitlesRail heading="Some titles" {titles} expandable />
|
|
14
|
+
|
|
15
|
+
{#if titles[1]}
|
|
16
|
+
<TitlesRail heading="Some other titles" titles={fetchSimilarTitles(titles[1])} expandable />
|
|
17
|
+
{/if}
|
|
18
|
+
|
|
19
|
+
{#if titles[5]}
|
|
20
|
+
<TitlesRail heading="Some more titles" titles={fetchSimilarTitles(titles[5])} expandable />
|
|
21
|
+
{/if}
|
|
22
|
+
{/if}
|
|
23
|
+
{/await}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { heading } from '$lib/actions/heading'
|
|
3
2
|
import { fetchAds } from '$lib/api/ads'
|
|
4
3
|
import { fetchTitles } from '$lib/api/titles'
|
|
5
4
|
import { MetaEvent, TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
6
|
-
import { exploreParentSelector } from '$lib/explore'
|
|
7
5
|
import { openModal } from '$lib/modal'
|
|
8
6
|
import { track } from '$lib/tracking'
|
|
9
7
|
import type { APIPaginatedResult } from '$lib/types/api'
|
|
@@ -13,25 +11,27 @@
|
|
|
13
11
|
import { trackViaPixel } from '@playpilot/retargeting-tracking'
|
|
14
12
|
import { onMount } from 'svelte'
|
|
15
13
|
import { t } from '$lib/localization'
|
|
16
|
-
import Button from '
|
|
17
|
-
import GridTitle from '
|
|
18
|
-
import GridTitleSkeleton from '
|
|
19
|
-
import ListTitle from '
|
|
20
|
-
import ListTitleSkeleton from '
|
|
21
|
-
import Filter from '
|
|
22
|
-
import
|
|
23
|
-
|
|
14
|
+
import Button from '../../Button.svelte'
|
|
15
|
+
import GridTitle from '../../GridTitle.svelte'
|
|
16
|
+
import GridTitleSkeleton from '../../GridTitleSkeleton.svelte'
|
|
17
|
+
import ListTitle from '../../ListTitle.svelte'
|
|
18
|
+
import ListTitleSkeleton from '../../ListTitleSkeleton.svelte'
|
|
19
|
+
import Filter from '../Filter/Filter.svelte'
|
|
20
|
+
import Empty from '../Empty.svelte'
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
searchQuery?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { searchQuery = '' }: Props = $props()
|
|
24
27
|
|
|
25
28
|
const filter: ExploreFilter = $state({})
|
|
26
29
|
|
|
27
|
-
let element: HTMLElement | null = null
|
|
28
30
|
let titles: TitleData[] = $state([])
|
|
29
31
|
let page = $state(1)
|
|
30
32
|
let debounce: ReturnType<typeof setTimeout> | null = null
|
|
31
33
|
let latestRequestId = 0
|
|
32
|
-
let searchQuery = $state('')
|
|
33
34
|
let promise = $state(getTitlesForFilter())
|
|
34
|
-
let height: string | null = $state(null)
|
|
35
35
|
let width = $state(0)
|
|
36
36
|
|
|
37
37
|
const grid = $derived(width > 500)
|
|
@@ -45,9 +45,7 @@
|
|
|
45
45
|
const SkeletonComponent = $derived(grid ? GridTitleSkeleton : ListTitleSkeleton)
|
|
46
46
|
|
|
47
47
|
$effect(() => {
|
|
48
|
-
|
|
49
|
-
// allow our element to be scrollable, rather than the parent needing to be scrollable.
|
|
50
|
-
height = element?.closest<HTMLElement>(exploreParentSelector)?.style.height || null
|
|
48
|
+
if (searchQuery) search(searchQuery)
|
|
51
49
|
})
|
|
52
50
|
|
|
53
51
|
onMount(async () => {
|
|
@@ -85,8 +83,6 @@
|
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
async function search(query: string): Promise<void> {
|
|
88
|
-
searchQuery = query
|
|
89
|
-
|
|
90
86
|
if (debounce) clearTimeout(debounce)
|
|
91
87
|
|
|
92
88
|
debounce = setTimeout(() => {
|
|
@@ -123,114 +119,49 @@
|
|
|
123
119
|
}
|
|
124
120
|
</script>
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
<div class="
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<div class="heading" use:heading>
|
|
132
|
-
{t('Streaming Guide')}
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
<p class="description">
|
|
136
|
-
{t('Streaming Guide Description')}
|
|
137
|
-
</p>
|
|
138
|
-
</div>
|
|
122
|
+
{#key grid}
|
|
123
|
+
<div class="filter">
|
|
124
|
+
<Filter {filter} limit={!grid} onchange={setFilter} showSorting={!searchQuery} />
|
|
125
|
+
</div>
|
|
126
|
+
{/key}
|
|
139
127
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
128
|
+
<div class="titles" class:grid role="main" style:--grid-columns={gridColumns} bind:clientWidth={width} data-testid="explore-results">
|
|
129
|
+
{#each titles as title}
|
|
130
|
+
{#key title.sid}
|
|
131
|
+
<TitleComponent {title} onclick={(event: MouseEvent) => openTitle(event, title)} />
|
|
143
132
|
{/key}
|
|
144
|
-
|
|
133
|
+
{/each}
|
|
145
134
|
|
|
146
|
-
|
|
147
|
-
{#each
|
|
148
|
-
|
|
149
|
-
<TitleComponent {title} onclick={(event: MouseEvent) => openTitle(event, title)} />
|
|
150
|
-
{/key}
|
|
135
|
+
{#await promise}
|
|
136
|
+
{#each { length: 24 } as _}
|
|
137
|
+
<SkeletonComponent />
|
|
151
138
|
{/each}
|
|
152
|
-
|
|
153
|
-
{#await promise}
|
|
154
|
-
{#each { length: 24 } as _}
|
|
155
|
-
<SkeletonComponent />
|
|
156
|
-
{/each}
|
|
157
|
-
{:catch}
|
|
158
|
-
<!-- This is handled below -->
|
|
159
|
-
{/await}
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
{#await promise then { next }}
|
|
163
|
-
{#if next}
|
|
164
|
-
<div class="show-more">
|
|
165
|
-
<Button size="large" onclick={fetchMoreTitles}>{t('Show More')}</Button>
|
|
166
|
-
</div>
|
|
167
|
-
{/if}
|
|
168
|
-
|
|
169
|
-
{#if !titles?.length && page === 1}
|
|
170
|
-
<Empty />
|
|
171
|
-
{/if}
|
|
172
139
|
{:catch}
|
|
173
|
-
|
|
140
|
+
<!-- This is handled below -->
|
|
174
141
|
{/await}
|
|
175
142
|
</div>
|
|
176
143
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
:global(*) {
|
|
192
|
-
box-sizing: border-box;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
144
|
+
{#await promise then { next }}
|
|
145
|
+
{#if next}
|
|
146
|
+
<div class="show-more" class:grid>
|
|
147
|
+
<Button size="large" onclick={fetchMoreTitles}>{t('Show More')}</Button>
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
|
|
151
|
+
{#if !titles?.length && page === 1}
|
|
152
|
+
<Empty />
|
|
153
|
+
{/if}
|
|
154
|
+
{:catch}
|
|
155
|
+
<p>Something went wrong</p>
|
|
156
|
+
{/await}
|
|
195
157
|
|
|
196
|
-
|
|
158
|
+
<style lang="scss">
|
|
159
|
+
.filter {
|
|
197
160
|
display: flex;
|
|
198
161
|
flex-direction: column;
|
|
199
162
|
gap: margin(0.5);
|
|
200
|
-
margin: theme(explore-header-margin, 0 0 margin(2));
|
|
201
163
|
width: 100%;
|
|
202
|
-
|
|
203
|
-
.grid & {
|
|
204
|
-
gap: margin(1);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
.divider {
|
|
209
|
-
width: 100%;
|
|
210
|
-
height: theme(explore-divider-height, 0);
|
|
211
|
-
max-width: theme(explore-header-max-width, 600px);
|
|
212
|
-
background: theme(explore-divider-background, text-color);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.heading {
|
|
216
|
-
margin: theme(explore-heading-margin, margin(0.25) 0);
|
|
217
|
-
color: theme(text-color);
|
|
218
|
-
font-size: theme(explore-heading-size, clamp(margin(1.5), 5vw, margin(2)));
|
|
219
|
-
font-weight: theme(explore-heading-font-weight, font-bold);
|
|
220
|
-
text-transform: theme(explore-heading-text-transform, normal);
|
|
221
|
-
line-height: theme(explore-heading-line-height, 1.5);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.description {
|
|
225
|
-
max-width: theme(explore-header-max-width, 600px);
|
|
226
|
-
margin: theme(explore-description-margin, 0 0 margin(1));
|
|
227
|
-
color: theme(text-color-alt);
|
|
228
|
-
font-size: theme(font-size-small);
|
|
229
|
-
line-height: 1.35;
|
|
230
|
-
|
|
231
|
-
.grid & {
|
|
232
|
-
font-size: theme(font-size-base);
|
|
233
|
-
}
|
|
164
|
+
margin: margin(0.5) 0 margin(2);
|
|
234
165
|
}
|
|
235
166
|
|
|
236
167
|
.titles {
|
|
@@ -242,7 +173,7 @@
|
|
|
242
173
|
flex-direction: column;
|
|
243
174
|
gap: margin(0.5);
|
|
244
175
|
|
|
245
|
-
|
|
176
|
+
&.grid {
|
|
246
177
|
display: grid;
|
|
247
178
|
grid-template-columns: repeat(var(--grid-columns), minmax(120px, 1fr));
|
|
248
179
|
gap: margin(1);
|
|
@@ -254,7 +185,7 @@
|
|
|
254
185
|
display: grid;
|
|
255
186
|
margin-top: margin(1);
|
|
256
187
|
|
|
257
|
-
|
|
188
|
+
&.grid {
|
|
258
189
|
margin: margin(2) auto 0;
|
|
259
190
|
max-width: 600px;
|
|
260
191
|
width: 100%;
|