Skip to content

Commit 2ceccfd

Browse files
committed
wip
1 parent 364981d commit 2ceccfd

9 files changed

Lines changed: 226 additions & 54 deletions

File tree

projects/client/src/lib/components/drawer/Drawer.svelte

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
variant?: "default" | "vip";
3434
drilldown?: ListDrilldownLinkProps;
3535
headerVariant?: "default" | "overlay";
36+
// Raise this drawer (and its underlay) above a base-layer drawer, so a
37+
// drawer stacked on top of another still closes on outside tap.
38+
elevated?: boolean;
3639
};
3740
3841
const {
@@ -50,12 +53,15 @@
5053
variant = "default",
5154
drilldown,
5255
headerVariant = "default",
56+
elevated = false,
5357
}: DrawerProps = $props();
5458
5559
const isMobile = useMedia(WellKnownMediaQuery.mobile);
5660
const slideAxis = $derived($isMobile ? "y" : "x");
5761
58-
const { portal } = $derived(useDrawerPortal({ hasAutoClose, onClose }));
62+
const { portal } = $derived(
63+
useDrawerPortal({ hasAutoClose, onClose, elevated }),
64+
);
5965
6066
const trap = $derived((element: HTMLElement) => {
6167
if (trapSelector) {
@@ -94,6 +100,7 @@
94100
<div
95101
class={drawerClass}
96102
data-size={size}
103+
data-elevated={elevated}
97104
data-header-variant={headerVariant}
98105
style:--drawer-header-overlay-opacity={headerOverlayOpacity}
99106
transition:slide={{ duration: 150, axis: slideAxis }}
@@ -194,6 +201,12 @@
194201
position: fixed;
195202
box-sizing: border-box;
196203
204+
// Stacked above a base-layer drawer; its underlay (layer-menu + 2) must
205+
// still sit below it so outside taps land on the underlay, not the drawer.
206+
&[data-elevated="true"] {
207+
z-index: calc(var(--layer-menu) + 3);
208+
}
209+
197210
top: 0;
198211
bottom: 0;
199212
inset-inline-end: 0;

projects/client/src/lib/components/drawer/_internal/useDrawerPortal.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import { onMount } from 'svelte';
66
type DrawerPortalProps = {
77
hasAutoClose: boolean;
88
onClose: () => void;
9+
elevated?: boolean;
910
};
1011

11-
export function useDrawerPortal({ hasAutoClose, onClose }: DrawerPortalProps) {
12+
export function useDrawerPortal(
13+
{ hasAutoClose, onClose, elevated }: DrawerPortalProps,
14+
) {
1215
const portal = (element: HTMLElement) => {
1316
// The route the drawer was opened on. When a cached page mounts the drawer
1417
// synchronously during the arriving navigation, afterNavigate would fire
@@ -21,7 +24,7 @@ export function useDrawerPortal({ hasAutoClose, onClose }: DrawerPortalProps) {
2124
return;
2225
}
2326

24-
const newUnderlay = createUnderlay();
27+
const newUnderlay = createUnderlay({ elevated });
2528

2629
document.body.appendChild(newUnderlay);
2730
document.body.appendChild(element);

projects/client/src/lib/components/summary/RatingList.svelte

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
ratings: MediaRating;
3434
entry: MediaEntry | EpisodeEntry;
3535
drilldown?: TraktRatingDrilldown;
36+
// Alternative to `drilldown`: open a locally-mounted drawer instead of
37+
// navigating via a URL. Used by the episode drawer, which can't route to
38+
// a `view=` drawer without replacing itself.
39+
onDrilldown?: () => void;
3640
variant?: "all" | "external";
3741
isLoading?: boolean;
3842
};
@@ -42,6 +46,7 @@
4246
ratings,
4347
entry,
4448
drilldown,
49+
onDrilldown,
4550
variant = "all",
4651
isLoading = false,
4752
}: RatingListProps = $props();
@@ -107,7 +112,17 @@
107112
</RatingItem>
108113
{/if}
109114

110-
{#if drilldown}
115+
{#if onDrilldown}
116+
<ActionButton
117+
classList="trakt-ratings-drilldown-button"
118+
onclick={onDrilldown}
119+
label={i18n.viewBreakdownLabel()}
120+
style="ghost"
121+
size="small"
122+
>
123+
<CaretRightIcon />
124+
</ActionButton>
125+
{:else if drilldown}
111126
<ActionButton
112127
classList="trakt-ratings-drilldown-button"
113128
href={drilldown.href}

projects/client/src/lib/features/portal/_internal/createUnderlay.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { PORTAL_UNDERLAY_ID } from './constants.ts';
22

3-
export const createUnderlay = () => {
3+
type CreateUnderlayOptions = {
4+
// Raise the underlay above a base-layer drawer so a stacked drawer's
5+
// outside-tap target sits on top of the drawer beneath it.
6+
elevated?: boolean;
7+
};
8+
9+
export const createUnderlay = ({ elevated }: CreateUnderlayOptions = {}) => {
410
const underlay = document.createElement('div');
511

612
underlay.id = PORTAL_UNDERLAY_ID;
@@ -11,7 +17,9 @@ export const createUnderlay = () => {
1117
underlay.style.width = '100%';
1218
underlay.style.height = '100%';
1319

14-
underlay.style.zIndex = 'calc(var(--layer-menu) - 1)';
20+
underlay.style.zIndex = elevated
21+
? 'calc(var(--layer-menu) + 2)'
22+
: 'calc(var(--layer-menu) - 1)';
1523

1624
return underlay;
1725
};

projects/client/src/lib/sections/summary/components/_internal/SocialActivitiesButton.svelte

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
type SocialActivitiesButtonProps = {
1616
target: MediaSocialQueryTarget;
1717
title: string;
18+
// When provided, open a locally-mounted drawer instead of navigating to
19+
// the `view=social` URL drawer (used by the episode drawer, which can't
20+
// route without replacing itself).
21+
onclick?: () => void;
1822
};
1923
20-
const { target, title }: SocialActivitiesButtonProps = $props();
24+
const { target, title, onclick }: SocialActivitiesButtonProps = $props();
2125
2226
const target$ = fromRune(() => target);
2327
const {
@@ -41,52 +45,65 @@
4145
const drawerLink = $derived(buildDrawerLink(SummaryDrawers.Social));
4246
</script>
4347

48+
{#snippet content()}
49+
<span class="social-activities-content">
50+
{#if hasWatchers}
51+
<span class="trakt-social-activities-avatar-stack" aria-hidden="true">
52+
{#each watchers as watcher, index (watcher.key)}
53+
<span
54+
class="trakt-social-activities-avatar"
55+
style:z-index={avatarStackCount - index}
56+
>
57+
<UserAvatar user={watcher.user} size="small" />
58+
</span>
59+
{/each}
60+
</span>
61+
{/if}
62+
63+
<span
64+
class="trakt-social-activities-label"
65+
class:is-text-only={activityCount === 0}
66+
>
67+
{#if activityCount > 0}
68+
<span class="trakt-social-activities-count bold">
69+
{activityCountLabel}
70+
</span>
71+
<span class="trakt-social-activities-text">
72+
{activityCount === 1
73+
? m.text_social_activity()
74+
: m.text_social_activities()}
75+
</span>
76+
{:else}
77+
<span class="trakt-social-activities-text">
78+
{m.text_no_activity()}
79+
</span>
80+
{/if}
81+
</span>
82+
</span>
83+
{/snippet}
84+
4485
<div class="trakt-social-activities-button-link-wrapper">
4586
{#if isInitialLoading}
4687
<div class="social-activities-skeleton" role="status" aria-busy="true">
4788
<span class="skeleton-count"></span>
4889
<span class="skeleton-label"></span>
4990
</div>
91+
{:else if onclick}
92+
<button
93+
type="button"
94+
class="social-activities-trigger"
95+
onclick={onclick}
96+
aria-label={m.link_label_view_social_activities({ title })}
97+
>
98+
{@render content()}
99+
</button>
50100
{:else}
51101
<Link
52102
{...drawerLink}
53103
color="inherit"
54104
label={m.link_label_view_social_activities({ title })}
55105
>
56-
<span class="social-activities-content">
57-
{#if hasWatchers}
58-
<span class="trakt-social-activities-avatar-stack" aria-hidden="true">
59-
{#each watchers as watcher, index (watcher.key)}
60-
<span
61-
class="trakt-social-activities-avatar"
62-
style:z-index={avatarStackCount - index}
63-
>
64-
<UserAvatar user={watcher.user} size="small" />
65-
</span>
66-
{/each}
67-
</span>
68-
{/if}
69-
70-
<span
71-
class="trakt-social-activities-label"
72-
class:is-text-only={activityCount === 0}
73-
>
74-
{#if activityCount > 0}
75-
<span class="trakt-social-activities-count bold">
76-
{activityCountLabel}
77-
</span>
78-
<span class="trakt-social-activities-text">
79-
{activityCount === 1
80-
? m.text_social_activity()
81-
: m.text_social_activities()}
82-
</span>
83-
{:else}
84-
<span class="trakt-social-activities-text">
85-
{m.text_no_activity()}
86-
</span>
87-
{/if}
88-
</span>
89-
</span>
106+
{@render content()}
90107
</Link>
91108
{/if}
92109
</div>
@@ -102,6 +119,18 @@
102119
text-decoration: none;
103120
}
104121
122+
.social-activities-trigger {
123+
appearance: none;
124+
background: none;
125+
border: 0;
126+
padding: 0;
127+
margin: 0;
128+
font: inherit;
129+
color: inherit;
130+
cursor: pointer;
131+
text-align: start;
132+
}
133+
105134
@include for-tablet-sm-and-below {
106135
align-self: center;
107136
}

projects/client/src/lib/sections/summary/components/episode-drawer/EpisodeDrawerHost.svelte

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
import DrawerCastSection from "$lib/sections/summary/components/_internal/DrawerCastSection.svelte";
1111
import MediaStats from "$lib/sections/summary/components/details/_internal/MediaStats.svelte";
1212
import MediaStatsSkeleton from "$lib/sections/summary/components/details/_internal/MediaStatsSkeleton.svelte";
13+
import RatingsDrawer from "$lib/sections/summary/components/rating/RatingsDrawer.svelte";
1314
import SeasonEpisodesTab from "$lib/sections/summary/components/seasons/SeasonEpisodesTab.svelte";
15+
import SocialDrawerHost from "$lib/sections/summary/components/social/SocialDrawerHost.svelte";
16+
import { episodeActivityTitle } from "$lib/utils/intl/episodeActivityTitle.ts";
1417
import { episodeNumberLabel } from "$lib/utils/intl/episodeNumberLabel.ts";
1518
import { fromRune } from "$lib/utils/store/fromRune.svelte.ts";
19+
import { writable } from "$lib/utils/store/WritableSubject.ts";
1620
import { onMount } from "svelte";
1721
import { fade } from "svelte/transition";
1822
import EpisodeInfoHeader from "./_internal/EpisodeInfoHeader.svelte";
@@ -38,6 +42,19 @@
3842
let isOpen = $state(false);
3943
let activeTab = $state("info");
4044
45+
// Ratings distribution and social activities open as drawers stacked on top
46+
// of this one, mounted locally (not via the `view=` URL param, which would
47+
// replace this drawer and resolve show-scoped data instead of the episode).
48+
const isRatingsOpen = writable(false);
49+
const isSocialOpen = writable(false);
50+
51+
const socialTarget = $derived({
52+
type: "episode" as const,
53+
slug: show.slug,
54+
season,
55+
episode,
56+
});
57+
4158
const params$ = fromRune(() => ({
4259
slug: show.slug,
4360
season,
@@ -87,6 +104,10 @@
87104
? `${numberLabel} - ${$episodeEntry.title}`
88105
: numberLabel;
89106
});
107+
108+
const socialTitle = $derived(
109+
$episodeEntry ? episodeActivityTitle($episodeEntry, show) : show.title,
110+
);
90111
</script>
91112

92113
{#snippet infoIcon()}
@@ -158,6 +179,10 @@
158179
entry={$episodeEntry}
159180
crew={$crew}
160181
isCrewLoading={$isPeopleLoading}
182+
{socialTarget}
183+
{socialTitle}
184+
onRatingsOpen={() => isRatingsOpen.set(true)}
185+
onSocialOpen={() => isSocialOpen.set(true)}
161186
/>
162187

163188
<TabView
@@ -188,6 +213,27 @@
188213
{/if}
189214
</Drawer>
190215

216+
{#if $isRatingsOpen && $episodeEntry}
217+
<RatingsDrawer
218+
type="episode"
219+
episode={$episodeEntry}
220+
{show}
221+
crew={$crew}
222+
{seasons}
223+
elevated
224+
onClose={() => isRatingsOpen.set(false)}
225+
/>
226+
{/if}
227+
228+
{#if $isSocialOpen}
229+
<SocialDrawerHost
230+
target={socialTarget}
231+
title={socialTitle}
232+
elevated
233+
onClose={() => isSocialOpen.set(false)}
234+
/>
235+
{/if}
236+
191237
<style lang="scss">
192238
:global(.trakt-drawer.trakt-episode-drawer) {
193239
gap: var(--ni-0);

0 commit comments

Comments
 (0)