itube-specs 0.0.957 → 0.0.959
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.
|
@@ -868,6 +868,8 @@ $bg-fade-90: $color-zinc-950-90; // почти непрозрачный
|
|
|
868
868
|
// ─── Model card ───
|
|
869
869
|
$model-card-bg: $color-zinc-900-50;
|
|
870
870
|
$model-card-bg--hover: $color-zinc-800-50;
|
|
871
|
+
$model-card-thumb-bg: $color-zinc-800;
|
|
872
|
+
$model-card-separator: $color-zinc-600;
|
|
871
873
|
$model-card-border: $color-zinc-800-50;
|
|
872
874
|
$model-card-border--hover: $color-zinc-700-50;
|
|
873
875
|
$model-card-params: $color-zinc-500;
|
|
@@ -894,6 +894,8 @@ $bg-fade-90: $color-gray-950-90; // почти непрозрачный
|
|
|
894
894
|
// ─── Model card ───
|
|
895
895
|
$model-card-bg: $color-gray-100;
|
|
896
896
|
$model-card-bg--hover: $model-card-bg;
|
|
897
|
+
$model-card-thumb-bg: $color-gray-200;
|
|
898
|
+
$model-card-separator: $color-gray-400;
|
|
897
899
|
$model-card-border: transparent;
|
|
898
900
|
$model-card-border--hover: $model-card-border;
|
|
899
901
|
$model-card-params: $color-gray-600;
|
|
@@ -1,82 +1,83 @@
|
|
|
1
|
-
<!-- Карточка модели:
|
|
1
|
+
<!-- Карточка модели: портрет 3:4, имя со страной, параметры и счётчики видео/обновлений -->
|
|
2
2
|
<template>
|
|
3
3
|
<NuxtLink
|
|
4
4
|
:to="generateLink(`/models/${convertString().toSlug(card.title)}`)"
|
|
5
5
|
:title="card.title"
|
|
6
6
|
class="card-model"
|
|
7
7
|
>
|
|
8
|
-
<div class="card-
|
|
8
|
+
<div class="card-model__thumb">
|
|
9
9
|
<UiImg
|
|
10
10
|
:src="currentImage"
|
|
11
11
|
:loading="loading"
|
|
12
12
|
:alt="card.title"
|
|
13
13
|
:priority="priority"
|
|
14
|
+
:sizes="`(max-width: ${CSS_BREAKPOINTS.xs}px) 50vw, 240px`"
|
|
14
15
|
class="card-model__img"
|
|
15
|
-
width="
|
|
16
|
-
height="
|
|
17
|
-
sizes="xs:64px sm:64px md:64px lg:64px"
|
|
16
|
+
width="240"
|
|
17
|
+
height="320"
|
|
18
18
|
@error="onImageError"
|
|
19
19
|
/>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</span>
|
|
38
|
-
</p>
|
|
39
|
-
<p class="card-model__parameters">
|
|
40
|
-
<span
|
|
41
|
-
v-for="(parameter, index) in card.parameters"
|
|
42
|
-
:key="`parameter-${index}`"
|
|
43
|
-
class="card-model__parameter"
|
|
44
|
-
>
|
|
45
|
-
<span
|
|
46
|
-
v-if="parameter.title"
|
|
47
|
-
class="card-model__parameter-title"
|
|
48
|
-
>{{ getValue(parameter) }}</span>
|
|
49
|
-
</span>
|
|
50
|
-
</p>
|
|
51
|
-
<p class="card-model__parameters-footer">
|
|
52
|
-
<span
|
|
53
|
-
v-if="card.videosCount"
|
|
54
|
-
class="card-model__count"
|
|
20
|
+
</div>
|
|
21
|
+
<div class="card-model__wrapper">
|
|
22
|
+
<p class="card-model__info">
|
|
23
|
+
<span class="card-model__info-name">
|
|
24
|
+
{{ card.title }}
|
|
25
|
+
</span>
|
|
26
|
+
<span
|
|
27
|
+
v-if="card.country?.name"
|
|
28
|
+
:title="`${t('birthplace_country')}: ${card.country.title}`"
|
|
29
|
+
class="card-model__country-wrapper"
|
|
30
|
+
>
|
|
31
|
+
<img
|
|
32
|
+
:alt="card.country.title"
|
|
33
|
+
:src="`/icons/flags/${card.country.name}.svg`"
|
|
34
|
+
class="card-model__info-country"
|
|
35
|
+
width="16"
|
|
36
|
+
height="12"
|
|
55
37
|
>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
</span>
|
|
39
|
+
</p>
|
|
40
|
+
<p class="card-model__parameters">
|
|
41
|
+
<span
|
|
42
|
+
v-for="(parameter, index) in visibleParameters"
|
|
43
|
+
:key="`parameter-${index}`"
|
|
44
|
+
class="card-model__parameter"
|
|
45
|
+
>
|
|
62
46
|
<span
|
|
63
|
-
v-if="
|
|
64
|
-
class="card-
|
|
65
|
-
>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
47
|
+
v-if="parameter.title"
|
|
48
|
+
class="card-model__parameter-title"
|
|
49
|
+
>{{ getValue(parameter) }}</span>
|
|
50
|
+
</span>
|
|
51
|
+
<span
|
|
52
|
+
v-if="card.videosCount"
|
|
53
|
+
class="card-model__count"
|
|
54
|
+
>
|
|
55
|
+
<UiIcon
|
|
56
|
+
name="video-camera"
|
|
57
|
+
size="14"
|
|
58
|
+
/>
|
|
59
|
+
{{ $n(card.videosCount, 'compact') }}
|
|
60
|
+
</span>
|
|
61
|
+
</p>
|
|
62
|
+
<p
|
|
63
|
+
v-if="card.lastVideoAddedAt"
|
|
64
|
+
class="card-model__update"
|
|
65
|
+
>
|
|
66
|
+
<UiIcon
|
|
67
|
+
name="time-updated"
|
|
68
|
+
size="12"
|
|
69
|
+
/>
|
|
70
|
+
<span class="card-model__update-text">
|
|
71
|
+
{{ t('library.updated_at', { date: formatDate(card.lastVideoAddedAt) }) }}
|
|
72
|
+
</span>
|
|
73
|
+
</p>
|
|
74
74
|
</div>
|
|
75
75
|
</NuxtLink>
|
|
76
76
|
</template>
|
|
77
77
|
|
|
78
78
|
<script setup lang="ts">
|
|
79
79
|
import { convertString, formatDate } from '../../runtime';
|
|
80
|
+
import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
|
|
80
81
|
import type { IModelCard } from '../../types';
|
|
81
82
|
|
|
82
83
|
const props = withDefaults(defineProps<{
|
|
@@ -102,6 +103,16 @@ function onImageError() {
|
|
|
102
103
|
imageError.value = true;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/** В строку помещается только первый параметр из списка (тип парня) и возраст — остальные скрыты. */
|
|
107
|
+
const visibleParameters = computed(() => {
|
|
108
|
+
const isAge = (item: IModelCard['parameters'][number]) => item.title === 'age';
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
props.card.parameters.find(item => !isAge(item)),
|
|
112
|
+
props.card.parameters.find(isAge),
|
|
113
|
+
].filter(item => item !== undefined);
|
|
114
|
+
});
|
|
115
|
+
|
|
105
116
|
function getValue(item: { title?: string; value?: string }) {
|
|
106
117
|
if (item.title === 'age') {
|
|
107
118
|
return `${item.value} ${t('yo')}`;
|
|
@@ -112,11 +123,12 @@ function getValue(item: { title?: string; value?: string }) {
|
|
|
112
123
|
|
|
113
124
|
<style lang="scss">
|
|
114
125
|
.card-model {
|
|
115
|
-
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
116
128
|
background-color: $model-card-bg;
|
|
117
|
-
border-radius: $radius-
|
|
129
|
+
border-radius: $radius-md;
|
|
118
130
|
border: 1px solid $model-card-border;
|
|
119
|
-
|
|
131
|
+
overflow: hidden;
|
|
120
132
|
|
|
121
133
|
@include hover(border-color, background-color, box-shadow) {
|
|
122
134
|
border-color: $model-card-border--hover;
|
|
@@ -125,52 +137,48 @@ function getValue(item: { title?: string; value?: string }) {
|
|
|
125
137
|
}
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
.card-
|
|
129
|
-
|
|
140
|
+
.card-model__thumb {
|
|
141
|
+
aspect-ratio: 3/4;
|
|
142
|
+
background-color: $model-card-thumb-bg;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
}
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
.card-model__img {
|
|
147
|
+
width: 100%;
|
|
148
|
+
height: 100%;
|
|
149
|
+
object-fit: cover;
|
|
150
|
+
object-position: center top;
|
|
135
151
|
|
|
136
152
|
@include from-br(sm) {
|
|
137
|
-
|
|
138
|
-
column-gap: 12px;
|
|
153
|
+
transition: var(--transition) transform;
|
|
139
154
|
}
|
|
140
155
|
}
|
|
141
156
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
display: flex;
|
|
147
|
-
flex-wrap: nowrap;
|
|
148
|
-
align-items: center;
|
|
157
|
+
@media (hover: hover) {
|
|
158
|
+
.card-model:is(:hover, :focus-visible) .card-model__img {
|
|
159
|
+
transform: scale(1.05);
|
|
160
|
+
}
|
|
149
161
|
}
|
|
150
162
|
|
|
151
|
-
.card-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
display: flex;
|
|
155
|
-
flex-wrap: nowrap;
|
|
156
|
-
align-items: center;
|
|
157
|
-
color: $model-card-params-footer;
|
|
158
|
-
margin-top: 2px;
|
|
159
|
-
column-gap: 12px;
|
|
163
|
+
.card-model__wrapper {
|
|
164
|
+
padding: 8px;
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
.card-
|
|
167
|
+
.card-model__info {
|
|
163
168
|
display: grid;
|
|
164
|
-
grid-template-columns:
|
|
165
|
-
column-gap:
|
|
169
|
+
grid-template-columns: minmax(0, auto) auto;
|
|
170
|
+
column-gap: 6px;
|
|
171
|
+
justify-content: start;
|
|
166
172
|
align-items: center;
|
|
173
|
+
margin-bottom: 2px;
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
.card-model__info-name {
|
|
177
|
+
@include font-sm;
|
|
178
|
+
|
|
170
179
|
font-weight: 500;
|
|
171
180
|
color: $model-card-name;
|
|
172
181
|
white-space: nowrap;
|
|
173
|
-
max-width: 100%;
|
|
174
182
|
text-overflow: ellipsis;
|
|
175
183
|
overflow: hidden;
|
|
176
184
|
text-transform: capitalize;
|
|
@@ -186,50 +194,64 @@ function getValue(item: { title?: string; value?: string }) {
|
|
|
186
194
|
}
|
|
187
195
|
}
|
|
188
196
|
|
|
189
|
-
.card-
|
|
190
|
-
display:
|
|
191
|
-
column-gap: 8px;
|
|
192
|
-
margin-bottom: 4px;
|
|
193
|
-
grid-template-columns: auto auto auto;
|
|
194
|
-
justify-content: start;
|
|
195
|
-
align-items: center;
|
|
197
|
+
.card-model__country-wrapper {
|
|
198
|
+
display: flex;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
.card-
|
|
201
|
+
.card-model__info-country {
|
|
202
|
+
border-radius: 2px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.card-model__parameters {
|
|
199
206
|
@include font-sm;
|
|
200
207
|
|
|
201
208
|
display: flex;
|
|
209
|
+
flex-wrap: nowrap;
|
|
202
210
|
align-items: center;
|
|
203
|
-
|
|
204
|
-
color: $model-card-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.card-model__img {
|
|
208
|
-
width: var(--img-size);
|
|
209
|
-
height: var(--img-size);
|
|
210
|
-
border-radius: $radius-xl;
|
|
211
|
-
object-fit: cover;
|
|
212
|
-
object-position: center 70%;
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
color: $model-card-params;
|
|
213
|
+
margin-bottom: 2px;
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
.card-model__parameter:not(:last-child) {
|
|
216
|
-
position: relative;
|
|
217
217
|
display: grid;
|
|
218
|
-
grid-template-columns: 1fr auto;
|
|
218
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
219
219
|
column-gap: 6px;
|
|
220
220
|
padding-right: 6px;
|
|
221
221
|
|
|
222
222
|
&::after {
|
|
223
223
|
content: '·';
|
|
224
|
+
color: $model-card-separator;
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
.card-model__parameter {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
.card-model__parameter-title {
|
|
229
|
+
white-space: nowrap;
|
|
230
|
+
text-overflow: ellipsis;
|
|
231
|
+
overflow: hidden;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
.card-
|
|
233
|
-
|
|
234
|
+
.card-model__count {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
column-gap: 4px;
|
|
238
|
+
flex-shrink: 0;
|
|
239
|
+
color: $model-card-count;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.card-model__update {
|
|
243
|
+
@include font-sm;
|
|
244
|
+
|
|
245
|
+
display: grid;
|
|
246
|
+
grid-template-columns: 12px minmax(0, 1fr);
|
|
247
|
+
column-gap: 4px;
|
|
248
|
+
align-items: center;
|
|
249
|
+
color: $model-card-params-footer;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.card-model__update-text {
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
text-overflow: ellipsis;
|
|
255
|
+
overflow: hidden;
|
|
234
256
|
}
|
|
235
257
|
</style>
|
|
@@ -26,15 +26,27 @@ defineProps<{
|
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
28
|
<style lang="scss">
|
|
29
|
+
/* Лестница колонок как в макете: 2 → 3 (640px) → 4 (768px) → 5 (1024px) → 6 (1280px).
|
|
30
|
+
Пороги — литералы макета, с тирами $brkpnts намеренно не совпадают. */
|
|
29
31
|
.grid-models {
|
|
30
|
-
--min-width: 280px;
|
|
31
|
-
|
|
32
32
|
display: grid;
|
|
33
|
-
grid-template-columns: repeat(
|
|
33
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
34
34
|
gap: 12px;
|
|
35
35
|
|
|
36
|
-
@include from-br(
|
|
37
|
-
|
|
36
|
+
@include from-br(640px) {
|
|
37
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@include from-br(768px) {
|
|
41
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@include from-br(1024px) {
|
|
45
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@include from-br(1280px) {
|
|
49
|
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
52
|
</style>
|
|
@@ -164,18 +164,36 @@ function closeLanguage() {
|
|
|
164
164
|
|
|
165
165
|
/* Поиск живёт в своей колонке (свободный 1fr между лого и правым блоком), а не оверлеем
|
|
166
166
|
поверх всей строки: так он сжимается вместе с ней и на него ничто не наезжает */
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Поиск стоит строго по центру ШАПКИ, а не по центру свободного места: он растянут
|
|
170
|
+
* на всю строку сетки и центрирован `justify-self`. Иначе центр уезжал влево —
|
|
171
|
+
* правая группа (ниша, колокольчик, язык, вход) шире логотипа, и с добавлением
|
|
172
|
+
* переключателя ниш перекос дошёл до сотни пикселей.
|
|
173
|
+
*
|
|
174
|
+
* Наезжать при этом не на что: по обе стороны резервируется `--header-side-space`
|
|
175
|
+
* — ширина более широкой (правой) группы: ниша 54 + колокольчик, язык и вход.
|
|
176
|
+
* Считается БЕЗ бокового паддинга шапки, потому что `100%` у поиска — это уже
|
|
177
|
+
* контентная ширина. Не хватило места — поиск сужается, а боковые группы остаются
|
|
178
|
+
* на своих местах. Значение вынесено в переменную: у сайта без переключателя ниш
|
|
179
|
+
* правая группа уже, и он может его уменьшить.
|
|
180
|
+
*/
|
|
167
181
|
.layout-header__search {
|
|
182
|
+
--header-side-space: 358px;
|
|
183
|
+
|
|
168
184
|
@include from-br($brkpnt-burger) {
|
|
169
|
-
grid-column:
|
|
185
|
+
grid-column: 1 / -1;
|
|
170
186
|
grid-row: 1;
|
|
187
|
+
justify-self: center;
|
|
171
188
|
width: 100%;
|
|
172
189
|
min-width: 0;
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
|
|
191
|
+
// 24px — два column-gap шапки по бокам от поиска
|
|
192
|
+
max-width: min($search-max-width, calc(100% - 2 * var(--header-side-space) - 24px));
|
|
175
193
|
}
|
|
176
194
|
|
|
177
195
|
@include from-br(md) {
|
|
178
|
-
max-width: $search-max-width-md;
|
|
196
|
+
max-width: min($search-max-width-md, calc(100% - 2 * var(--header-side-space) - 24px));
|
|
179
197
|
}
|
|
180
198
|
}
|
|
181
199
|
|
package/package.json
CHANGED
package/pages/models/index.vue
CHANGED
|
@@ -46,11 +46,14 @@ const testModelImages = [
|
|
|
46
46
|
'/img/models/Sebastian _Kane.png',
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/** Портреты 3:4 — только под карточку модели, круглым аватарам топа они не подходят. */
|
|
50
|
+
const testModelPortraits = Array.from({ length: 16 }, (_, index) => `/img/models/portrait-${index + 1}.png`);
|
|
51
|
+
|
|
52
|
+
function applyTestImages<T extends { primaryImageUrl?: string; thumbUrl?: string }>(items: T[] | undefined, images: string[]): T[] | undefined {
|
|
50
53
|
return items?.map((item, i) => ({
|
|
51
54
|
...item,
|
|
52
|
-
primaryImageUrl:
|
|
53
|
-
thumbUrl:
|
|
55
|
+
primaryImageUrl: images[i % images.length],
|
|
56
|
+
thumbUrl: images[i % images.length],
|
|
54
57
|
}));
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -63,10 +66,10 @@ const [
|
|
|
63
66
|
]);
|
|
64
67
|
|
|
65
68
|
if (topModels.value?.items) {
|
|
66
|
-
topModels.value.items = applyTestImages(topModels.value.items)!;
|
|
69
|
+
topModels.value.items = applyTestImages(topModels.value.items, testModelImages)!;
|
|
67
70
|
}
|
|
68
71
|
if (models.value?.items) {
|
|
69
|
-
models.value.items = applyTestImages(models.value.items)!;
|
|
72
|
+
models.value.items = applyTestImages(models.value.items, testModelPortraits)!;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
const { meta, h1 } = useMeta({
|