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.
@@ -1,307 +0,0 @@
1
- <!-- Блок подписки на модель: шапка с аватаром, тегами и счётчиками + лента последних роликов -->
2
- <template>
3
- <article class="library-model">
4
- <header class="library-model__header">
5
- <NuxtLink
6
- :to="link"
7
- :title="model.title"
8
- class="library-model__avatar-link"
9
- >
10
- <UiImg
11
- :src="currentImage"
12
- :alt="model.title"
13
- :loading="priority ? 'eager' : 'lazy'"
14
- :priority="priority"
15
- class="library-model__avatar"
16
- width="96"
17
- height="96"
18
- sizes="96px"
19
- @error="imageError = true"
20
- />
21
- </NuxtLink>
22
-
23
- <div class="library-model__info">
24
- <div class="library-model__heading">
25
- <NuxtLink
26
- :to="link"
27
- class="library-model__name"
28
- >
29
- {{ model.title }}
30
- <img
31
- v-if="model.country"
32
- :alt="model.country.title"
33
- :title="`${t('birthplace_country')}: ${model.country.title}`"
34
- :src="`/icons/flags/${model.country.name}.svg`"
35
- class="library-model__flag"
36
- width="20"
37
- height="14"
38
- >
39
- </NuxtLink>
40
-
41
- <span
42
- v-if="model.newCount"
43
- class="library-model__badge"
44
- >{{ t('library.new_count', { count: model.newCount }) }}</span>
45
- </div>
46
-
47
- <p
48
- v-if="model.tags.length"
49
- class="library-model__tags"
50
- >{{ model.tags.join(' · ') }}</p>
51
-
52
- <p class="library-model__meta">
53
- <span class="library-model__meta-item">
54
- <UiIcon
55
- name="video-camera"
56
- size="14"
57
- />
58
- <span class="library-model__meta-videos">{{ $n(model.videosCount, 'compact') }}</span>
59
- {{ model.age }} {{ t('yo') }}
60
- </span>
61
-
62
- <span class="library-model__meta-item">
63
- <UiIcon
64
- name="date"
65
- size="14"
66
- />
67
- {{ t('library.updated_at', { date: formatDate(model.lastVideoAddedAt) }) }}
68
- </span>
69
- </p>
70
- </div>
71
-
72
- <div class="library-model__actions _from-sm">
73
- <UiBtn
74
- :theme="following ? 'secondary' : 'primary'"
75
- class="library-model__follow"
76
- @click="emit('toggle-follow', model)"
77
- >
78
- <UiIcon
79
- :name="following ? 'check' : 'plus'"
80
- size="16"
81
- />
82
- {{ following ? t('library.following_state') : t('follow') }}
83
- </UiBtn>
84
-
85
- <UiLink
86
- :to="link"
87
- theme="secondary"
88
- >
89
- {{ t('plural.videos', model.videosCount) }}
90
- <UiIcon
91
- name="arrow-right"
92
- size="16"
93
- />
94
- </UiLink>
95
- </div>
96
- </header>
97
-
98
- <div class="library-model__grid">
99
- <CardVideo
100
- v-for="(video, index) in model.videos"
101
- :key="video.guid"
102
- :card="video"
103
- :loading="priority && index < 3 ? 'eager' : 'lazy'"
104
- :priority="priority && index < 3"
105
- compact
106
- />
107
- </div>
108
-
109
- <UiLink
110
- :to="link"
111
- theme="secondary"
112
- class="library-model__all _to-sm"
113
- wide
114
- >
115
- {{ t('plural.videos', model.videosCount) }}
116
- <UiIcon
117
- name="arrow-right"
118
- size="16"
119
- />
120
- </UiLink>
121
- </article>
122
- </template>
123
-
124
- <script setup lang="ts">
125
- import { convertString, formatDate } from '../../runtime';
126
- import type { LibraryModelStub } from '../../stubs/library-models';
127
-
128
- const props = defineProps<{
129
- model: LibraryModelStub
130
- following?: boolean
131
- priority?: boolean
132
- }>();
133
-
134
- const emit = defineEmits<{
135
- (eventName: 'toggle-follow', model: LibraryModelStub): void,
136
- }>();
137
-
138
- const { t } = useI18n();
139
- const { generateLink } = useGenerateLink();
140
-
141
- const link = computed(() => generateLink(`/models/${convertString().toSlug(props.model.title)}`));
142
-
143
- const fallbackImage = '/img/model-placeholder.webp';
144
- const imageError = ref(false);
145
-
146
- const currentImage = computed(() => imageError.value ? fallbackImage : props.model.primaryImageUrl);
147
- </script>
148
-
149
- <style lang="scss">
150
- .library-model {
151
- padding: 16px;
152
- background-color: $library-model-bg;
153
- border: 1px solid $library-model-border;
154
- border-radius: $radius-xl;
155
-
156
- @include from-br(sm) {
157
- padding: 20px;
158
- }
159
- }
160
-
161
- .library-model__header {
162
- display: grid;
163
- grid-template-columns: auto 1fr;
164
- column-gap: 16px;
165
- row-gap: 12px;
166
- align-items: start;
167
- margin-bottom: 20px;
168
-
169
- @include from-br(sm) {
170
- grid-template-columns: auto 1fr auto;
171
- }
172
- }
173
-
174
- .library-model__avatar {
175
- width: 80px;
176
- height: 80px;
177
- object-fit: cover;
178
- object-position: center 30%;
179
- border-radius: $radius-full;
180
- border: 2px solid $library-model-avatar-border;
181
-
182
- @include from-br(sm) {
183
- width: 96px;
184
- height: 96px;
185
- transition: var(--transition) border-color;
186
- }
187
- }
188
-
189
- @include from-br(sm) {
190
- .library-model__avatar-link:is(:hover, :focus-visible) .library-model__avatar {
191
- border-color: $library-model-avatar-border--hover;
192
- }
193
- }
194
-
195
- .library-model__info {
196
- min-width: 0;
197
- }
198
-
199
- .library-model__heading {
200
- display: flex;
201
- flex-wrap: wrap;
202
- align-items: center;
203
- column-gap: 10px;
204
- row-gap: 6px;
205
- }
206
-
207
- .library-model__name {
208
- @include font-lg;
209
-
210
- display: flex;
211
- align-items: center;
212
- column-gap: 8px;
213
- font-weight: 700;
214
- text-transform: capitalize;
215
- color: $library-model-name;
216
-
217
- @include from-br(sm) {
218
- transition: var(--transition) color;
219
- }
220
-
221
- @include hover(color) {
222
- color: $library-model-name--hover;
223
- }
224
- }
225
-
226
- .library-model__flag {
227
- flex-shrink: 0;
228
- border-radius: 2px;
229
- }
230
-
231
- .library-model__badge {
232
- @include font-3xs;
233
-
234
- flex-shrink: 0;
235
- font-weight: 600;
236
- white-space: nowrap;
237
- padding-block: 4px;
238
- padding-inline: 10px;
239
- color: $library-model-badge-text;
240
- background-color: $library-model-badge-bg;
241
- border-radius: $radius-full;
242
- }
243
-
244
- .library-model__tags {
245
- @include font-sm;
246
-
247
- margin-top: 4px;
248
- color: $library-model-tags;
249
- }
250
-
251
- .library-model__meta {
252
- @include font-sm;
253
-
254
- display: flex;
255
- flex-wrap: wrap;
256
- align-items: center;
257
- column-gap: 16px;
258
- row-gap: 4px;
259
- margin-top: 2px;
260
- color: $library-model-meta;
261
- }
262
-
263
- .library-model__meta-item {
264
- display: flex;
265
- align-items: center;
266
- column-gap: 6px;
267
- }
268
-
269
- .library-model__meta-videos::after {
270
- content: '·';
271
- margin-inline-start: 6px;
272
- color: $library-model-meta-dot;
273
- }
274
-
275
- .library-model__actions {
276
- display: flex;
277
- align-items: center;
278
- column-gap: 8px;
279
- }
280
-
281
- .library-model__follow {
282
- min-width: 124px;
283
- }
284
-
285
- .library-model__grid {
286
- display: grid;
287
- gap: 16px;
288
- grid-template-columns: repeat(2, 1fr);
289
-
290
- @include from-br(xm) {
291
- grid-template-columns: repeat(3, 1fr);
292
- }
293
-
294
- @include from-br(sm) {
295
- gap: 24px 16px;
296
- grid-template-columns: repeat(4, 1fr);
297
- }
298
-
299
- @include from-br(md) {
300
- grid-template-columns: repeat(5, 1fr);
301
- }
302
- }
303
-
304
- .library-model__all {
305
- margin-top: 16px;
306
- }
307
- </style>
@@ -1,75 +0,0 @@
1
- import { VIDEO_POOL } from './videos';
2
- import type { StubVideo } from './videos';
3
-
4
- const HOUR = 3600;
5
- const DAY = 24 * HOUR;
6
-
7
- /** Ролик в ленте подписки: пул + флаг «новинки» и уникальный guid внутри модели. */
8
- export type LibraryModelVideo = StubVideo & {
9
- guid: string;
10
- isNew: boolean;
11
- };
12
-
13
- export type LibraryModelStub = {
14
- guid: string;
15
- title: string;
16
- primaryImageUrl: string;
17
- country: {
18
- name: string
19
- title: string
20
- };
21
- age: number;
22
- videosCount: number;
23
- /** Сколько роликов вышло с прошлого захода */
24
- newCount: number;
25
- /** Unix timestamp (сек.) последнего добавленного ролика */
26
- lastVideoAddedAt: number;
27
- tags: string[];
28
- videos: LibraryModelVideo[];
29
- };
30
-
31
- /** Сколько последних роликов модели показываем в ленте. */
32
- const PREVIEW_LIMIT = 5;
33
-
34
- /** Порядок обновлений намеренно не совпадает с алфавитным — иначе обе сортировки дают одно и то же. */
35
- const MODELS = [
36
- { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile'], updatedAgo: 6 * HOUR, videoOffset: 3 },
37
- { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top'], updatedAgo: 2 * DAY, videoOffset: 9 },
38
- { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom'], updatedAgo: 9 * DAY, videoOffset: 0 },
39
- { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom'], updatedAgo: 21 * DAY, videoOffset: 6 },
40
- ];
41
-
42
- /**
43
- * Подписки пользователя на моделей. Ручки под них пока нет — вёрстка живёт на заглушке.
44
- * Возраст ролика считаем от «сейчас», чтобы даты в разметке не устаревали.
45
- */
46
- export function getLibraryModelsStub(): LibraryModelStub[] {
47
- const now = Math.floor(Date.now() / 1000);
48
-
49
- return MODELS.map((model) => {
50
- const slug = model.title.toLowerCase().replace(/\s+/g, '-');
51
-
52
- const videos = Array.from({ length: PREVIEW_LIMIT }, (_, index) => {
53
- const video = VIDEO_POOL[(model.videoOffset + index) % VIDEO_POOL.length];
54
-
55
- return {
56
- ...video,
57
- guid: `${slug}-${video.id}`,
58
- isNew: index < model.newCount,
59
- };
60
- });
61
-
62
- return {
63
- guid: slug,
64
- title: model.title,
65
- primaryImageUrl: `/img/models/${model.image}`,
66
- country: model.country,
67
- age: model.age,
68
- videosCount: model.videosCount,
69
- newCount: model.newCount,
70
- lastVideoAddedAt: now - model.updatedAgo,
71
- tags: model.tags,
72
- videos,
73
- };
74
- });
75
- }