itube-specs 0.0.893 → 0.0.896
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/scss/themes/dark/_theme.scss +16 -12
- package/assets/scss/themes/light/_theme.scss +16 -12
- package/components/card/video/index.vue +17 -2
- package/components/library/feed.vue +340 -0
- package/components/library/follows.vue +147 -0
- package/components/ui/expand-row.vue +21 -2
- package/package.json +1 -1
- package/pages/library/categories.vue +61 -0
- package/pages/library/channels.vue +62 -0
- package/pages/library/index.vue +2 -2
- package/pages/library/models.vue +25 -130
- package/stubs/library-follows.ts +135 -0
- package/types/index.d.ts +1 -0
- package/types/library-feed.d.ts +34 -0
- package/components/library/model.vue +0 -307
- package/stubs/library-models.ts +0 -75
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!-- Подписки на категории: широкие показывают лучшее за неделю, узкие — новинки. Данные пока заглушка -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="_page _container">
|
|
4
|
+
<UiBreadcrumbs
|
|
5
|
+
:items="breadcrumbs"
|
|
6
|
+
class="_breadcrumbs"
|
|
7
|
+
/>
|
|
8
|
+
|
|
9
|
+
<LibraryFollows
|
|
10
|
+
:items="items"
|
|
11
|
+
:title="t('library.my_categories')"
|
|
12
|
+
icon="grid"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { getLibraryCategoriesStub } from '../../stubs/library-follows';
|
|
19
|
+
import { convertString, formatDate } from '../../runtime';
|
|
20
|
+
import type { IBreadcrumbItem, ILibraryFeedItem } from '../../types';
|
|
21
|
+
|
|
22
|
+
const { t } = useI18n();
|
|
23
|
+
const { generateLink } = useGenerateLink();
|
|
24
|
+
|
|
25
|
+
const items = computed<ILibraryFeedItem[]>(() => getLibraryCategoriesStub().map(category => ({
|
|
26
|
+
guid: category.guid,
|
|
27
|
+
title: category.title,
|
|
28
|
+
to: generateLink(`/categories/${convertString().toSlug(category.title)}`),
|
|
29
|
+
newCount: category.newCount,
|
|
30
|
+
meta: [
|
|
31
|
+
{ icon: 'video-camera', text: String(category.videosCount) },
|
|
32
|
+
{ icon: 'date', text: t('library.updated_at', { date: formatDate(category.lastVideoAddedAt) }) },
|
|
33
|
+
],
|
|
34
|
+
videosCount: category.videosCount,
|
|
35
|
+
// Нечего показать в новинках — лента откатывается на лучшее за неделю
|
|
36
|
+
feedTitle: category.newCount ? t('library.latest') : t('library.top_this_week'),
|
|
37
|
+
videos: category.videos,
|
|
38
|
+
})));
|
|
39
|
+
|
|
40
|
+
useNoindex();
|
|
41
|
+
|
|
42
|
+
const { brandName } = useAppConfig();
|
|
43
|
+
|
|
44
|
+
useHead(() => ({
|
|
45
|
+
title: `${t('library.my_categories')} — ${brandName}`,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const breadcrumbs = [
|
|
49
|
+
{
|
|
50
|
+
name: `${t('navigation.main')}`,
|
|
51
|
+
to: '/',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: `${t('navigation.library')}`,
|
|
55
|
+
to: '/library',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: `${t('library.my_categories')}`,
|
|
59
|
+
},
|
|
60
|
+
] as IBreadcrumbItem[];
|
|
61
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!-- Подписки на каналы: аватар, теги, счётчик подписчиков, лента последних роликов. Данные пока заглушка -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="_page _container">
|
|
4
|
+
<UiBreadcrumbs
|
|
5
|
+
:items="breadcrumbs"
|
|
6
|
+
class="_breadcrumbs"
|
|
7
|
+
/>
|
|
8
|
+
|
|
9
|
+
<LibraryFollows
|
|
10
|
+
:items="items"
|
|
11
|
+
:title="t('library.my_channels')"
|
|
12
|
+
icon="play-circle"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { getLibraryChannelsStub } from '../../stubs/library-follows';
|
|
19
|
+
import { convertString, formatDate } from '../../runtime';
|
|
20
|
+
import type { IBreadcrumbItem, ILibraryFeedItem } from '../../types';
|
|
21
|
+
|
|
22
|
+
const { t, n } = useI18n();
|
|
23
|
+
const { generateLink } = useGenerateLink();
|
|
24
|
+
|
|
25
|
+
const items = computed<ILibraryFeedItem[]>(() => getLibraryChannelsStub().map(channel => ({
|
|
26
|
+
guid: channel.guid,
|
|
27
|
+
title: channel.title,
|
|
28
|
+
to: generateLink(`/channels/${convertString().toSlug(channel.title)}`),
|
|
29
|
+
avatarUrl: channel.avatarUrl,
|
|
30
|
+
tags: channel.tags.map(tag => convertString().toChips(tag, 'tag')),
|
|
31
|
+
newCount: channel.newCount,
|
|
32
|
+
meta: [
|
|
33
|
+
{ icon: 'video-camera', text: String(channel.videosCount) },
|
|
34
|
+
{ icon: 'user-group', text: t('library.followers', { count: n(channel.followersCount, 'compact') }) },
|
|
35
|
+
{ icon: 'date', text: t('library.updated_at', { date: formatDate(channel.lastVideoAddedAt) }) },
|
|
36
|
+
],
|
|
37
|
+
videosCount: channel.videosCount,
|
|
38
|
+
videos: channel.videos,
|
|
39
|
+
})));
|
|
40
|
+
|
|
41
|
+
useNoindex();
|
|
42
|
+
|
|
43
|
+
const { brandName } = useAppConfig();
|
|
44
|
+
|
|
45
|
+
useHead(() => ({
|
|
46
|
+
title: `${t('library.my_channels')} — ${brandName}`,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const breadcrumbs = [
|
|
50
|
+
{
|
|
51
|
+
name: `${t('navigation.main')}`,
|
|
52
|
+
to: '/',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: `${t('navigation.library')}`,
|
|
56
|
+
to: '/library',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: `${t('library.my_channels')}`,
|
|
60
|
+
},
|
|
61
|
+
] as IBreadcrumbItem[];
|
|
62
|
+
</script>
|
package/pages/library/index.vue
CHANGED
|
@@ -212,14 +212,14 @@ const following = computed<LibraryLink[]>(() => [
|
|
|
212
212
|
badge: t('library.new_count', { count: libraryStub.newModels }),
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
|
-
to: generateLink('/channels'),
|
|
215
|
+
to: generateLink('/library/channels'),
|
|
216
216
|
icon: 'play-circle',
|
|
217
217
|
title: t('channels'),
|
|
218
218
|
subtitle: t('plural.channels', libraryStub.channels),
|
|
219
219
|
badge: t('library.new_count', { count: libraryStub.newChannels }),
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
|
-
to: generateLink('/categories'),
|
|
222
|
+
to: generateLink('/library/categories'),
|
|
223
223
|
icon: 'grid',
|
|
224
224
|
title: t('categories'),
|
|
225
225
|
subtitle: t('plural.categories', libraryStub.categories),
|
package/pages/library/models.vue
CHANGED
|
@@ -1,120 +1,43 @@
|
|
|
1
|
-
<!-- Подписки на моделей:
|
|
1
|
+
<!-- Подписки на моделей: аватар, теги, лента последних роликов. Данные пока заглушка -->
|
|
2
2
|
<template>
|
|
3
|
-
<div class="
|
|
3
|
+
<div class="_page _container">
|
|
4
4
|
<UiBreadcrumbs
|
|
5
5
|
:items="breadcrumbs"
|
|
6
6
|
class="_breadcrumbs"
|
|
7
7
|
/>
|
|
8
8
|
|
|
9
|
-
<
|
|
9
|
+
<LibraryFollows
|
|
10
|
+
:items="items"
|
|
10
11
|
:title="t('library.my_models')"
|
|
11
|
-
:count="models.length"
|
|
12
|
-
class="library-models__title"
|
|
13
12
|
icon="user"
|
|
14
13
|
/>
|
|
15
|
-
|
|
16
|
-
<div class="library-models__sort">
|
|
17
|
-
<UiBtn
|
|
18
|
-
v-for="item in sortItems"
|
|
19
|
-
:key="item.name"
|
|
20
|
-
:theme="sort === item.name ? 'primary' : 'secondary'"
|
|
21
|
-
@click="sort = item.name"
|
|
22
|
-
>{{ item.title }}</UiBtn>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<div
|
|
26
|
-
v-if="models.length > 0"
|
|
27
|
-
class="library-models__list"
|
|
28
|
-
>
|
|
29
|
-
<LibraryModel
|
|
30
|
-
v-for="(model, index) in models"
|
|
31
|
-
:key="model.guid"
|
|
32
|
-
:model="model"
|
|
33
|
-
:following="isFollowing(model)"
|
|
34
|
-
:priority="index === 0"
|
|
35
|
-
@toggle-follow="onToggleFollow"
|
|
36
|
-
/>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<UiEmptyState
|
|
40
|
-
v-else
|
|
41
|
-
without-reset
|
|
42
|
-
/>
|
|
43
|
-
|
|
44
|
-
<UiConfirm
|
|
45
|
-
v-model="isUnfollowOpen"
|
|
46
|
-
:title="t('library.unfollow_title', { name: pendingUnfollow?.title })"
|
|
47
|
-
:confirm-text="t('library.unfollow_confirm')"
|
|
48
|
-
@confirm="onUnfollowConfirm"
|
|
49
|
-
>
|
|
50
|
-
<p class="library-models__confirm">{{ t('library.unfollow_text') }}</p>
|
|
51
|
-
</UiConfirm>
|
|
52
14
|
</div>
|
|
53
15
|
</template>
|
|
54
16
|
|
|
55
17
|
<script setup lang="ts">
|
|
56
|
-
import { getLibraryModelsStub } from '../../stubs/library-
|
|
57
|
-
import
|
|
58
|
-
import type { IBreadcrumbItem } from '../../types';
|
|
59
|
-
|
|
60
|
-
const LibrarySort = {
|
|
61
|
-
New: 'new',
|
|
62
|
-
Alphabet: 'alphabet',
|
|
63
|
-
} as const;
|
|
64
|
-
|
|
65
|
-
type LibrarySortValue = typeof LibrarySort[keyof typeof LibrarySort];
|
|
18
|
+
import { getLibraryModelsStub } from '../../stubs/library-follows';
|
|
19
|
+
import { convertString, formatDate } from '../../runtime';
|
|
20
|
+
import type { IBreadcrumbItem, ILibraryFeedItem } from '../../types';
|
|
66
21
|
|
|
67
22
|
const { t } = useI18n();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
function isFollowing(model: LibraryModelStub) {
|
|
89
|
-
return !unfollowed.value.includes(model.guid);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Модель, для которой открыт попап отписки. */
|
|
93
|
-
const pendingUnfollow = ref<LibraryModelStub | null>(null);
|
|
94
|
-
|
|
95
|
-
const isUnfollowOpen = computed({
|
|
96
|
-
get: () => pendingUnfollow.value !== null,
|
|
97
|
-
set: value => {
|
|
98
|
-
if (!value) pendingUnfollow.value = null;
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
/** Подписка — сразу, отписка — только через подтверждение: она сносит ленту обновлений. */
|
|
103
|
-
function onToggleFollow(model: LibraryModelStub) {
|
|
104
|
-
if (isFollowing(model)) {
|
|
105
|
-
pendingUnfollow.value = model;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
unfollowed.value = unfollowed.value.filter(guid => guid !== model.guid);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function onUnfollowConfirm() {
|
|
113
|
-
if (!pendingUnfollow.value) return;
|
|
114
|
-
|
|
115
|
-
unfollowed.value = [...unfollowed.value, pendingUnfollow.value.guid];
|
|
116
|
-
pendingUnfollow.value = null;
|
|
117
|
-
}
|
|
23
|
+
const { generateLink } = useGenerateLink();
|
|
24
|
+
|
|
25
|
+
const items = computed<ILibraryFeedItem[]>(() => getLibraryModelsStub().map(model => ({
|
|
26
|
+
guid: model.guid,
|
|
27
|
+
title: model.title,
|
|
28
|
+
to: generateLink(`/models/${convertString().toSlug(model.title)}`),
|
|
29
|
+
avatarUrl: model.avatarUrl,
|
|
30
|
+
country: model.country,
|
|
31
|
+
tags: model.tags.map(tag => convertString().toChips(tag, 'tag')),
|
|
32
|
+
newCount: model.newCount,
|
|
33
|
+
meta: [
|
|
34
|
+
{ icon: 'video-camera', text: String(model.videosCount) },
|
|
35
|
+
{ text: `${model.age} ${t('yo')}` },
|
|
36
|
+
{ icon: 'date', text: t('library.updated_at', { date: formatDate(model.lastVideoAddedAt) }) },
|
|
37
|
+
],
|
|
38
|
+
videosCount: model.videosCount,
|
|
39
|
+
videos: model.videos,
|
|
40
|
+
})));
|
|
118
41
|
|
|
119
42
|
useNoindex();
|
|
120
43
|
|
|
@@ -138,31 +61,3 @@ const breadcrumbs = [
|
|
|
138
61
|
},
|
|
139
62
|
] as IBreadcrumbItem[];
|
|
140
63
|
</script>
|
|
141
|
-
|
|
142
|
-
<style lang="scss">
|
|
143
|
-
.library-models__title {
|
|
144
|
-
margin-bottom: 16px;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.library-models__sort {
|
|
148
|
-
display: flex;
|
|
149
|
-
flex-wrap: wrap;
|
|
150
|
-
align-items: center;
|
|
151
|
-
gap: 8px;
|
|
152
|
-
margin-bottom: 24px;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.library-models__list {
|
|
156
|
-
display: grid;
|
|
157
|
-
row-gap: 24px;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.library-models__confirm {
|
|
161
|
-
@include font-sm;
|
|
162
|
-
|
|
163
|
-
max-width: 400px;
|
|
164
|
-
margin: 0 auto;
|
|
165
|
-
text-align: center;
|
|
166
|
-
color: $page-library-confirm-text;
|
|
167
|
-
}
|
|
168
|
-
</style>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { VIDEO_POOL } from './videos';
|
|
2
|
+
import type { StubVideo } from './videos';
|
|
3
|
+
|
|
4
|
+
const HOUR = 3600;
|
|
5
|
+
const DAY = 24 * HOUR;
|
|
6
|
+
|
|
7
|
+
/** Сколько последних роликов сущности показываем в ленте. */
|
|
8
|
+
const PREVIEW_LIMIT = 5;
|
|
9
|
+
|
|
10
|
+
export type LibraryFollowVideo = StubVideo & {
|
|
11
|
+
guid: string;
|
|
12
|
+
isNew: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type LibraryFollowBase = {
|
|
16
|
+
guid: string;
|
|
17
|
+
title: string;
|
|
18
|
+
videosCount: number;
|
|
19
|
+
/** Сколько роликов вышло с прошлого захода; 0 — плашки «новое» нет */
|
|
20
|
+
newCount: number;
|
|
21
|
+
/** Unix timestamp (сек.) последнего добавленного ролика */
|
|
22
|
+
lastVideoAddedAt: number;
|
|
23
|
+
videos: LibraryFollowVideo[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type LibraryModelStub = LibraryFollowBase & {
|
|
27
|
+
avatarUrl: string;
|
|
28
|
+
country: {
|
|
29
|
+
name: string
|
|
30
|
+
title: string
|
|
31
|
+
};
|
|
32
|
+
age: number;
|
|
33
|
+
tags: string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type LibraryChannelStub = LibraryFollowBase & {
|
|
37
|
+
avatarUrl?: string;
|
|
38
|
+
followersCount: number;
|
|
39
|
+
tags: string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type LibraryCategoryStub = LibraryFollowBase;
|
|
43
|
+
|
|
44
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
45
|
+
|
|
46
|
+
const toSlug = (title: string) => title.toLowerCase().replace(/\s+/g, '-');
|
|
47
|
+
|
|
48
|
+
/** Нарезает пул на ленту сущности: guid уникален внутри неё, первые `newCount` помечены новинками. */
|
|
49
|
+
function buildVideos(slug: string, offset: number, newCount: number): LibraryFollowVideo[] {
|
|
50
|
+
return Array.from({ length: PREVIEW_LIMIT }, (_, index) => {
|
|
51
|
+
const video = VIDEO_POOL[(offset + index) % VIDEO_POOL.length];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...video,
|
|
55
|
+
guid: `${slug}-${video.id}`,
|
|
56
|
+
isNew: index < newCount,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Порядок по числу новинок намеренно не совпадает с алфавитным — иначе обе сортировки дают одно и то же. */
|
|
62
|
+
const MODELS = [
|
|
63
|
+
{ 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 },
|
|
64
|
+
{ 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
|
+
{ 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
|
+
{ 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
|
+
];
|
|
68
|
+
|
|
69
|
+
const CHANNELS = [
|
|
70
|
+
{ 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 },
|
|
71
|
+
{ 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
|
+
{ title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
|
|
73
|
+
{ 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
|
+
];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Широкие категории пополняются постоянно, поэтому новинки у них есть всегда; узкие вроде Firemen
|
|
78
|
+
* неделями стоят на месте — там `newCount: 0`, и лента откатывается на лучшее за неделю.
|
|
79
|
+
*/
|
|
80
|
+
const CATEGORIES = [
|
|
81
|
+
{ title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
|
|
82
|
+
{ 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 },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Подписки пользователя. Ручек под них пока нет — вёрстка живёт на заглушке.
|
|
89
|
+
* Возраст ролика считаем от «сейчас», чтобы даты в разметке не устаревали.
|
|
90
|
+
*/
|
|
91
|
+
export function getLibraryModelsStub(): LibraryModelStub[] {
|
|
92
|
+
const stamp = now();
|
|
93
|
+
|
|
94
|
+
return MODELS.map(({ image, updatedAgo, videoOffset, ...model }) => {
|
|
95
|
+
const guid = toSlug(model.title);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...model,
|
|
99
|
+
guid,
|
|
100
|
+
avatarUrl: `/img/models/${image}`,
|
|
101
|
+
lastVideoAddedAt: stamp - updatedAgo,
|
|
102
|
+
videos: buildVideos(guid, videoOffset, model.newCount),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getLibraryChannelsStub(): LibraryChannelStub[] {
|
|
108
|
+
const stamp = now();
|
|
109
|
+
|
|
110
|
+
return CHANNELS.map(({ updatedAgo, videoOffset, ...channel }) => {
|
|
111
|
+
const guid = toSlug(channel.title);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...channel,
|
|
115
|
+
guid,
|
|
116
|
+
lastVideoAddedAt: stamp - updatedAgo,
|
|
117
|
+
videos: buildVideos(guid, videoOffset, channel.newCount),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getLibraryCategoriesStub(): LibraryCategoryStub[] {
|
|
123
|
+
const stamp = now();
|
|
124
|
+
|
|
125
|
+
return CATEGORIES.map(({ updatedAgo, videoOffset, ...category }) => {
|
|
126
|
+
const guid = toSlug(category.title);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...category,
|
|
130
|
+
guid,
|
|
131
|
+
lastVideoAddedAt: stamp - updatedAgo,
|
|
132
|
+
videos: buildVideos(guid, videoOffset, category.newCount),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export * from './navigation-items.d.ts';
|
|
|
49
49
|
export * from './link-item.d.ts';
|
|
50
50
|
export * from './fluid-player.d.ts';
|
|
51
51
|
export * from './chips-item.d.ts';
|
|
52
|
+
export * from './library-feed.d.ts';
|
|
52
53
|
export * from './nav-link-item.d.ts';
|
|
53
54
|
export * from './paginated-response.d.ts';
|
|
54
55
|
export * from './request-pagination.d.ts';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IChipsItem } from './chips-item';
|
|
2
|
+
import type { IVideoCard } from './video-card';
|
|
3
|
+
|
|
4
|
+
/** Счётчик в строке под тегами: «17», «128.4K followers», «Updated 04.06.2026». */
|
|
5
|
+
export interface ILibraryFeedMeta {
|
|
6
|
+
icon?: string
|
|
7
|
+
text: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Блок одной подписки в «Библиотеке». Модели, каналы и категории отличаются только
|
|
12
|
+
* составом шапки, поэтому страница приводит их к этой форме, а вёрстка одна.
|
|
13
|
+
*/
|
|
14
|
+
export interface ILibraryFeedItem {
|
|
15
|
+
guid: string
|
|
16
|
+
title: string
|
|
17
|
+
/** Ссылка на саму сущность */
|
|
18
|
+
to: string
|
|
19
|
+
/** Нет — вместо картинки рисуются инициалы */
|
|
20
|
+
avatarUrl?: string
|
|
21
|
+
/** Только у моделей */
|
|
22
|
+
country?: {
|
|
23
|
+
name: string
|
|
24
|
+
title: string
|
|
25
|
+
}
|
|
26
|
+
tags?: IChipsItem[]
|
|
27
|
+
/** Роликов с прошлого захода: плашка «N new» и порядок в сортировке «сначала новые». 0 — плашки нет */
|
|
28
|
+
newCount: number
|
|
29
|
+
meta: ILibraryFeedMeta[]
|
|
30
|
+
videosCount: number
|
|
31
|
+
/** Подпись над лентой: «Latest» / «Top this week». Нет — лента без подписи */
|
|
32
|
+
feedTitle?: string
|
|
33
|
+
videos: IVideoCard[]
|
|
34
|
+
}
|