Skip to content

Commit e748f20

Browse files
committed
feat(panel): refactor show panel
1 parent ed6b6f3 commit e748f20

File tree

13 files changed

+378
-93
lines changed

13 files changed

+378
-93
lines changed

src/components/AppComponent.vue

+25-7
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ const { isAuthenticated } = useAuthSettingsStoreRefs();
1414
const { currentRoute, push, back } = useRouter();
1515
1616
const panel = ref(false);
17+
const base = ref();
1718
1819
watch(
1920
currentRoute,
2021
(_next, _prev) => {
2122
panel.value = !!_next.meta?.panel;
23+
base.value = _next.meta?.base;
2224
},
2325
{
2426
immediate: true,
@@ -28,17 +30,16 @@ watch(
2830
const asideRef = ref();
2931
3032
const onAfterLeave = () => {
31-
const base = currentRoute.value?.meta?.base as string;
32-
if (!base) return;
33-
push({ name: base });
33+
if (!base.value) return;
34+
push({ name: base.value });
3435
};
3536
3637
const onClose = () => {
3738
panel.value = false;
3839
};
3940
4041
const onBack = () => {
41-
if (window.history.length > 1) return back();
42+
if (window.history.state.back) return back();
4243
return onClose();
4344
};
4445
</script>
@@ -72,13 +73,14 @@ const onBack = () => {
7273
v-model:show="panel"
7374
:to="asideRef"
7475
width="100%"
76+
class="panel"
7577
close-on-esc
7678
:on-after-leave="onAfterLeave"
7779
auto-focus
7880
>
7981
<NDrawerContent>
8082
<!-- Header -->
81-
<NFlex justify="space-between">
83+
<NFlex justify="space-between" class="panel-header">
8284
<NButton circle quaternary @click="onBack">
8385
<template #icon>
8486
<NIcon>
@@ -96,9 +98,9 @@ const onBack = () => {
9698
</NFlex>
9799

98100
<!-- Content -->
99-
<KeepAlive>
101+
<div class="panel-content">
100102
<component :is="PanelComponent" />
101-
</KeepAlive>
103+
</div>
102104
</NDrawerContent>
103105
</NDrawer>
104106
</RouterView>
@@ -139,4 +141,20 @@ main {
139141
min-height: calc(100% - #{layout.$header-navbar-height});
140142
margin-top: layout.$header-navbar-height;
141143
}
144+
145+
.panel {
146+
position: relative;
147+
max-height: calc(100% - #{layout.$header-navbar-height});
148+
overflow: auto;
149+
150+
&-header {
151+
position: sticky;
152+
top: 0;
153+
}
154+
155+
&-content {
156+
margin-top: -1.125rem;
157+
padding: 0 3rem 1.25rem;
158+
}
159+
}
142160
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script lang="ts" setup>
2+
import { type ButtonProps, NButton } from 'naive-ui';
3+
import { RouterLink, type RouterLinkProps } from 'vue-router';
4+
5+
import type { PropType } from 'vue';
6+
7+
defineProps({
8+
link: {
9+
type: Object as PropType<RouterLinkProps>,
10+
required: true,
11+
},
12+
button: {
13+
type: Object as PropType<ButtonProps>,
14+
required: false,
15+
},
16+
});
17+
</script>
18+
19+
<template>
20+
<RouterLink
21+
v-slot="{ isActive, href, navigate, isExactActive, route }"
22+
v-bind="link"
23+
custom
24+
>
25+
<NButton
26+
tag="a"
27+
quaternary
28+
class="button-link"
29+
:class="$attrs.class"
30+
:type="isActive ? 'primary' : 'default'"
31+
round
32+
size="small"
33+
:href="href"
34+
v-bind="button"
35+
@click="navigate"
36+
>
37+
<slot :is-active="isActive" :is-exact-active="isExactActive" :route="route" />
38+
</NButton>
39+
</RouterLink>
40+
</template>
41+
42+
<style lang="scss" scoped>
43+
.button-link {
44+
width: 1.5rem;
45+
height: 1.5rem;
46+
margin: 0;
47+
padding: 0.125rem;
48+
}
49+
</style>

src/components/common/list/ListItemPanel.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ const onTagClick = (e: MouseEvent, url?: string) => {
127127
}}</NEllipsis>
128128
</div>
129129
<NFlex v-if="date || tags?.length" size="medium" class="tags">
130-
<template v-for="tag of tags" :key="tag.label">
130+
<template v-for="(tag, i) of tags" :key="`${i}-${tag.label}`">
131131
<NSkeleton v-if="loading" text style="width: 6%" />
132132
<a v-else :href="tag.url" @click="e => onTagClick(e, tag.url)">
133133
<NTag
+112-65
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
<script setup lang="ts">
2-
import { NFlex, NSkeleton } from 'naive-ui';
3-
import { computed, onActivated, onDeactivated, ref, toRefs } from 'vue';
2+
import { NFlex, NH2, NH4, NSkeleton } from 'naive-ui';
3+
import { computed, onMounted, onUnmounted, ref, toRefs, Transition, watch } from 'vue';
44
5-
import { useRoute } from 'vue-router';
6-
7-
import type { PosterItem } from '~/models/poster.model';
85
import type { TraktEpisodeExtended } from '~/models/trakt/trakt-episode.model';
96
import type { TraktShowExtended } from '~/models/trakt/trakt-show.model';
107
11-
import type { ImageQuery } from '~/stores/data/image.store';
12-
13-
import PosterComponent from '~/components/common/poster/PosterComponent.vue';
8+
import ShowPanelPicker from '~/components/views/panel/ShowPanelPicker.vue';
9+
import ShowPanelPoster from '~/components/views/panel/ShowPanelPoster.vue';
1410
import { type ShowSeasons, useShowStore } from '~/stores/data/show.store';
11+
import { deCapitalise } from '~/utils/string.utils';
1512
1613
const props = defineProps({
1714
showId: {
@@ -37,91 +34,141 @@ const episode = ref<TraktEpisodeExtended>();
3734
const { showId, seasonNumber, episodeNumber } = toRefs(props);
3835
3936
const seasonNb = computed(() => {
40-
if (!seasonNumber?.value) return;
37+
if (seasonNumber?.value === undefined) return;
4138
const _seasonNumber = Number(seasonNumber.value);
4239
if (Number.isNaN(_seasonNumber)) return;
4340
return _seasonNumber;
4441
});
4542
4643
const episodeNb = computed(() => {
47-
if (!episodeNumber?.value) return;
44+
if (episodeNumber?.value === undefined) return;
4845
const _episodeNumber = Number(episodeNumber.value);
4946
if (Number.isNaN(_episodeNumber)) return;
5047
return _episodeNumber;
5148
});
5249
53-
const size = computed(() => window?.innerWidth ?? 800 / 2);
54-
55-
const { query } = useRoute();
56-
const posterItem = computed<PosterItem | undefined>(() => {
57-
const poster = query.poster?.toString();
58-
if (poster) return { poster } satisfies PosterItem;
59-
60-
const tmdb = query.tmdb?.toString();
61-
if (!tmdb) return;
62-
const imageQuery: ImageQuery = {
63-
id: tmdb,
64-
season: seasonNb.value,
65-
episode: episodeNb.value,
66-
type: 'show',
67-
};
68-
if (episodeNb.value) imageQuery.type = 'episode';
69-
else if (seasonNb.value) imageQuery.type = 'season';
70-
71-
return {
72-
posterRef: ref(),
73-
getPosterQuery: () => imageQuery,
74-
};
50+
const showTitle = computed(() => {
51+
if (!show.value?.title) return;
52+
return deCapitalise(show.value.title);
53+
});
54+
55+
const episodeTitle = computed(() => {
56+
if (!episode.value?.title) return;
57+
return deCapitalise(episode.value?.title);
7558
});
7659
7760
const subscriptions = new Set<() => void>();
7861
79-
onActivated(() => {
80-
if (showId?.value) subscriptions.add(getShowRef(showId.value, show).unsub);
81-
if (showId?.value) subscriptions.add(getShowSeasonsRef(showId.value, seasons).unsub);
82-
if (showId?.value && seasonNb?.value && episodeNb?.value) {
83-
subscriptions.add(
84-
getShowEpisodeRef(
85-
{
86-
id: showId.value,
87-
season: seasonNb.value,
88-
episode: episodeNb.value,
89-
},
90-
episode,
91-
).unsub,
92-
);
93-
}
62+
const watchData = () =>
63+
watch(
64+
props,
65+
(next, prev) => {
66+
if (next.showId !== prev?.showId) {
67+
show.value = undefined;
68+
seasons.value = undefined;
69+
episode.value = undefined;
70+
} else if (next.episodeNumber !== prev?.episodeNumber) {
71+
episode.value = undefined;
72+
} else if (next.seasonNumber !== prev?.seasonNumber) {
73+
episode.value = undefined;
74+
}
75+
76+
if (showId?.value) subscriptions.add(getShowRef(showId.value, show).unsub);
77+
if (showId?.value)
78+
subscriptions.add(getShowSeasonsRef(showId.value, seasons).unsub);
79+
if (
80+
showId?.value &&
81+
seasonNb?.value !== undefined &&
82+
episodeNb?.value !== undefined
83+
) {
84+
subscriptions.add(
85+
getShowEpisodeRef(
86+
{
87+
id: showId.value,
88+
season: seasonNb.value,
89+
episode: episodeNb.value,
90+
},
91+
episode,
92+
).unsub,
93+
);
94+
}
95+
},
96+
{ immediate: true },
97+
);
98+
99+
onMounted(() => {
100+
subscriptions.add(watchData());
94101
});
95102
96-
onDeactivated(() => {
103+
onUnmounted(() => {
97104
subscriptions.forEach(unsub => unsub());
98105
subscriptions.clear();
99-
show.value = undefined;
100-
seasons.value = undefined;
101-
episode.value = undefined;
102106
});
103107
</script>
104108

105109
<template>
106110
<NFlex justify="center" align="center" vertical>
107-
<NFlex v-if="posterItem" class="poster-container">
108-
<PosterComponent :item="posterItem" :episode="!!episodeNumber" :size="size" />
109-
</NFlex>
110-
<div>show {{ showId }}</div>
111-
<div v-if="show">show : {{ show?.title }}</div>
111+
<NH2 v-if="showTitle" class="show-title">{{ showTitle }}</NH2>
112112
<NSkeleton v-else />
113-
<div>season {{ seasonNumber }}</div>
114-
<div v-if="seasons">season {{ Object.keys(seasons) }}</div>
115-
<NSkeleton v-else />
116-
<div>episode {{ episodeNumber }}</div>
117-
<div v-if="episode">episode {{ episode?.title }}</div>
118-
<NSkeleton v-else
119-
/></NFlex>
113+
114+
<ShowPanelPicker :seasons="seasons" :season-number="seasonNb" />
115+
116+
<ShowPanelPoster
117+
:show-id="show?.ids.tmdb"
118+
:season-number="seasonNb"
119+
:episode-number="episodeNb"
120+
/>
121+
122+
<Transition name="scale" mode="out-in">
123+
<NFlex
124+
v-if="episodeNumber !== undefined"
125+
:key="`season-${seasonNb}-episode-${episodeNb}`"
126+
justify="center"
127+
align="center"
128+
vertical
129+
class="episode-container"
130+
>
131+
<NH4 v-if="episodeTitle" class="episode-title">{{ episodeTitle }}</NH4>
132+
<NSkeleton v-else />
133+
134+
<div v-if="episode">{{ episode?.overview }}</div>
135+
<NSkeleton v-else />
136+
</NFlex>
137+
</Transition>
138+
</NFlex>
120139
</template>
121140

122141
<style lang="scss" scoped>
142+
@use '~/styles/transition' as transition;
143+
@include transition.scale;
144+
123145
.poster-container {
124-
--poster-width: 50dvw;
125-
--poster-height: calc(var(--poster-width) * (9 / 16));
146+
--poster-height: calc(50dvw * (9 / 16));
147+
--poster-width: calc(var(--poster-height) * (2 / 3));
148+
149+
&.landscape {
150+
--poster-width: 50dvw;
151+
--poster-height: calc(var(--poster-width) * (9 / 16));
152+
}
153+
154+
position: relative;
155+
border: 1px solid var(--border-white);
156+
box-shadow: var(--image-box-shadow);
157+
}
158+
159+
.show-title {
160+
margin-bottom: 0.5rem;
161+
}
162+
163+
.episode {
164+
&-container {
165+
width: 100%;
166+
}
167+
168+
&-title {
169+
margin-top: 1rem;
170+
margin-bottom: 1rem;
171+
font-weight: bold;
172+
}
126173
}
127174
</style>

0 commit comments

Comments
 (0)