Skip to content

Commit 061153d

Browse files
committed
feat(panel): adds ratings to movie & show panels
1 parent 2c3dacd commit 061153d

File tree

5 files changed

+243
-27
lines changed

5 files changed

+243
-27
lines changed

src/components/common/typography/TextField.vue

+43-19
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,26 @@ defineProps({
4646
type: Object as PropType<SkeletonProps>,
4747
required: false,
4848
},
49+
vertical: {
50+
type: Boolean,
51+
required: false,
52+
default: false,
53+
},
54+
wrap: {
55+
type: Boolean,
56+
required: false,
57+
default: false,
58+
},
59+
align: {
60+
type: String,
61+
required: false,
62+
default: 'baseline',
63+
},
64+
size: {
65+
type: String,
66+
required: false,
67+
default: 'medium',
68+
},
4969
});
5070
5171
const { openTab } = useLinksStore();
@@ -56,29 +76,33 @@ const { openTab } = useLinksStore();
5676
class="detail"
5777
:class="{ grow, array }"
5878
:style="{ '--prefix-min-width': labelWidth, '--text-flex': flex }"
59-
align="baseline"
60-
:wrap="false"
79+
:align="align"
80+
:wrap="wrap"
81+
:vertical="vertical"
82+
:size="size"
6183
>
6284
<span class="prefix">{{ label }}</span>
63-
<NFlex v-if="array" class="value">
64-
<template v-if="values !== undefined">
65-
<TagLinkComponent
66-
v-for="(tag, i) of values"
67-
:key="i"
68-
:tag="{ ...tag, round: true }"
69-
@on-click="openTab"
70-
/>
71-
</template>
85+
<slot>
86+
<NFlex v-if="array" class="value">
87+
<template v-if="values !== undefined">
88+
<TagLinkComponent
89+
v-for="(tag, i) of values"
90+
:key="i"
91+
:tag="{ ...tag, round: true }"
92+
@on-click="openTab"
93+
/>
94+
</template>
95+
<template v-else>
96+
<NSkeleton round v-bind="skeleton" />
97+
<NSkeleton round v-bind="skeleton" />
98+
<NSkeleton round v-bind="skeleton" />
99+
</template>
100+
</NFlex>
72101
<template v-else>
73-
<NSkeleton round v-bind="skeleton" />
74-
<NSkeleton round v-bind="skeleton" />
75-
<NSkeleton round v-bind="skeleton" />
102+
<span v-if="value !== undefined" class="value" :class="{ pre }">{{ value }}</span>
103+
<NSkeleton v-else :repeat="pre ? 3 : 0" round v-bind="skeleton" />
76104
</template>
77-
</NFlex>
78-
<template v-else>
79-
<span v-if="value !== undefined" class="value" :class="{ pre }">{{ value }}</span>
80-
<NSkeleton v-else :repeat="pre ? 3 : 0" round v-bind="skeleton" />
81-
</template>
105+
</slot>
82106
</NFlex>
83107
</template>
84108

src/components/views/panel/MoviePanel.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import MoviePanelDetails from '~/components/views/panel/MoviePanelDetails.vue';
99
import MoviePanelOverview from '~/components/views/panel/MoviePanelOverview.vue';
1010
import PanelPoster from '~/components/views/panel/PanelPoster.vue';
1111
12+
import PanelRatings from '~/components/views/panel/PanelRatings.vue';
1213
import {
1314
PanelButtonsOption,
1415
type PanelButtonsOptions,
@@ -289,7 +290,10 @@ onMounted(() => {
289290
round
290291
/>
291292

292-
<PanelPoster :tmdb="movie?.ids.tmdb" mode="movie" />
293+
<NFlex>
294+
<PanelPoster :tmdb="movie?.ids.tmdb" mode="movie" />
295+
<PanelRatings :votes="movie?.votes" :rating="movie?.rating" />
296+
</NFlex>
293297

294298
<MoviePanelDetails
295299
:movie="movie"
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<script lang="ts" setup>
2+
import { NFlex, NNumberAnimation, NProgress, NStatistic } from 'naive-ui';
3+
import { computed, onMounted, ref, toRefs, watch } from 'vue';
4+
5+
import TextField from '~/components/common/typography/TextField.vue';
6+
import { useI18n } from '~/utils/i18n.utils';
7+
8+
const props = defineProps({
9+
rating: {
10+
type: Number,
11+
required: false,
12+
default: 0,
13+
},
14+
votes: {
15+
type: Number,
16+
required: false,
17+
default: 0,
18+
},
19+
score: {
20+
type: Number,
21+
required: false,
22+
default: 0,
23+
},
24+
duration: {
25+
type: Number,
26+
required: false,
27+
default: 1000,
28+
},
29+
precision: {
30+
type: Number,
31+
required: false,
32+
default: 0,
33+
},
34+
});
35+
36+
const i18n = useI18n('panel', 'ratings');
37+
38+
const { votes, rating, score } = toRefs(props);
39+
40+
const votesUnit = computed(() => {
41+
if (votes?.value >= 10000) return 'k';
42+
if (votes?.value >= 1000000) return 'm';
43+
if (votes?.value >= 1000000000) return 'b';
44+
return null;
45+
});
46+
const _votes = computed(() => {
47+
if (!votes?.value || !votesUnit.value) return votes?.value ?? 0;
48+
if (votesUnit.value === 'k') return votes.value / 10000;
49+
if (votesUnit.value === 'm') return votes.value / 1000000;
50+
if (votesUnit.value === 'b') return votes.value / 1000000000;
51+
return votes?.value ?? 0;
52+
});
53+
54+
const _rating = computed(() => (rating?.value ?? 0) * 10);
55+
const _ratingProgress = ref(0);
56+
57+
const _score = computed(() => (score?.value ?? 0) * 10);
58+
const _scoreProgress = ref(0);
59+
60+
onMounted(() => {
61+
watch(
62+
_rating,
63+
val => {
64+
_ratingProgress.value = val;
65+
},
66+
{ immediate: true },
67+
);
68+
watch(
69+
_score,
70+
val => {
71+
_scoreProgress.value = val;
72+
},
73+
{ immediate: true },
74+
);
75+
});
76+
</script>
77+
78+
<template>
79+
<NFlex class="rating-container" align="center" justify="center" vertical>
80+
<TextField :label="i18n('label_votes')" vertical size="small" flex="0 1 auto">
81+
<NStatistic class="statistics" tabular-nums>
82+
<NNumberAnimation
83+
:from="0"
84+
:to="_votes"
85+
:duration="duration"
86+
:precision="votesUnit ? 2 : 0"
87+
/>
88+
<span v-if="votesUnit" class="unit">{{ votesUnit }}</span>
89+
</NStatistic>
90+
</TextField>
91+
<TextField :label="i18n('label_rating')" vertical flex="0 1 auto">
92+
<NProgress
93+
class="progress"
94+
type="circle"
95+
:percentage="_ratingProgress"
96+
:style="{ '--duration': `${duration}ms` }"
97+
>
98+
<NStatistic class="statistics" tabular-nums>
99+
<NNumberAnimation
100+
:from="0"
101+
:to="_rating"
102+
:duration="duration"
103+
:precision="precision"
104+
/>
105+
</NStatistic>
106+
</NProgress>
107+
</TextField>
108+
<TextField :label="i18n('label_score')" vertical flex="0 1 auto">
109+
<NProgress
110+
class="progress"
111+
type="circle"
112+
:percentage="_scoreProgress"
113+
:style="{ '--duration': `${duration}ms` }"
114+
>
115+
<NStatistic class="statistics" tabular-nums>
116+
<NNumberAnimation
117+
:from="0"
118+
:to="_pStores"
119+
:duration="duration"
120+
:precision="precision"
121+
/>
122+
</NStatistic>
123+
</NProgress>
124+
</TextField>
125+
</NFlex>
126+
</template>
127+
128+
<style lang="scss" scoped>
129+
.rating-container {
130+
--duration: 1000ms;
131+
132+
gap: 1rem;
133+
padding: 1rem;
134+
135+
.progress {
136+
width: 3rem;
137+
138+
:deep(path.n-progress-graph-circle-fill) {
139+
transition:
140+
opacity var(--duration) var(--n-bezier),
141+
stroke var(--duration) var(--n-bezier),
142+
stroke-dasharray var(--duration) var(--n-bezier);
143+
}
144+
}
145+
146+
.statistics {
147+
--n-value-font-size: 1rem !important;
148+
149+
font-variant-numeric: tabular-nums;
150+
align-self: center;
151+
152+
.unit {
153+
padding-left: 0.125rem;
154+
color: var(--white-50);
155+
}
156+
}
157+
}
158+
</style>

src/components/views/panel/ShowPanel.vue

+23-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { computed, onMounted, toRefs, watch } from 'vue';
66
77
import TitleLink from '~/components/common/buttons/TitleLink.vue';
88
import PanelPoster from '~/components/views/panel/PanelPoster.vue';
9+
import PanelRatings from '~/components/views/panel/PanelRatings.vue';
910
import ShowPanelButtons from '~/components/views/panel/ShowPanelButtons.vue';
1011
import ShowPanelDetails from '~/components/views/panel/ShowPanelDetails.vue';
1112
import ShowPanelOverview from '~/components/views/panel/ShowPanelOverview.vue';
@@ -378,6 +379,18 @@ onMounted(() => {
378379
);
379380
});
380381
382+
const votes = computed(() => {
383+
if (panelType.value === 'episode') return episode.value?.votes;
384+
if (panelType.value === 'season') return season.value?.votes;
385+
return show.value?.votes;
386+
});
387+
388+
const rating = computed(() => {
389+
if (panelType.value === 'episode') return episode.value?.rating;
390+
if (panelType.value === 'season') return season.value?.rating;
391+
return show.value?.rating;
392+
});
393+
381394
const { openTab } = useLinksStore();
382395
</script>
383396

@@ -399,13 +412,16 @@ const { openTab } = useLinksStore();
399412
round
400413
/>
401414

402-
<PanelPoster
403-
:tmdb="show?.ids.tmdb"
404-
:mode="panelType"
405-
:portait="panelType === 'season'"
406-
:season-number="seasonNb"
407-
:episode-number="episodeNb"
408-
/>
415+
<NFlex>
416+
<PanelPoster
417+
:tmdb="show?.ids.tmdb"
418+
:mode="panelType"
419+
:portait="panelType === 'season'"
420+
:season-number="seasonNb"
421+
:episode-number="episodeNb"
422+
/>
423+
<PanelRatings :votes="votes" :rating="rating" />
424+
</NFlex>
409425

410426
<ShowPanelDetails
411427
:show="show"

src/i18n/en/panel/panel-ratings.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"panel__ratings__label_votes": {
3+
"message": "Votes",
4+
"description": "Label for the number of votes in the ratings panel"
5+
},
6+
"panel__ratings__label_rating": {
7+
"message": "Rating",
8+
"description": "Label for the rating in the ratings panel"
9+
},
10+
"panel__ratings__label_score": {
11+
"message": "Score",
12+
"description": "Label for the score in the ratings panel"
13+
}
14+
}

0 commit comments

Comments
 (0)