itube-specs 0.0.897 → 0.0.898
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/assets/icons/common/bell.svg +1 -0
- package/assets/scss/mixins/_font-sizes.scss +5 -0
- package/assets/scss/themes/dark/_theme.scss +38 -23
- package/assets/scss/themes/light/_theme.scss +34 -21
- package/assets/scss/vars/_palette.scss +1 -0
- package/assets/scss/vars/_tokens.scss +2 -0
- package/components/card/category.vue +22 -1
- package/components/card/playlist.vue +32 -4
- package/components/card/round.vue +48 -4
- package/components/filter/main.vue +2 -6
- package/components/filter/model.vue +2 -6
- package/components/grid/playlists.vue +2 -0
- package/components/layout/burger-item.vue +8 -8
- package/components/layout/extra-links.vue +2 -0
- package/components/layout/header.vue +29 -3
- package/components/layout/logo.vue +11 -2
- package/components/layout/notifications.vue +79 -0
- package/components/section/swiper-cards.vue +65 -34
- package/components/ui/btn.vue +15 -0
- package/components/ui/popup.vue +0 -4
- package/components/ui/swiper.vue +19 -4
- package/components/ui/view-all.vue +113 -0
- package/package.json +1 -1
- package/pages/following.vue +201 -0
- package/pages/library/index.vue +5 -2
- package/pages/search/[slug]/index.vue +19 -37
- package/stubs/library-follows.ts +75 -2
- package/types/button-themes.d.ts +1 -1
- package/types/category-card.d.ts +2 -0
- package/types/playlist-card.d.ts +2 -0
- package/types/round-card.d.ts +2 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!-- Колокольчик уведомлений в шапке: ссылка на подписки + счётчик обновлений. Данные пока заглушка -->
|
|
2
|
+
<template>
|
|
3
|
+
<NuxtLink
|
|
4
|
+
:to="generateLink('/following')"
|
|
5
|
+
:aria-label="t('notifications')"
|
|
6
|
+
:title="t('notifications')"
|
|
7
|
+
class="layout-notifications"
|
|
8
|
+
>
|
|
9
|
+
<UiIcon
|
|
10
|
+
class="layout-notifications__icon"
|
|
11
|
+
name="bell"
|
|
12
|
+
size="24"
|
|
13
|
+
/>
|
|
14
|
+
<span
|
|
15
|
+
v-if="count > 0"
|
|
16
|
+
class="layout-notifications__badge"
|
|
17
|
+
>{{ badge }}</span>
|
|
18
|
+
</NuxtLink>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { getFollowingUpdatesTotal } from '../../stubs/library-follows';
|
|
23
|
+
|
|
24
|
+
const MAX_COUNT = 99;
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
const { generateLink } = useGenerateLink();
|
|
28
|
+
|
|
29
|
+
const count = computed(() => getFollowingUpdatesTotal());
|
|
30
|
+
|
|
31
|
+
const badge = computed(() => count.value > MAX_COUNT ? `${MAX_COUNT}+` : `${count.value}`);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style lang="scss">
|
|
35
|
+
/* Бейдж лежит в потоке и наезжает на иконку отрицательным отступом: разряды уходят
|
|
36
|
+
вправо, растягивая контрол. Место под «99+» не резервируется, а зазор от края
|
|
37
|
+
бейджа до соседа в шапке одинаков при любом числе — его держит $badge-inset. */
|
|
38
|
+
$padding: 8px;
|
|
39
|
+
$badge-inset: 2px;
|
|
40
|
+
$badge-overlap: 10px;
|
|
41
|
+
$badge-raise: 6px;
|
|
42
|
+
|
|
43
|
+
.layout-notifications {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
padding: $padding $badge-inset $padding $padding;
|
|
47
|
+
color: $notifications-icon;
|
|
48
|
+
|
|
49
|
+
/* На мобиле это такой же тап-контрол, как бургер рядом */
|
|
50
|
+
@include to-br(sm) {
|
|
51
|
+
color: $notifications-icon--mobile;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@include hover(color) {
|
|
55
|
+
color: $notifications-icon--hover;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.layout-notifications__badge {
|
|
60
|
+
@include font-4xs;
|
|
61
|
+
|
|
62
|
+
/* Левым краем на верхний правый угол иконки, дальше растёт вправо */
|
|
63
|
+
position: relative;
|
|
64
|
+
z-index: $z-index-plus;
|
|
65
|
+
display: grid;
|
|
66
|
+
place-content: center;
|
|
67
|
+
align-self: flex-start;
|
|
68
|
+
min-width: 16px;
|
|
69
|
+
height: 16px;
|
|
70
|
+
margin-top: -$badge-raise;
|
|
71
|
+
margin-left: -$badge-overlap;
|
|
72
|
+
padding-inline: 4px;
|
|
73
|
+
font-weight: 700;
|
|
74
|
+
color: $notifications-badge-text;
|
|
75
|
+
background-color: $notifications-badge-bg;
|
|
76
|
+
border-radius: $radius-full;
|
|
77
|
+
box-shadow: 0 0 0 2px $notifications-badge-ring;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
<!-- Горизонтальный свайпер аватаров моделей с заголовком и ссылкой «все» -->
|
|
2
2
|
<template>
|
|
3
3
|
<section class="section-swiper-cards">
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
<header class="section-swiper-cards__header">
|
|
5
|
+
<UiSectionTitle
|
|
6
|
+
:title="title || t('top_models')"
|
|
7
|
+
:count="count ?? items.length"
|
|
8
|
+
:icon="icon"
|
|
9
|
+
class="section-swiper-cards__title"
|
|
10
|
+
title-tag="h2"
|
|
11
|
+
/>
|
|
12
|
+
|
|
13
|
+
<!-- На мобиле плитки «все» в ленте нет, поэтому ссылка вынесена в шапку -->
|
|
14
|
+
<UiLink
|
|
15
|
+
v-if="allLink"
|
|
16
|
+
:to="generateLink(allLink)"
|
|
17
|
+
class="section-swiper-cards__link _to-sm"
|
|
18
|
+
theme="simple"
|
|
19
|
+
>
|
|
20
|
+
{{ headerLinkText ?? allLinkText }}
|
|
21
|
+
<UiIcon
|
|
22
|
+
name="arrow-right"
|
|
23
|
+
size="16"
|
|
24
|
+
/>
|
|
25
|
+
</UiLink>
|
|
26
|
+
</header>
|
|
11
27
|
<div class="section-swiper-cards__grid">
|
|
12
28
|
<UiSwiper
|
|
13
29
|
:items="items"
|
|
@@ -17,6 +33,7 @@
|
|
|
17
33
|
<CardRound
|
|
18
34
|
:card="item"
|
|
19
35
|
:placeholder-avatar="placeholderAvatar"
|
|
36
|
+
:without-count="withoutCount"
|
|
20
37
|
class="section-swiper-cards__card"
|
|
21
38
|
/>
|
|
22
39
|
</template>
|
|
@@ -24,11 +41,12 @@
|
|
|
24
41
|
v-if="allLink"
|
|
25
42
|
#append
|
|
26
43
|
>
|
|
27
|
-
<
|
|
28
|
-
:to="
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
<UiViewAll
|
|
45
|
+
:to="allLink ?? '/'"
|
|
46
|
+
:text="allLinkText ?? ''"
|
|
47
|
+
:note="allLinkNote"
|
|
48
|
+
mini
|
|
49
|
+
/>
|
|
32
50
|
</template>
|
|
33
51
|
</UiSwiper>
|
|
34
52
|
</div>
|
|
@@ -38,14 +56,25 @@
|
|
|
38
56
|
<script setup lang="ts">
|
|
39
57
|
import type { IRoundCard } from '../../types';
|
|
40
58
|
|
|
41
|
-
defineProps<{
|
|
59
|
+
withDefaults(defineProps<{
|
|
42
60
|
items: IRoundCard[]
|
|
43
61
|
onlyGrid?: boolean
|
|
44
62
|
allLink?: string
|
|
45
63
|
allLinkText?: string
|
|
64
|
+
/** Вторая строка в плитке «все» — «16 models» */
|
|
65
|
+
allLinkNote?: string
|
|
66
|
+
/** Подпись ссылки в шапке («All models»); по умолчанию — как в плитке */
|
|
67
|
+
headerLinkText?: string
|
|
46
68
|
title?: string
|
|
69
|
+
/** Счётчик в шапке; по умолчанию — число карточек */
|
|
70
|
+
count?: string | number
|
|
71
|
+
icon?: string
|
|
47
72
|
placeholderAvatar?: string
|
|
48
|
-
|
|
73
|
+
/** Карточки только с аватаром и названием, без счётчика роликов */
|
|
74
|
+
withoutCount?: boolean
|
|
75
|
+
}>(), {
|
|
76
|
+
icon: 'user'
|
|
77
|
+
});
|
|
49
78
|
|
|
50
79
|
const { t } = useI18n();
|
|
51
80
|
const { generateLink } = useGenerateLink();
|
|
@@ -58,12 +87,22 @@ const { generateLink } = useGenerateLink();
|
|
|
58
87
|
min-width: 0;
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
.section-swiper-cards__header {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
column-gap: 16px;
|
|
95
|
+
margin-bottom: 16px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* text-transform здесь не нужен: капс и трекинг заголовку даёт его собственный
|
|
99
|
+
`._title`, а на корне он красил заодно и чип счётчика («6 UPDATES»). */
|
|
61
100
|
.section-swiper-cards__title {
|
|
62
101
|
@include font-lg;
|
|
102
|
+
}
|
|
63
103
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
letter-spacing: var(--tracking-wide);
|
|
104
|
+
.section-swiper-cards__link {
|
|
105
|
+
flex-shrink: 0;
|
|
67
106
|
}
|
|
68
107
|
|
|
69
108
|
.section-swiper-cards .section-swiper-cards__card {
|
|
@@ -74,24 +113,16 @@ const { generateLink } = useGenerateLink();
|
|
|
74
113
|
min-width: 0;
|
|
75
114
|
}
|
|
76
115
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
place-content: center;
|
|
82
|
-
width: $card-round-size;
|
|
83
|
-
height: $card-round-size;
|
|
84
|
-
aspect-ratio: 1/1;
|
|
85
|
-
border-radius: $radius-full;
|
|
86
|
-
object-fit: cover;
|
|
87
|
-
border: 2px solid $card-round-border;
|
|
88
|
-
background-color: $card-round-all-bg;
|
|
89
|
-
color: $card-round-all-text;
|
|
90
|
-
text-align: center;
|
|
116
|
+
/* Слайд ровно по круглой карточке: 80px на мобиле, 96px с sm */
|
|
117
|
+
.section-swiper-cards .ui-swiper__slide {
|
|
118
|
+
width: var(--card-round-size);
|
|
119
|
+
}
|
|
91
120
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
/* Плитка «все» — только с sm, на мобиле её роль играет ссылка в шапке */
|
|
122
|
+
.section-swiper-cards .ui-swiper__slide--append {
|
|
123
|
+
@include to-br(sm) {
|
|
124
|
+
display: none;
|
|
95
125
|
}
|
|
96
126
|
}
|
|
127
|
+
|
|
97
128
|
</style>
|
package/components/ui/btn.vue
CHANGED
|
@@ -161,4 +161,19 @@ const slotText = computed(() => {
|
|
|
161
161
|
background-color: var(--button-bg--hover, #{$button-warning-bg--hover});
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
/* Без подложки, рамки и паддингов — обычная текстовая ссылка с иконкой */
|
|
166
|
+
.ui-btn.--simple {
|
|
167
|
+
min-height: 0;
|
|
168
|
+
padding: 0;
|
|
169
|
+
column-gap: 4px;
|
|
170
|
+
color: var(--button-text, #{$button-simple-text});
|
|
171
|
+
background-color: transparent;
|
|
172
|
+
border-color: transparent;
|
|
173
|
+
border-radius: 0;
|
|
174
|
+
|
|
175
|
+
@include hover(color) {
|
|
176
|
+
color: var(--button-text--hover, #{$button-simple-text--hover});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
164
179
|
</style>
|
package/components/ui/popup.vue
CHANGED
|
@@ -313,10 +313,6 @@ const { snackbarText, isSnackBarInPopup } = useSnackbar();
|
|
|
313
313
|
.ui-popup__footer {
|
|
314
314
|
padding: 16px 20px;
|
|
315
315
|
border-top: 1px solid $popup-footer-border;
|
|
316
|
-
|
|
317
|
-
@include from-br(sm) {
|
|
318
|
-
padding: 16px 24px;
|
|
319
|
-
}
|
|
320
316
|
}
|
|
321
317
|
|
|
322
318
|
.ui-popup[open].--drawer {
|
package/components/ui/swiper.vue
CHANGED
|
@@ -97,6 +97,10 @@ onMounted(() => {
|
|
|
97
97
|
</script>
|
|
98
98
|
|
|
99
99
|
<style lang="scss">
|
|
100
|
+
/* Запас под ring ховера карточек: у трека overflow-x: auto, а значит по спецификации
|
|
101
|
+
и overflow-y становится auto — без запаса тень обрезается по всем краям. */
|
|
102
|
+
$ring-room: 4px;
|
|
103
|
+
|
|
100
104
|
.ui-swiper {
|
|
101
105
|
position: relative;
|
|
102
106
|
|
|
@@ -113,8 +117,13 @@ onMounted(() => {
|
|
|
113
117
|
scroll-snap-type: x mandatory;
|
|
114
118
|
-webkit-overflow-scrolling: touch;
|
|
115
119
|
scrollbar-width: none;
|
|
120
|
+
padding-block: $ring-room;
|
|
116
121
|
padding-inline: var(--container-padding-inline);
|
|
117
122
|
|
|
123
|
+
/* Без scroll-padding mandatory-снап считает точкой привязки сам край скроллпорта
|
|
124
|
+
и на старте подтягивает первую карточку под padding — она липнет к краю вьюпорта. */
|
|
125
|
+
scroll-padding-inline: var(--container-padding-inline);
|
|
126
|
+
|
|
118
127
|
&::-webkit-scrollbar {
|
|
119
128
|
display: none;
|
|
120
129
|
}
|
|
@@ -123,9 +132,12 @@ onMounted(() => {
|
|
|
123
132
|
gap: 8px;
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
/* Отрицательный margin гасит боковой запас, чтобы первая карточка осталась по краю */
|
|
126
136
|
@include from-br(sm) {
|
|
127
137
|
gap: 16px;
|
|
128
|
-
padding-inline:
|
|
138
|
+
padding-inline: $ring-room;
|
|
139
|
+
scroll-padding-inline: $ring-room;
|
|
140
|
+
margin-inline: -$ring-room;
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
|
|
@@ -144,7 +156,7 @@ $nav-gap: 4px;
|
|
|
144
156
|
$size: 40px;
|
|
145
157
|
|
|
146
158
|
position: absolute;
|
|
147
|
-
top: calc(var(--slide-width) / 2);
|
|
159
|
+
top: calc(var(--slide-width) / 2 + #{$ring-room});
|
|
148
160
|
transform: translateY(-50%);
|
|
149
161
|
z-index: calc($z-index-plus + 2);
|
|
150
162
|
display: grid;
|
|
@@ -165,15 +177,18 @@ $nav-gap: 4px;
|
|
|
165
177
|
background-color: $swiper-nav-bg--hover;
|
|
166
178
|
}
|
|
167
179
|
|
|
180
|
+
/* Трек вылезает за .ui-swiper на $ring-room, поэтому градиент уводим на
|
|
181
|
+
($nav-gap + $ring-room) — иначе между затемнением и краем свайпера
|
|
182
|
+
остаётся полоска непокрытых карточек. */
|
|
168
183
|
&::before {
|
|
169
184
|
position: absolute;
|
|
170
185
|
content: '';
|
|
171
186
|
top: 50%;
|
|
172
|
-
right: $nav-gap
|
|
187
|
+
right: -($nav-gap + $ring-room);
|
|
173
188
|
transform: translateY(-50%);
|
|
174
189
|
display: block;
|
|
175
190
|
width: 64px;
|
|
176
|
-
height: var(--slide-width);
|
|
191
|
+
height: calc(var(--slide-width) + #{$ring-room * 2});
|
|
177
192
|
background: linear-gradient(270deg, $swiper-fade 0%, $swiper-fade-end 100%);
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<!-- Плитка-ссылка «смотреть все» в конце ленты: точечная текстура, стрелка и подпись. mini — круглая, под свайпер аватаров -->
|
|
2
|
+
<template>
|
|
3
|
+
<NuxtLink
|
|
4
|
+
:class="{'--mini': mini}"
|
|
5
|
+
:to="generateLink(to)"
|
|
6
|
+
class="ui-view-all"
|
|
7
|
+
>
|
|
8
|
+
<span
|
|
9
|
+
v-if="!mini"
|
|
10
|
+
class="ui-view-all__icon"
|
|
11
|
+
>
|
|
12
|
+
<UiIcon
|
|
13
|
+
name="arrow-right"
|
|
14
|
+
size="20"
|
|
15
|
+
/>
|
|
16
|
+
</span>
|
|
17
|
+
|
|
18
|
+
<span class="ui-view-all__content">
|
|
19
|
+
<span class="ui-view-all__text">{{ text }}</span>
|
|
20
|
+
<span
|
|
21
|
+
v-if="note"
|
|
22
|
+
class="ui-view-all__note"
|
|
23
|
+
>{{ note }}</span>
|
|
24
|
+
</span>
|
|
25
|
+
</NuxtLink>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
defineProps<{
|
|
30
|
+
to: string
|
|
31
|
+
text: string
|
|
32
|
+
/** Вторая строка: «16 models» */
|
|
33
|
+
note?: string
|
|
34
|
+
/** Круглый вариант в размер аватар-карточки — для свайпера */
|
|
35
|
+
mini?: boolean
|
|
36
|
+
}>();
|
|
37
|
+
|
|
38
|
+
const { generateLink } = useGenerateLink();
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<style lang="scss">
|
|
42
|
+
.ui-view-all {
|
|
43
|
+
display: grid;
|
|
44
|
+
place-content: center;
|
|
45
|
+
justify-items: center;
|
|
46
|
+
row-gap: 12px;
|
|
47
|
+
padding: 12px;
|
|
48
|
+
text-align: center;
|
|
49
|
+
border: 1px solid $view-all-border;
|
|
50
|
+
border-radius: $radius-xl;
|
|
51
|
+
|
|
52
|
+
/* точки поверх градиента — двумя слоями, без отдельной ноды-оверлея */
|
|
53
|
+
background-image:
|
|
54
|
+
radial-gradient(circle, $view-all-dots 1px, transparent 1px),
|
|
55
|
+
linear-gradient(to bottom right in oklab, $view-all-gradient-from, $view-all-gradient-via, $view-all-gradient-to);
|
|
56
|
+
background-size: 14px 14px, cover;
|
|
57
|
+
|
|
58
|
+
@include hover(border-color) {
|
|
59
|
+
border-color: $view-all-border--hover;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ui-view-all__icon {
|
|
64
|
+
display: grid;
|
|
65
|
+
place-content: center;
|
|
66
|
+
width: 40px;
|
|
67
|
+
height: 40px;
|
|
68
|
+
color: $view-all-icon;
|
|
69
|
+
background-color: $view-all-icon-bg;
|
|
70
|
+
border: 1px solid $view-all-icon-border;
|
|
71
|
+
border-radius: $radius-full;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ui-view-all__content {
|
|
75
|
+
display: grid;
|
|
76
|
+
row-gap: 2px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ui-view-all__text {
|
|
80
|
+
@include font-sm;
|
|
81
|
+
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
color: $view-all-text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ui-view-all__note {
|
|
87
|
+
@include font-xs;
|
|
88
|
+
|
|
89
|
+
color: $view-all-note;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.ui-view-all.--mini {
|
|
93
|
+
width: var(--card-round-size);
|
|
94
|
+
height: var(--card-round-size);
|
|
95
|
+
padding: 8px;
|
|
96
|
+
row-gap: 0;
|
|
97
|
+
border-width: 2px;
|
|
98
|
+
border-color: $card-round-border;
|
|
99
|
+
border-radius: $radius-full;
|
|
100
|
+
background-size: 12px 12px, cover;
|
|
101
|
+
|
|
102
|
+
@include hover(border-color) {
|
|
103
|
+
border-color: $card-round-border--hover;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ui-view-all.--mini .ui-view-all__text,
|
|
108
|
+
.ui-view-all.--mini .ui-view-all__note {
|
|
109
|
+
@include font-xs;
|
|
110
|
+
|
|
111
|
+
line-height: 1.2;
|
|
112
|
+
}
|
|
113
|
+
</style>
|
package/package.json
CHANGED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<!-- Подписки: что нового у моделей, каналов, категорий и плейлистов. Данные пока заглушка -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="following _page _container">
|
|
4
|
+
<UiBreadcrumbs
|
|
5
|
+
:items="breadcrumbs"
|
|
6
|
+
class="_breadcrumbs"
|
|
7
|
+
/>
|
|
8
|
+
|
|
9
|
+
<h1 class="_title">{{ t('following.title') }}</h1>
|
|
10
|
+
|
|
11
|
+
<SectionSwiperCards
|
|
12
|
+
:items="models"
|
|
13
|
+
:count="t('following.updates', { count: modelsUpdates })"
|
|
14
|
+
:title="t('models')"
|
|
15
|
+
:all-link-text="t('following.view_all')"
|
|
16
|
+
:all-link-note="t('plural.models', models.length)"
|
|
17
|
+
:header-link-text="t('following.all_models')"
|
|
18
|
+
all-link="/library/models"
|
|
19
|
+
icon="user"
|
|
20
|
+
placeholder-avatar="/img/model-placeholder.webp"
|
|
21
|
+
class="following__section"
|
|
22
|
+
without-count
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
<SectionSwiperCards
|
|
26
|
+
:items="channels"
|
|
27
|
+
:count="t('following.updates', { count: channelsUpdates })"
|
|
28
|
+
:title="t('channels')"
|
|
29
|
+
:all-link-text="t('following.view_all')"
|
|
30
|
+
:all-link-note="t('plural.channels', channels.length)"
|
|
31
|
+
:header-link-text="t('following.all_channels')"
|
|
32
|
+
all-link="/library/channels"
|
|
33
|
+
icon="play-circle"
|
|
34
|
+
placeholder-avatar="/img/logo-placeholder.svg"
|
|
35
|
+
class="following__section"
|
|
36
|
+
without-count
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<section class="following__section">
|
|
40
|
+
<header class="following__header">
|
|
41
|
+
<UiSectionTitle
|
|
42
|
+
:title="t('categories')"
|
|
43
|
+
:count="t('following.updates', { count: categoriesUpdates })"
|
|
44
|
+
icon="grid"
|
|
45
|
+
title-tag="h2"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<!-- На мобиле плитки «все» в гриде нет, поэтому ссылка вынесена в шапку -->
|
|
49
|
+
<UiLink
|
|
50
|
+
:to="generateLink('/library/categories')"
|
|
51
|
+
class="following__link _to-sm"
|
|
52
|
+
theme="simple"
|
|
53
|
+
>
|
|
54
|
+
{{ t('following.all_categories') }}
|
|
55
|
+
<UiIcon
|
|
56
|
+
name="arrow-right"
|
|
57
|
+
size="16"
|
|
58
|
+
/>
|
|
59
|
+
</UiLink>
|
|
60
|
+
</header>
|
|
61
|
+
|
|
62
|
+
<GridCategories :categories="categories">
|
|
63
|
+
<UiViewAll
|
|
64
|
+
:text="t('following.view_all')"
|
|
65
|
+
:note="t('plural.categories', categories.length)"
|
|
66
|
+
class="_from-sm"
|
|
67
|
+
to="/library/categories"
|
|
68
|
+
/>
|
|
69
|
+
</GridCategories>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<section class="following__section">
|
|
73
|
+
<header class="following__header">
|
|
74
|
+
<UiSectionTitle
|
|
75
|
+
:title="t('playlists')"
|
|
76
|
+
:count="t('following.updates', { count: playlistsUpdates })"
|
|
77
|
+
icon="playlist"
|
|
78
|
+
title-tag="h2"
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
<UiLink
|
|
82
|
+
:to="generateLink('/playlists')"
|
|
83
|
+
class="following__link _to-sm"
|
|
84
|
+
theme="simple"
|
|
85
|
+
>
|
|
86
|
+
{{ t('following.all_playlists') }}
|
|
87
|
+
<UiIcon
|
|
88
|
+
name="arrow-right"
|
|
89
|
+
size="16"
|
|
90
|
+
/>
|
|
91
|
+
</UiLink>
|
|
92
|
+
</header>
|
|
93
|
+
|
|
94
|
+
<GridPlaylists :items="playlists">
|
|
95
|
+
<UiViewAll
|
|
96
|
+
:text="t('following.view_all')"
|
|
97
|
+
:note="t('plural.playlists', playlists.length)"
|
|
98
|
+
class="_from-sm"
|
|
99
|
+
to="/playlists"
|
|
100
|
+
/>
|
|
101
|
+
</GridPlaylists>
|
|
102
|
+
</section>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<script setup lang="ts">
|
|
107
|
+
import {
|
|
108
|
+
getLibraryCategoriesStub,
|
|
109
|
+
getLibraryChannelsStub,
|
|
110
|
+
getLibraryModelsStub,
|
|
111
|
+
getLibraryPlaylistsStub
|
|
112
|
+
} from '../stubs/library-follows';
|
|
113
|
+
import { convertString } from '../runtime';
|
|
114
|
+
import type { IBreadcrumbItem, ICategoryCard, IRoundCard } from '../types';
|
|
115
|
+
|
|
116
|
+
const { t } = useI18n();
|
|
117
|
+
const { generateLink } = useGenerateLink();
|
|
118
|
+
|
|
119
|
+
/** Сначала те, у кого больше новинок — ради этого страница и открывается. */
|
|
120
|
+
const byNewCount = <T extends { newCount: number }>(items: T[]) => [...items].sort((a, b) => b.newCount - a.newCount);
|
|
121
|
+
|
|
122
|
+
const sumUpdates = (items: { newCount: number }[]) => items.reduce((total, item) => total + item.newCount, 0);
|
|
123
|
+
|
|
124
|
+
const modelsStub = computed(() => byNewCount(getLibraryModelsStub()));
|
|
125
|
+
const channelsStub = computed(() => byNewCount(getLibraryChannelsStub()));
|
|
126
|
+
const categoriesStub = computed(() => byNewCount(getLibraryCategoriesStub()));
|
|
127
|
+
const playlists = computed(() => byNewCount(getLibraryPlaylistsStub()));
|
|
128
|
+
|
|
129
|
+
const models = computed<IRoundCard[]>(() => modelsStub.value.map(model => ({
|
|
130
|
+
title: model.title,
|
|
131
|
+
primaryImageUrl: model.avatarUrl,
|
|
132
|
+
videosCount: model.videosCount,
|
|
133
|
+
link: `/models/${convertString().toSlug(model.title)}`,
|
|
134
|
+
newCount: model.newCount,
|
|
135
|
+
})));
|
|
136
|
+
|
|
137
|
+
const channels = computed<IRoundCard[]>(() => channelsStub.value.map(channel => ({
|
|
138
|
+
title: channel.title,
|
|
139
|
+
primaryImageUrl: channel.avatarUrl ?? '',
|
|
140
|
+
videosCount: channel.videosCount,
|
|
141
|
+
link: `/channels/${convertString().toSlug(channel.title)}`,
|
|
142
|
+
newCount: channel.newCount,
|
|
143
|
+
})));
|
|
144
|
+
|
|
145
|
+
const categories = computed<ICategoryCard[]>(() => categoriesStub.value.map(category => ({
|
|
146
|
+
guid: category.guid,
|
|
147
|
+
name: convertString().toSlug(category.title),
|
|
148
|
+
title: category.title,
|
|
149
|
+
videosCount: category.videosCount,
|
|
150
|
+
thumbUrls: category.videos[0]?.thumbUrls,
|
|
151
|
+
newCount: category.newCount,
|
|
152
|
+
})));
|
|
153
|
+
|
|
154
|
+
const modelsUpdates = computed(() => sumUpdates(modelsStub.value));
|
|
155
|
+
const channelsUpdates = computed(() => sumUpdates(channelsStub.value));
|
|
156
|
+
const categoriesUpdates = computed(() => sumUpdates(categoriesStub.value));
|
|
157
|
+
const playlistsUpdates = computed(() => sumUpdates(playlists.value));
|
|
158
|
+
|
|
159
|
+
useNoindex();
|
|
160
|
+
|
|
161
|
+
const { brandName } = useAppConfig();
|
|
162
|
+
|
|
163
|
+
useHead(() => ({
|
|
164
|
+
title: `${t('library.following')} — ${brandName}`,
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const breadcrumbs = [
|
|
168
|
+
{
|
|
169
|
+
name: `${t('navigation.main')}`,
|
|
170
|
+
to: '/',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: `${t('library.following')}`,
|
|
174
|
+
},
|
|
175
|
+
] as IBreadcrumbItem[];
|
|
176
|
+
</script>
|
|
177
|
+
|
|
178
|
+
<style lang="scss">
|
|
179
|
+
.following__section {
|
|
180
|
+
margin-bottom: 48px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.following__header {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
justify-content: space-between;
|
|
187
|
+
column-gap: 16px;
|
|
188
|
+
margin-bottom: 16px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.following__link {
|
|
192
|
+
flex-shrink: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Плитка «смотреть все» часто оказывается одна в новом ряду: без равных рядов
|
|
196
|
+
ряд сжимается по её контенту и она выходит ниже карточек. */
|
|
197
|
+
.following .grid-categories,
|
|
198
|
+
.following .grid-playlists {
|
|
199
|
+
grid-auto-rows: 1fr;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
package/pages/library/index.vue
CHANGED
|
@@ -97,13 +97,16 @@
|
|
|
97
97
|
{{ t('library.following') }}
|
|
98
98
|
</h2>
|
|
99
99
|
|
|
100
|
-
<
|
|
100
|
+
<UiLink
|
|
101
|
+
:to="generateLink('/following')"
|
|
102
|
+
theme="secondary"
|
|
103
|
+
>
|
|
101
104
|
{{ t('library.whats_new', { count: newTotal }) }}
|
|
102
105
|
<UiIcon
|
|
103
106
|
name="arrow-right"
|
|
104
107
|
size="16"
|
|
105
108
|
/>
|
|
106
|
-
</
|
|
109
|
+
</UiLink>
|
|
107
110
|
</header>
|
|
108
111
|
|
|
109
112
|
<ul class="library__grid">
|