itube-specs 0.0.897 → 0.0.899

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.
@@ -0,0 +1,205 @@
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
+ size="xl"
21
+ placeholder-avatar="/img/model-placeholder.webp"
22
+ class="following__section"
23
+ without-count
24
+ />
25
+
26
+ <SectionSwiperCards
27
+ :items="channels"
28
+ :count="t('following.updates', { count: channelsUpdates })"
29
+ :title="t('channels')"
30
+ :all-link-text="t('following.view_all')"
31
+ :all-link-note="t('plural.channels', channels.length)"
32
+ :header-link-text="t('following.all_channels')"
33
+ all-link="/library/channels"
34
+ icon="play-circle"
35
+ size="xl"
36
+ placeholder-avatar="/img/logo-placeholder.svg"
37
+ class="following__section"
38
+ without-count
39
+ />
40
+
41
+ <section class="following__section">
42
+ <header class="following__header">
43
+ <UiSectionTitle
44
+ :title="t('categories')"
45
+ :count="t('following.updates', { count: categoriesUpdates })"
46
+ icon="grid"
47
+ title-tag="h2"
48
+ size="xl"
49
+ />
50
+
51
+ <!-- На мобиле плитки «все» в гриде нет, поэтому ссылка вынесена в шапку -->
52
+ <UiLink
53
+ :to="generateLink('/library/categories')"
54
+ class="following__link _to-sm"
55
+ theme="simple"
56
+ >
57
+ {{ t('following.all_categories') }}
58
+ <UiIcon
59
+ name="arrow-right"
60
+ size="16"
61
+ />
62
+ </UiLink>
63
+ </header>
64
+
65
+ <GridCategories :categories="categories">
66
+ <UiViewAll
67
+ :text="t('following.view_all')"
68
+ :note="t('plural.categories', categories.length)"
69
+ class="_from-sm"
70
+ to="/library/categories"
71
+ />
72
+ </GridCategories>
73
+ </section>
74
+
75
+ <section class="following__section">
76
+ <header class="following__header">
77
+ <UiSectionTitle
78
+ :title="t('playlists')"
79
+ :count="t('following.updates', { count: playlistsUpdates })"
80
+ icon="playlist"
81
+ title-tag="h2"
82
+ size="xl"
83
+ />
84
+
85
+ <UiLink
86
+ :to="generateLink('/playlists')"
87
+ class="following__link _to-sm"
88
+ theme="simple"
89
+ >
90
+ {{ t('following.all_playlists') }}
91
+ <UiIcon
92
+ name="arrow-right"
93
+ size="16"
94
+ />
95
+ </UiLink>
96
+ </header>
97
+
98
+ <GridPlaylists :items="playlists">
99
+ <UiViewAll
100
+ :text="t('following.view_all')"
101
+ :note="t('plural.playlists', playlists.length)"
102
+ class="_from-sm"
103
+ to="/playlists"
104
+ />
105
+ </GridPlaylists>
106
+ </section>
107
+ </div>
108
+ </template>
109
+
110
+ <script setup lang="ts">
111
+ import {
112
+ getLibraryCategoriesStub,
113
+ getLibraryChannelsStub,
114
+ getLibraryModelsStub,
115
+ getLibraryPlaylistsStub
116
+ } from '../stubs/library-follows';
117
+ import { convertString } from '../runtime';
118
+ import type { IBreadcrumbItem, ICategoryCard, IRoundCard } from '../types';
119
+
120
+ const { t } = useI18n();
121
+ const { generateLink } = useGenerateLink();
122
+
123
+ /** Сначала те, у кого больше новинок — ради этого страница и открывается. */
124
+ const byNewCount = <T extends { newCount: number }>(items: T[]) => [...items].sort((a, b) => b.newCount - a.newCount);
125
+
126
+ const sumUpdates = (items: { newCount: number }[]) => items.reduce((total, item) => total + item.newCount, 0);
127
+
128
+ const modelsStub = computed(() => byNewCount(getLibraryModelsStub()));
129
+ const channelsStub = computed(() => byNewCount(getLibraryChannelsStub()));
130
+ const categoriesStub = computed(() => byNewCount(getLibraryCategoriesStub()));
131
+ const playlists = computed(() => byNewCount(getLibraryPlaylistsStub()));
132
+
133
+ const models = computed<IRoundCard[]>(() => modelsStub.value.map(model => ({
134
+ title: model.title,
135
+ primaryImageUrl: model.avatarUrl,
136
+ videosCount: model.videosCount,
137
+ link: `/models/${convertString().toSlug(model.title)}`,
138
+ newCount: model.newCount,
139
+ })));
140
+
141
+ const channels = computed<IRoundCard[]>(() => channelsStub.value.map(channel => ({
142
+ title: channel.title,
143
+ primaryImageUrl: channel.avatarUrl ?? '',
144
+ videosCount: channel.videosCount,
145
+ link: `/channels/${convertString().toSlug(channel.title)}`,
146
+ newCount: channel.newCount,
147
+ })));
148
+
149
+ const categories = computed<ICategoryCard[]>(() => categoriesStub.value.map(category => ({
150
+ guid: category.guid,
151
+ name: convertString().toSlug(category.title),
152
+ title: category.title,
153
+ videosCount: category.videosCount,
154
+ thumbUrls: category.videos[0]?.thumbUrls,
155
+ newCount: category.newCount,
156
+ })));
157
+
158
+ const modelsUpdates = computed(() => sumUpdates(modelsStub.value));
159
+ const channelsUpdates = computed(() => sumUpdates(channelsStub.value));
160
+ const categoriesUpdates = computed(() => sumUpdates(categoriesStub.value));
161
+ const playlistsUpdates = computed(() => sumUpdates(playlists.value));
162
+
163
+ useNoindex();
164
+
165
+ const { brandName } = useAppConfig();
166
+
167
+ useHead(() => ({
168
+ title: `${t('library.following')} — ${brandName}`,
169
+ }));
170
+
171
+ const breadcrumbs = [
172
+ {
173
+ name: `${t('navigation.main')}`,
174
+ to: '/',
175
+ },
176
+ {
177
+ name: `${t('library.following')}`,
178
+ },
179
+ ] as IBreadcrumbItem[];
180
+ </script>
181
+
182
+ <style lang="scss">
183
+ .following__section {
184
+ margin-bottom: 48px;
185
+ }
186
+
187
+ .following__header {
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ column-gap: 16px;
192
+ margin-bottom: 16px;
193
+ }
194
+
195
+ .following__link {
196
+ flex-shrink: 0;
197
+ }
198
+
199
+ /* Плитка «смотреть все» часто оказывается одна в новом ряду: без равных рядов
200
+ ряд сжимается по её контенту и она выходит ниже карточек. */
201
+ .following .grid-categories,
202
+ .following .grid-playlists {
203
+ grid-auto-rows: 1fr;
204
+ }
205
+ </style>
@@ -21,15 +21,12 @@
21
21
  class="library__section"
22
22
  >
23
23
  <header class="library__section-header">
24
- <h2 class="library__heading">
25
- <span class="library__heading-icon">
26
- <UiIcon
27
- name="history"
28
- size="16"
29
- />
30
- </span>
31
- {{ t('library.continue_watching') }}
32
- </h2>
24
+ <UiSectionTitle
25
+ :title="t('library.continue_watching')"
26
+ icon="history"
27
+ title-tag="h2"
28
+ size="xl"
29
+ />
33
30
 
34
31
  <UiLink
35
32
  :to="generateLink('/history')"
@@ -59,15 +56,12 @@
59
56
 
60
57
  <section class="library__section">
61
58
  <header class="library__section-header">
62
- <h2 class="library__heading">
63
- <span class="library__heading-icon">
64
- <UiIcon
65
- name="layers"
66
- size="16"
67
- />
68
- </span>
69
- {{ t('library.collections') }}
70
- </h2>
59
+ <UiSectionTitle
60
+ :title="t('library.collections')"
61
+ icon="layers"
62
+ title-tag="h2"
63
+ size="xl"
64
+ />
71
65
  </header>
72
66
 
73
67
  <ul class="library__grid">
@@ -87,23 +81,23 @@
87
81
 
88
82
  <section class="library__section">
89
83
  <header class="library__section-header">
90
- <h2 class="library__heading">
91
- <span class="library__heading-icon">
92
- <UiIcon
93
- name="plus"
94
- size="16"
95
- />
96
- </span>
97
- {{ t('library.following') }}
98
- </h2>
99
-
100
- <UiBtn theme="secondary">
84
+ <UiSectionTitle
85
+ :title="t('library.following')"
86
+ icon="plus"
87
+ title-tag="h2"
88
+ size="xl"
89
+ />
90
+
91
+ <UiLink
92
+ :to="generateLink('/following')"
93
+ theme="secondary"
94
+ >
101
95
  {{ t('library.whats_new', { count: newTotal }) }}
102
96
  <UiIcon
103
97
  name="arrow-right"
104
98
  size="16"
105
99
  />
106
- </UiBtn>
100
+ </UiLink>
107
101
  </header>
108
102
 
109
103
  <ul class="library__grid">
@@ -304,28 +298,6 @@ const breadcrumbs = [
304
298
  margin-bottom: 16px;
305
299
  }
306
300
 
307
- .library__heading {
308
- @include font-xl;
309
-
310
- display: flex;
311
- align-items: center;
312
- column-gap: 8px;
313
- margin: 0;
314
- font-weight: 600;
315
- color: $page-library-heading;
316
- }
317
-
318
- .library__heading-icon {
319
- display: grid;
320
- place-content: center;
321
- flex-shrink: 0;
322
- width: 24px;
323
- height: 24px;
324
- color: $page-library-heading-icon;
325
- background-color: $page-library-heading-icon-bg;
326
- border-radius: $radius-md;
327
- }
328
-
329
301
  .library__continue {
330
302
  display: grid;
331
303
  gap: 16px;
@@ -56,13 +56,12 @@
56
56
  v-if="modelsData.total > MAXIMUM_MODELS"
57
57
  #append
58
58
  >
59
- <NuxtLink
60
- :to="generateLink(`/search/${slug}/models`)"
61
- class="section-swiper-cards__all"
62
- >
63
- <span>+{{(modelsData.total - MAXIMUM_MODELS) }}</span>
64
- <span>{{ $t('models')}}</span>
65
- </NuxtLink>
59
+ <UiViewAll
60
+ :to="`/search/${slug}/models`"
61
+ :text="`+${modelsData.total - MAXIMUM_MODELS}`"
62
+ :note="$t('models')"
63
+ mini
64
+ />
66
65
  </template>
67
66
  </UiSwiper>
68
67
  </div>
@@ -86,14 +85,13 @@
86
85
  v-if="featureFlags.AdsNativeEnabled"
87
86
  hydrate-on-visible
88
87
  />
89
- <NuxtLink
88
+ <UiViewAll
90
89
  v-if="videosData.items.length > MAXIMUM_VIDEOS || videosData.total > MAXIMUM_VIDEOS"
91
- :to="generateLink(`/search/${slug}/videos`)"
90
+ :to="`/search/${slug}/videos`"
91
+ :text="`+${videosData.total - MAXIMUM_VIDEOS}`"
92
+ :note="$t('show_more_videos')"
92
93
  class="search__show-all"
93
- >
94
- <span class="search__show-all-number">+{{ videosData.total - MAXIMUM_VIDEOS }}</span>
95
- {{ $t('show_more_videos') }}
96
- </NuxtLink>
94
+ />
97
95
  </template>
98
96
  </GridVideos>
99
97
  </div>
@@ -232,38 +230,22 @@ const breadcrumbs = [
232
230
  min-width: 0;
233
231
  }
234
232
 
235
- .search__show-all {
236
- @include font-sm;
233
+ /* Слайд ровно по круглой карточке: 80px на мобиле, 96px с sm */
234
+ .search__result-section .ui-swiper__slide {
235
+ width: var(--card-round-size);
236
+ }
237
237
 
238
- display: grid;
239
- row-gap: 4px;
240
- place-content: center;
238
+ /* Плитка стоит в гриде видео — равняем её по превью, а не по всей карточке */
239
+ .search__show-all {
241
240
  align-self: start;
242
241
  aspect-ratio: 16/9;
243
- background-color: $search-show-all-bg;
244
- border-radius: $radius-lg;
245
- border: 1px solid $search-show-all-border;
246
- color: $search-show-all-text;
247
-
248
- @include hover(border-color, color) {
249
- border-color: $search-show-all-border--hover;
250
- color: $search-show-all-text--hover;
251
- }
252
242
  }
253
243
 
254
- .search__show-all-number {
244
+ /* Здесь главный акцент — число, поэтому крупнее дефолтной подписи плитки */
245
+ .search__show-all .ui-view-all__text {
255
246
  @include font-2xl;
256
247
 
257
- text-align: center;
258
248
  font-weight: 700;
259
- color: $search-show-all-number;
260
- transition: var(--transition) color;
261
- }
262
-
263
- .search__show-all:is(:hover,:focus-visible) .search__show-all-number {
264
- @include from-br(sm) {
265
- color: $search-show-all-number--hover;
266
- }
267
249
  }
268
250
 
269
251
  .search__title {
@@ -1,5 +1,7 @@
1
1
  import { VIDEO_POOL } from './videos';
2
2
  import type { StubVideo } from './videos';
3
+ import { PlaylistType } from '../runtime/constants/playlist-type';
4
+ import type { IPlaylistCard } from '../types';
3
5
 
4
6
  const HOUR = 3600;
5
7
  const DAY = 24 * HOUR;
@@ -41,6 +43,9 @@ export type LibraryChannelStub = LibraryFollowBase & {
41
43
 
42
44
  export type LibraryCategoryStub = LibraryFollowBase;
43
45
 
46
+ /** Плейлист подписки: карточка как в общем гриде + число новых роликов. */
47
+ export type LibraryPlaylistStub = IPlaylistCard & { newCount: number };
48
+
44
49
  const now = () => Math.floor(Date.now() / 1000);
45
50
 
46
51
  const toSlug = (title: string) => title.toLowerCase().replace(/\s+/g, '-');
@@ -64,6 +69,17 @@ const MODELS = [
64
69
  { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top', 'Tattooed'], updatedAgo: 2 * DAY, videoOffset: 9 },
65
70
  { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom', 'Blond', 'Solo'], updatedAgo: 9 * DAY, videoOffset: 0 },
66
71
  { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom', 'Fisting', 'Hardcore', 'Professional'], updatedAgo: 21 * DAY, videoOffset: 6 },
72
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
73
+ { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top', 'Tattooed'], updatedAgo: 2 * DAY, videoOffset: 9 },
74
+ { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom', 'Blond', 'Solo'], updatedAgo: 9 * DAY, videoOffset: 0 },
75
+ { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom', 'Fisting', 'Hardcore', 'Professional'], updatedAgo: 21 * DAY, videoOffset: 6 },
76
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
77
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
78
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
79
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
80
+ { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top', 'Tattooed'], updatedAgo: 2 * DAY, videoOffset: 9 },
81
+ { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom', 'Blond', 'Solo'], updatedAgo: 9 * DAY, videoOffset: 0 },
82
+ { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom', 'Fisting', 'Hardcore', 'Professional'], updatedAgo: 21 * DAY, videoOffset: 6 },
67
83
  ];
68
84
 
69
85
  const CHANNELS = [
@@ -71,6 +87,18 @@ const CHANNELS = [
71
87
  { title: 'Icon Male', avatarUrl: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', videosCount: 128, followersCount: 128400, newCount: 2, tags: ['Muscle', 'Daddy', 'Bareback', 'Professional', 'Exclusive', 'HD', 'Studio'], updatedAgo: DAY, videoOffset: 0 },
72
88
  { title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
73
89
  { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
90
+ { title: 'Top To Bottom', avatarUrl: undefined, videosCount: 17, followersCount: 12800, newCount: 3, tags: ['Versatile', 'Flip-Flop', 'HD', 'Professional', 'Exclusive', 'Bareback'], updatedAgo: 4 * HOUR, videoOffset: 2 },
91
+ { title: 'Icon Male', avatarUrl: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', videosCount: 128, followersCount: 128400, newCount: 2, tags: ['Muscle', 'Daddy', 'Bareback', 'Professional', 'Exclusive', 'HD', 'Studio'], updatedAgo: DAY, videoOffset: 0 },
92
+ { title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
93
+ { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
94
+ { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
95
+ { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
96
+ { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
97
+ { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
98
+ { title: 'Top To Bottom', avatarUrl: undefined, videosCount: 17, followersCount: 12800, newCount: 3, tags: ['Versatile', 'Flip-Flop', 'HD', 'Professional', 'Exclusive', 'Bareback'], updatedAgo: 4 * HOUR, videoOffset: 2 },
99
+ { title: 'Icon Male', avatarUrl: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', videosCount: 128, followersCount: 128400, newCount: 2, tags: ['Muscle', 'Daddy', 'Bareback', 'Professional', 'Exclusive', 'HD', 'Studio'], updatedAgo: DAY, videoOffset: 0 },
100
+ { title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
101
+ { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
74
102
  ];
75
103
 
76
104
  /**
@@ -80,8 +108,27 @@ const CHANNELS = [
80
108
  const CATEGORIES = [
81
109
  { title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
82
110
  { title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 },
83
- { title: 'Bears', videosCount: 92, newCount: 0, updatedAgo: 11 * DAY, videoOffset: 10 },
84
- { title: 'Firemen', videosCount: 61, newCount: 0, updatedAgo: 24 * DAY, videoOffset: 8 },
111
+ { title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
112
+ { title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 },
113
+ { title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
114
+ { title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 },
115
+ ];
116
+
117
+ /** Чужие плейлисты, на которые подписан пользователь. */
118
+ const PLAYLISTS = [
119
+ { name: 'Suits after office', username: 'andyblair2', videosCount: 24, likes: 156, views: 1200, newCount: 4, createdAgo: 12 * DAY, videoOffset: 0 },
120
+ { name: 'Weekend binge', username: 'mattdvoe', videosCount: 33, likes: 271, views: 2400, newCount: 5, createdAgo: 28 * DAY, videoOffset: 4 },
121
+ { name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
122
+ { name: 'Late night picks', username: 'romerofx', videosCount: 7, likes: 42, views: 340, newCount: 1, createdAgo: 5 * DAY, videoOffset: 2 },
123
+ { name: 'Suits after office', username: 'andyblair2', videosCount: 24, likes: 156, views: 1200, newCount: 4, createdAgo: 12 * DAY, videoOffset: 0 },
124
+ { name: 'Weekend binge', username: 'mattdvoe', videosCount: 33, likes: 271, views: 2400, newCount: 5, createdAgo: 28 * DAY, videoOffset: 4 },
125
+ { name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
126
+ { name: 'Late night picks', username: 'romerofx', videosCount: 7, likes: 42, views: 340, newCount: 1, createdAgo: 5 * DAY, videoOffset: 2 },
127
+ { name: 'Suits after office', username: 'andyblair2', videosCount: 24, likes: 156, views: 1200, newCount: 4, createdAgo: 12 * DAY, videoOffset: 0 },
128
+ { name: 'Weekend binge', username: 'mattdvoe', videosCount: 33, likes: 271, views: 2400, newCount: 5, createdAgo: 28 * DAY, videoOffset: 4 },
129
+ { name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
130
+ { name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
131
+ { name: 'Late night picks', username: 'romerofx', videosCount: 7, likes: 42, views: 340, newCount: 1, createdAgo: 5 * DAY, videoOffset: 2 },
85
132
  ];
86
133
 
87
134
  /**
@@ -119,6 +166,32 @@ export function getLibraryChannelsStub(): LibraryChannelStub[] {
119
166
  });
120
167
  }
121
168
 
169
+ /** Мозаика обложек плейлиста: карточка раскладывает до 4 тумб. */
170
+ function buildThumbs(offset: number, count: number) {
171
+ return Array.from({ length: Math.min(count, 4) }, (_, index) => (
172
+ VIDEO_POOL[(offset + index) % VIDEO_POOL.length].thumbUrls
173
+ ));
174
+ }
175
+
176
+ export function getLibraryPlaylistsStub(): LibraryPlaylistStub[] {
177
+ const stamp = now();
178
+
179
+ return PLAYLISTS.map(({ createdAgo, videoOffset, ...playlist }, index) => ({
180
+ ...playlist,
181
+ id: `${index + 1}`,
182
+ playlistType: PlaylistType.Public,
183
+ created: stamp - createdAgo,
184
+ firstVideoId: VIDEO_POOL[videoOffset % VIDEO_POOL.length].id,
185
+ thumbUrls: buildThumbs(videoOffset, playlist.videosCount),
186
+ }));
187
+ }
188
+
189
+ /** Сумма новинок по всем подпискам — счётчик на колокольчике и в шапках секций. */
190
+ export function getFollowingUpdatesTotal(): number {
191
+ return [...MODELS, ...CHANNELS, ...CATEGORIES, ...PLAYLISTS]
192
+ .reduce((total, item) => total + item.newCount, 0);
193
+ }
194
+
122
195
  export function getLibraryCategoriesStub(): LibraryCategoryStub[] {
123
196
  const stamp = now();
124
197
 
@@ -1 +1 @@
1
- export type ButtonThemes = 'primary' | 'secondary' | 'warning' | 'muted' | undefined
1
+ export type ButtonThemes = 'primary' | 'secondary' | 'warning' | 'muted' | 'simple' | undefined
@@ -9,4 +9,6 @@ export interface ICategoryCard {
9
9
  isTop?: boolean,
10
10
  icon?: string;
11
11
  description?: string,
12
+ /** Новинки с прошлого захода — бейдж «3 new» (страница подписок) */
13
+ newCount?: number;
12
14
  }
@@ -14,4 +14,6 @@ export interface IPlaylistCard {
14
14
  views?: number;
15
15
  likes?: number;
16
16
  dislikes?: number;
17
+ /** Новинки с прошлого захода — бейдж «3 new» (страница подписок) */
18
+ newCount?: number;
17
19
  }
@@ -3,4 +3,6 @@ export interface IRoundCard {
3
3
  primaryImageUrl: string
4
4
  videosCount: number
5
5
  link: string
6
+ /** Новинки с прошлого захода — бейдж на аватаре (страница подписок) */
7
+ newCount?: number
6
8
  }