@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.
Files changed (40) hide show
  1. package/.env +1 -0
  2. package/dist/editorial.mount.js +9 -9
  3. package/dist/link-injections.js +1 -1
  4. package/dist/mount.js +8 -8
  5. package/events.md +1 -5
  6. package/package.json +1 -1
  7. package/src/lib/api/youtubeAvailability.ts +26 -0
  8. package/src/lib/enums/TrackingEvent.ts +1 -0
  9. package/src/lib/explore.ts +3 -21
  10. package/src/lib/modal.ts +3 -3
  11. package/src/lib/trailer.ts +31 -0
  12. package/src/lib/types/config.d.ts +0 -1
  13. package/src/lib/types/explore.d.ts +5 -0
  14. package/src/routes/components/Button.svelte +2 -1
  15. package/src/routes/components/Explore/ExploreLayout.svelte +102 -0
  16. package/src/routes/components/Explore/ExploreModal.svelte +2 -2
  17. package/src/routes/components/Explore/ExploreRouter.svelte +35 -0
  18. package/src/routes/components/Explore/Routes/ExploreHome.svelte +23 -0
  19. package/src/routes/components/Explore/{Explore.svelte → Routes/ExploreResults.svelte} +46 -115
  20. package/src/routes/components/Rails/TitlesRail.svelte +125 -14
  21. package/src/routes/components/Title.svelte +27 -11
  22. package/src/routes/components/TitleModal.svelte +2 -2
  23. package/src/routes/components/YouTubeEmbed.svelte +36 -0
  24. package/src/routes/components/YouTubeEmbedBackground.svelte +34 -0
  25. package/src/routes/components/YouTubeEmbedOverlay.svelte +14 -29
  26. package/src/routes/elements/+page.svelte +12 -2
  27. package/src/routes/explore/+page.svelte +1 -2
  28. package/src/tests/lib/api/youtubeAvailability.test.js +70 -0
  29. package/src/tests/lib/explore.test.js +1 -38
  30. package/src/tests/lib/trailer.test.js +57 -1
  31. package/src/tests/routes/components/Explore/ExploreLayout.test.js +52 -0
  32. package/src/tests/routes/components/Explore/ExploreRouter.test.js +20 -0
  33. package/src/tests/routes/components/Explore/Routes/ExploreHome.test.js +18 -0
  34. package/src/tests/routes/components/Explore/{Explore.test.js → Routes/ExploreResults.test.js} +29 -22
  35. package/src/tests/routes/components/Rails/TitlesRail.test.js +79 -2
  36. package/src/tests/routes/components/Title.test.js +35 -0
  37. package/src/tests/routes/components/YouTubeEmbed.test.js +38 -0
  38. package/src/tests/routes/components/YouTubeEmbedBackground.test.js +13 -0
  39. package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +1 -8
  40. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "7.3.0",
3
+ "version": "8.0.0-beta.explore-home.3",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -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
@@ -1,9 +1,8 @@
1
1
  import { mount, unmount } from 'svelte'
2
- import Explore from '../routes/components/Explore/Explore.svelte'
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(Explore, { target })
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.
@@ -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
+ }
@@ -100,5 +100,4 @@ export type ConfigResponse = {
100
100
  explore_navigation_path?: string
101
101
  explore_navigation_insert_position?: InsertPosition
102
102
  explore_insert_cta_in_tpi?: boolean
103
- explore_custom_style?: string
104
103
  }
@@ -0,0 +1,5 @@
1
+ export type ExploreRoute = {
2
+ key: string
3
+ label: string
4
+ component: any
5
+ }
@@ -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 Explore from './Explore.svelte'
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
- <Explore />
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 '../Button.svelte'
17
- import GridTitle from '../GridTitle.svelte'
18
- import GridTitleSkeleton from '../GridTitleSkeleton.svelte'
19
- import ListTitle from '../ListTitle.svelte'
20
- import ListTitleSkeleton from '../ListTitleSkeleton.svelte'
21
- import Filter from './Filter/Filter.svelte'
22
- import Search from './Filter/Search.svelte'
23
- import Empty from './Empty.svelte'
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
- // Set the height of this element to match that of it's container. That way we can
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
- <div class="explore playpilot-styled-scrollbar" class:grid bind:this={element} bind:clientWidth={width} style:height>
127
- <div class="header" role="banner">
128
- <div>
129
- <div class="divider"></div>
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
- {#key grid}
141
- <Search oninput={search} />
142
- <Filter {filter} limit={!grid} onchange={setFilter} showSorting={!searchQuery} />
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
- </div>
133
+ {/each}
145
134
 
146
- <div class="titles" role="main" style:--grid-columns={gridColumns}>
147
- {#each titles as title}
148
- {#key title.sid}
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
- <p>Something went wrong</p>
140
+ <!-- This is handled below -->
174
141
  {/await}
175
142
  </div>
176
143
 
177
- <style lang="scss">
178
- .explore {
179
- background: theme(explore-background, light);
180
- border-radius: theme(border-radius-large);
181
- max-width: theme(explore-max-width, 1200px);
182
- min-height: 75vh;
183
- margin: 0 auto;
184
- padding: theme(explore-padding, margin(1) margin(1) margin(6));
185
- overflow: auto;
186
- font-family: theme(font-family);
187
- font-family: theme(detail-font-family, font-family);
188
- font-weight: theme(detail-font-weight, normal);
189
- font-size: theme(detail-font-size, font-size-base);
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
- .header {
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
- .grid & {
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
- .grid & {
188
+ &.grid {
258
189
  margin: margin(2) auto 0;
259
190
  max-width: 600px;
260
191
  width: 100%;