itube-specs 0.0.1020 → 0.0.1021
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/SITE_UPDATES.md +12 -0
- package/assets/scss/base/_normalize.scss +3 -6
- package/assets/scss/themes/dark/_theme.scss +9 -5
- package/assets/scss/themes/light/_theme.scss +7 -3
- package/components/card/webcam/index.vue +2 -4
- package/components/info/grid.vue +19 -19
- package/components/info/model.vue +289 -180
- package/components/info/socials.vue +1 -17
- package/components/view/model-videos.vue +1 -7
- package/components/view/webcams.vue +1 -2
- package/i18n/locales/en-US.json +1 -0
- package/package.json +1 -1
- package/utils/info-grid/__tests__/model-filter-link.test.ts +16 -0
- package/utils/info-grid/model-filter-link.ts +17 -0
package/SITE_UPDATES.md
CHANGED
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
|
|
12
12
|
кому именно (всем / сайтам с N), что будет, если не сделать.
|
|
13
13
|
|
|
14
|
+
## 0.0.1021 — 2026-10-08
|
|
15
|
+
|
|
16
|
+
- **Сайтам со своей темой** (`assets/scss/vars/_theme.scss` в сайте, как у hdsex-remaster): завести токены
|
|
17
|
+
нового инфо-блока модели — как в `themes/dark/_theme.scss` слоя: `$info-model-about-title`,
|
|
18
|
+
`$info-model-about-text`, `$info-model-summary-bg`, `$info-model-summary-bg--hover`, `$info-model-summary-text`,
|
|
19
|
+
`$info-model-summary-icon`, `$info-grid-item-value`. Удалены (можно убрать): `$info-model-border`,
|
|
20
|
+
`$info-model-bg`, `$info-model-footer-border`. По макету: `$info-model-description` → `$color-zinc-300`,
|
|
21
|
+
`$info-grid-item-title` → `$color-zinc-450`. Не завести — сборка падает на undefined variable.
|
|
22
|
+
- **Сайтам с форком `components/view/model-videos.vue`:** убрать `<LazyUiFullDescr>` с `fullDescription` внизу
|
|
23
|
+
страницы — длинное описание теперь показывает `InfoModel` в блоке «About». Не убрать — текст на странице
|
|
24
|
+
задублируется.
|
|
25
|
+
|
|
14
26
|
## 0.0.1019 — 2026-10-08
|
|
15
27
|
|
|
16
28
|
- **Сайтам со своей темой** (`assets/scss/vars/_theme.scss` в сайте, как у hdsex-remaster): завести токены
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
box-sizing: border-box;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
ul[class],
|
|
8
|
-
ol[class] {
|
|
7
|
+
:where(ul[class], ol[class]) {
|
|
9
8
|
padding: 0;
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -15,8 +14,7 @@ h2,
|
|
|
15
14
|
h3,
|
|
16
15
|
h4,
|
|
17
16
|
p,
|
|
18
|
-
ul[class],
|
|
19
|
-
ol[class],
|
|
17
|
+
:where(ul[class], ol[class]),
|
|
20
18
|
li,
|
|
21
19
|
figure,
|
|
22
20
|
figcaption,
|
|
@@ -33,8 +31,7 @@ body {
|
|
|
33
31
|
line-height: 1.5;
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
ul[class],
|
|
37
|
-
ol[class] {
|
|
34
|
+
:where(ul[class], ol[class]) {
|
|
38
35
|
list-style: none;
|
|
39
36
|
}
|
|
40
37
|
|
|
@@ -814,8 +814,9 @@ $filters-main-bg--hover: $color-zinc-700-70;
|
|
|
814
814
|
$info-grid-group-title: $color-zinc-300;
|
|
815
815
|
$info-grid-item-border: $color-zinc-700-50;
|
|
816
816
|
$info-grid-item-bg--hover: $color-zinc-700-30;
|
|
817
|
-
$info-grid-item-title: $color-zinc-
|
|
817
|
+
$info-grid-item-title: $color-zinc-450;
|
|
818
818
|
$info-grid-item-title-bg: $color-zinc-900;
|
|
819
|
+
$info-grid-item-value: $color-zinc-300;
|
|
819
820
|
$info-grid-item-value-bg: $color-zinc-900;
|
|
820
821
|
$info-grid-item-value-bg--link: $color-zinc-700-50;
|
|
821
822
|
$info-grid-item-value-link-bg--hover: $color-zinc-700-50;
|
|
@@ -832,11 +833,14 @@ $info-img-border: $color-zinc-700;
|
|
|
832
833
|
$info-description: $color-zinc-400;
|
|
833
834
|
|
|
834
835
|
// ─── Info model ───
|
|
835
|
-
$info-model-border: $color-zinc-800-50;
|
|
836
|
-
$info-model-bg: $color-zinc-900-50;
|
|
837
836
|
$info-model-img-bg: $color-zinc-800;
|
|
838
|
-
$info-model-description: $color-zinc-
|
|
839
|
-
$info-model-
|
|
837
|
+
$info-model-description: $color-zinc-300;
|
|
838
|
+
$info-model-about-title: $color-zinc-300;
|
|
839
|
+
$info-model-about-text: $color-zinc-400;
|
|
840
|
+
$info-model-summary-bg: $color-zinc-700-50;
|
|
841
|
+
$info-model-summary-bg--hover: $color-zinc-700;
|
|
842
|
+
$info-model-summary-text: $color-white;
|
|
843
|
+
$info-model-summary-icon: $color-zinc-400;
|
|
840
844
|
$info-model-more-text: $color-zinc-500;
|
|
841
845
|
$info-model-more-text--hover: $color-zinc-300;
|
|
842
846
|
$info-model-also: $color-zinc-400;
|
|
@@ -843,6 +843,7 @@ $info-grid-item-border: $color-gray-300;
|
|
|
843
843
|
$info-grid-item-bg--hover: $color-slate-700;
|
|
844
844
|
$info-grid-item-title: $color-gray-500;
|
|
845
845
|
$info-grid-item-title-bg: $color-white;
|
|
846
|
+
$info-grid-item-value: $color-gray-900;
|
|
846
847
|
$info-grid-item-value-bg: $color-gray-100;
|
|
847
848
|
$info-grid-item-value-bg--link: $color-slate-700;
|
|
848
849
|
$info-grid-item-value-link-bg--hover: $color-slate-750;
|
|
@@ -859,11 +860,14 @@ $info-img-border: transparent;
|
|
|
859
860
|
$info-description: $color-gray-600;
|
|
860
861
|
|
|
861
862
|
// ─── Info model ───
|
|
862
|
-
$info-model-border: transparent;
|
|
863
|
-
$info-model-bg: $color-gray-200;
|
|
864
863
|
$info-model-img-bg: $color-gray-300;
|
|
865
864
|
$info-model-description: $color-gray-600;
|
|
866
|
-
$info-model-
|
|
865
|
+
$info-model-about-title: $color-gray-600;
|
|
866
|
+
$info-model-about-text: $color-gray-600;
|
|
867
|
+
$info-model-summary-bg: $color-slate-700;
|
|
868
|
+
$info-model-summary-bg--hover: $color-slate-750;
|
|
869
|
+
$info-model-summary-text: $color-white;
|
|
870
|
+
$info-model-summary-icon: $color-gray-400;
|
|
867
871
|
$info-model-more-text: $color-gray-600;
|
|
868
872
|
$info-model-more-text--hover: $color-gray-600;
|
|
869
873
|
$info-model-also: $color-gray-600;
|
|
@@ -677,9 +677,7 @@ onBeforeUnmount(() => {
|
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
|
|
681
|
-
// одиночного класса ему не хватает
|
|
682
|
-
.card-webcam > .card-webcam__tags {
|
|
680
|
+
.card-webcam__tags {
|
|
683
681
|
display: flex;
|
|
684
682
|
flex-wrap: wrap;
|
|
685
683
|
gap: 6px;
|
|
@@ -688,7 +686,7 @@ onBeforeUnmount(() => {
|
|
|
688
686
|
|
|
689
687
|
// на узких экранах теги в один ряд с горизонтальным скроллом: в две колонки они
|
|
690
688
|
// не влезают, а перенос делал карточки в сетке разной высоты
|
|
691
|
-
.card-
|
|
689
|
+
.card-webcam__tags {
|
|
692
690
|
@include to-br(xm) {
|
|
693
691
|
flex-wrap: nowrap;
|
|
694
692
|
overflow-x: auto;
|
package/components/info/grid.vue
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
:is="item.isFilter && value ? NuxtLink : 'div'"
|
|
27
27
|
:key="`s-info-grid-value-${gIndex}-${index}-${vIndex}`"
|
|
28
28
|
:class="{'--link': item.isFilter && value}"
|
|
29
|
-
:to="item.isFilter && value ? generateLink(
|
|
29
|
+
:to="item.isFilter && value ? generateLink(modelFilterLink(item, value.name)) : undefined"
|
|
30
30
|
class="info-grid__item"
|
|
31
31
|
>
|
|
32
32
|
<span
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
</template>
|
|
68
68
|
|
|
69
69
|
<script setup lang="ts">
|
|
70
|
-
import type { IGroupedParameter
|
|
70
|
+
import type { IGroupedParameter } from '../../types';
|
|
71
|
+
import { modelFilterLink } from '../../utils/info-grid/model-filter-link';
|
|
71
72
|
|
|
72
73
|
defineProps<{
|
|
73
74
|
groups: IGroupedParameter[]
|
|
@@ -84,16 +85,6 @@ const NuxtLink = resolveComponent('NuxtLink');
|
|
|
84
85
|
const { hiddenByUnits } = useUnits();
|
|
85
86
|
const { generateLink } = useGenerateLink();
|
|
86
87
|
|
|
87
|
-
function link(item: IGroupedParameterItem, value: string) {
|
|
88
|
-
const formattedValue = value.toLowerCase().replace(/\s+/g, '+');
|
|
89
|
-
|
|
90
|
-
if (item.kind === 'range') {
|
|
91
|
-
return `/models?filter_${item.name}_from=${formattedValue}&filter_${item.name}_to=${formattedValue}`;
|
|
92
|
-
} else {
|
|
93
|
-
return `/models?filter_${item.name}=${formattedValue}`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
88
|
function isSuccess(value: string) {
|
|
98
89
|
const values = ['active'];
|
|
99
90
|
return values.includes(value);
|
|
@@ -108,7 +99,7 @@ function isWarning(value: string) {
|
|
|
108
99
|
<style lang="scss">
|
|
109
100
|
.info-grid {
|
|
110
101
|
display: grid;
|
|
111
|
-
row-gap:
|
|
102
|
+
row-gap: 20px;
|
|
112
103
|
}
|
|
113
104
|
|
|
114
105
|
.info-grid__group-title {
|
|
@@ -116,9 +107,9 @@ function isWarning(value: string) {
|
|
|
116
107
|
|
|
117
108
|
margin-bottom: 12px;
|
|
118
109
|
color: $info-grid-group-title;
|
|
119
|
-
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
letter-spacing: var(--tracking-wide);
|
|
120
112
|
text-transform: uppercase;
|
|
121
|
-
font-weight: 500;
|
|
122
113
|
}
|
|
123
114
|
|
|
124
115
|
.info-grid__group-items {
|
|
@@ -133,14 +124,16 @@ function isWarning(value: string) {
|
|
|
133
124
|
|
|
134
125
|
display: flex;
|
|
135
126
|
flex-wrap: nowrap;
|
|
136
|
-
|
|
127
|
+
height: 36px;
|
|
137
128
|
border-radius: $radius-lg;
|
|
138
129
|
border: 1px solid $info-grid-item-border;
|
|
139
130
|
overflow: hidden;
|
|
140
131
|
}
|
|
141
132
|
|
|
142
133
|
.info-grid__item-title {
|
|
143
|
-
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
padding-inline: 10px;
|
|
144
137
|
color: $info-grid-item-title;
|
|
145
138
|
background-color: $info-grid-item-title-bg;
|
|
146
139
|
}
|
|
@@ -150,7 +143,7 @@ function isWarning(value: string) {
|
|
|
150
143
|
flex-wrap: nowrap;
|
|
151
144
|
align-items: center;
|
|
152
145
|
column-gap: 4px;
|
|
153
|
-
padding:
|
|
146
|
+
padding-inline: 10px;
|
|
154
147
|
}
|
|
155
148
|
|
|
156
149
|
.info-grid__item.--link {
|
|
@@ -175,10 +168,14 @@ function isWarning(value: string) {
|
|
|
175
168
|
}
|
|
176
169
|
|
|
177
170
|
.info-grid__item:not(.--link) .info-grid__item-value {
|
|
178
|
-
|
|
171
|
+
color: $info-grid-item-value;
|
|
179
172
|
background-color: $info-grid-item-value-bg;
|
|
180
173
|
}
|
|
181
174
|
|
|
175
|
+
.info-grid__item:not(.--link) .info-grid__item-title + .info-grid__item-value {
|
|
176
|
+
border-left: 1px solid $info-grid-item-value-border;
|
|
177
|
+
}
|
|
178
|
+
|
|
182
179
|
.info-grid__item:not(.--link) .info-grid__item-title {
|
|
183
180
|
background-color: $info-grid-item-value-bg;
|
|
184
181
|
}
|
|
@@ -203,6 +200,9 @@ function isWarning(value: string) {
|
|
|
203
200
|
.info-grid__plain-text {
|
|
204
201
|
@include font-sm;
|
|
205
202
|
|
|
203
|
+
max-width: 740px;
|
|
204
|
+
margin-block: -6px;
|
|
206
205
|
color: $info-grid-plain-text;
|
|
206
|
+
line-height: 1.625;
|
|
207
207
|
}
|
|
208
208
|
</style>
|
|
@@ -1,131 +1,164 @@
|
|
|
1
|
-
<!--
|
|
1
|
+
<!-- Инфо-блок модели: портрет 3:4, псевдонимы, соцсети, шапка с действиями, рождение, описание, сводка-чипсы; «About» и группы параметров — по «Show more» -->
|
|
2
2
|
<template>
|
|
3
3
|
<section class="info-model">
|
|
4
|
-
<div class="info-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
4
|
+
<div class="info-model__aside">
|
|
5
|
+
<UiImg
|
|
6
|
+
:src="currentImage"
|
|
7
|
+
:avif-src="currentAvifSrc"
|
|
8
|
+
:alt="card.title"
|
|
9
|
+
:sizes="IMG_SIZES"
|
|
10
|
+
class="info-model__img"
|
|
11
|
+
width="240"
|
|
12
|
+
height="320"
|
|
13
|
+
priority
|
|
14
|
+
@error="imageError = true"
|
|
15
|
+
/>
|
|
16
|
+
<p
|
|
17
|
+
v-if="akaNames.length"
|
|
18
|
+
class="info-model__also"
|
|
19
|
+
>
|
|
20
|
+
<span class="info-model__also-title">{{ t('also_known_as') }}</span>
|
|
21
|
+
<template
|
|
22
|
+
v-for="name in akaNames"
|
|
23
|
+
:key="name"
|
|
20
24
|
>
|
|
21
|
-
<span class="info-model__also-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
{{ ' ' }}<span class="info-model__also-name">{{ name }}</span>
|
|
26
|
+
</template>
|
|
27
|
+
</p>
|
|
28
|
+
<InfoSocials
|
|
29
|
+
v-if="socialItems.length"
|
|
30
|
+
:items="socialItems"
|
|
31
|
+
:known-icons="KNOWN_ICONS"
|
|
32
|
+
class="info-model__social"
|
|
33
|
+
icon-size="20"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
<header class="info-model__header">
|
|
38
|
+
<div class="info-model__heading">
|
|
34
39
|
<h1 class="info-model__title">{{ card.title }}</h1>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
size="16"
|
|
51
|
-
/>
|
|
52
|
-
<span class="info-model__share-text">{{ t('share') }}</span>
|
|
53
|
-
</UiBtn>
|
|
54
|
-
<UiFollowBtn
|
|
55
|
-
v-if="featureFlags.FeatureFollowEnabled"
|
|
56
|
-
:following="isFollowing"
|
|
57
|
-
:name="card.title"
|
|
58
|
-
class="info-model__follow"
|
|
59
|
-
size="s"
|
|
60
|
-
@toggle="onFollowClick"
|
|
40
|
+
<UiCount
|
|
41
|
+
class="info-model__count"
|
|
42
|
+
icon="video-camera"
|
|
43
|
+
>{{ $n(videosCount || card.videosCount, 'compact') }}</UiCount>
|
|
44
|
+
<UiBtn
|
|
45
|
+
:aria-label="t('share')"
|
|
46
|
+
:title="t('share')"
|
|
47
|
+
theme="muted"
|
|
48
|
+
size="s"
|
|
49
|
+
class="info-model__share"
|
|
50
|
+
@click="isShareOpen = true"
|
|
51
|
+
>
|
|
52
|
+
<UiIcon
|
|
53
|
+
name="share"
|
|
54
|
+
size="16"
|
|
61
55
|
/>
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
<span class="info-model__share-text">{{ t('share') }}</span>
|
|
57
|
+
</UiBtn>
|
|
58
|
+
</div>
|
|
59
|
+
<UiFollowBtn
|
|
60
|
+
v-if="featureFlags.FeatureFollowEnabled"
|
|
61
|
+
:following="isFollowing"
|
|
62
|
+
:name="card.title"
|
|
63
|
+
class="info-model__follow"
|
|
64
|
+
size="s"
|
|
65
|
+
@toggle="onFollowClick"
|
|
66
|
+
/>
|
|
67
|
+
</header>
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
<p
|
|
70
|
+
v-if="card.birthSummary"
|
|
71
|
+
class="info-model__born"
|
|
72
|
+
>
|
|
73
|
+
<UiIcon
|
|
74
|
+
class="info-model__born-icon"
|
|
75
|
+
name="geo"
|
|
76
|
+
size="14"
|
|
77
|
+
/>
|
|
78
|
+
<span class="info-model__born-text">{{ card.birthSummary }}</span>
|
|
79
|
+
<!-- Национальность нигде больше не подписана — флаг единственный её носитель,
|
|
80
|
+
поэтому в `alt` идёт название страны, а не ISO-код. -->
|
|
81
|
+
<img
|
|
82
|
+
v-for="(item, index) in card.nationalityIsoCodes"
|
|
83
|
+
:key="`nationalityIsoCodes-${index}`"
|
|
84
|
+
:src="`/icons/flags/${item}.svg`"
|
|
85
|
+
:alt="isoCodeToCountryName(item, locale)"
|
|
86
|
+
class="info-model__born-flag"
|
|
87
|
+
width="20"
|
|
88
|
+
height="14"
|
|
68
89
|
>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
<p
|
|
93
|
+
v-if="card.description"
|
|
94
|
+
class="info-model__description"
|
|
95
|
+
>{{ card.description }}</p>
|
|
96
|
+
|
|
97
|
+
<ul
|
|
98
|
+
v-if="summaryItems.length"
|
|
99
|
+
class="info-model__summary"
|
|
100
|
+
>
|
|
101
|
+
<li
|
|
102
|
+
v-for="item in summaryItems"
|
|
103
|
+
:key="item.key"
|
|
104
|
+
>
|
|
105
|
+
<component
|
|
106
|
+
:is="item.to ? NuxtLink : 'span'"
|
|
107
|
+
:class="{'--link': item.to}"
|
|
108
|
+
:to="item.to"
|
|
109
|
+
class="info-model__summary-item"
|
|
85
110
|
>
|
|
86
|
-
|
|
111
|
+
<UiIcon
|
|
112
|
+
v-if="item.icon"
|
|
113
|
+
:name="item.icon"
|
|
114
|
+
class="info-model__summary-icon"
|
|
115
|
+
prefix="models"
|
|
116
|
+
size="16"
|
|
117
|
+
/>
|
|
118
|
+
{{ item.title }}
|
|
119
|
+
</component>
|
|
120
|
+
</li>
|
|
121
|
+
</ul>
|
|
87
122
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
<div
|
|
124
|
+
v-if="hasMore"
|
|
125
|
+
:class="{'--collapsed': !isExpanded, '--about': card.fullDescription}"
|
|
126
|
+
class="info-model__details"
|
|
127
|
+
>
|
|
128
|
+
<div
|
|
129
|
+
v-if="card.fullDescription"
|
|
130
|
+
class="info-model__about"
|
|
131
|
+
>
|
|
132
|
+
<h3 class="info-model__about-title">{{ t('model_about') }}</h3>
|
|
133
|
+
<p
|
|
134
|
+
:class="{'_truncate': !isExpanded}"
|
|
135
|
+
class="info-model__about-text"
|
|
136
|
+
>{{ card.fullDescription }}</p>
|
|
137
|
+
</div>
|
|
93
138
|
|
|
94
139
|
<InfoGrid
|
|
95
|
-
v-if="
|
|
96
|
-
:groups="
|
|
140
|
+
v-if="groupedParameters.length"
|
|
141
|
+
:groups="groupedParameters"
|
|
97
142
|
:iso-codes="card.nationalityIsoCodes"
|
|
98
143
|
class="info-model__parameters"
|
|
99
144
|
/>
|
|
100
|
-
|
|
101
|
-
<InfoGrid
|
|
102
|
-
v-if="extraGroups.length > 0"
|
|
103
|
-
:class="{'--collapsed': !isExpanded}"
|
|
104
|
-
:groups="extraGroups"
|
|
105
|
-
:iso-codes="card.nationalityIsoCodes"
|
|
106
|
-
class="info-model__parameters-extra"
|
|
107
|
-
/>
|
|
108
145
|
</div>
|
|
109
146
|
|
|
110
|
-
<
|
|
147
|
+
<button
|
|
111
148
|
v-if="hasMore"
|
|
112
|
-
|
|
149
|
+
:aria-expanded="isExpanded"
|
|
150
|
+
class="info-model__more"
|
|
151
|
+
type="button"
|
|
152
|
+
@click="isExpanded = !isExpanded"
|
|
113
153
|
>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class="
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
:class="{'--expanded': isExpanded}"
|
|
123
|
-
class="info-model__more-icon"
|
|
124
|
-
name="angle-down"
|
|
125
|
-
size="12"
|
|
126
|
-
/>
|
|
127
|
-
</button>
|
|
128
|
-
</footer>
|
|
154
|
+
{{ isExpanded ? t('show_less') : t('show_more') }}
|
|
155
|
+
<UiIcon
|
|
156
|
+
:class="{'--expanded': isExpanded}"
|
|
157
|
+
class="info-model__more-icon"
|
|
158
|
+
name="angle-down"
|
|
159
|
+
size="16"
|
|
160
|
+
/>
|
|
161
|
+
</button>
|
|
129
162
|
|
|
130
163
|
<ShareMain v-model="isShareOpen">
|
|
131
164
|
<template #card>
|
|
@@ -141,6 +174,7 @@ import type { ICardInfo, IGroupedParameter, ILinkItem, IModelCard } from '../../
|
|
|
141
174
|
import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
|
|
142
175
|
import { AuthorizationApiService } from '../../services/api/authorization.service';
|
|
143
176
|
import { groupParametersByGroup } from '../../utils/info-main/group-parameters-by-group';
|
|
177
|
+
import { modelFilterLink } from '../../utils/info-grid/model-filter-link';
|
|
144
178
|
import { isoCodeToCountryName } from '../../utils/iso-code-to-country-name';
|
|
145
179
|
|
|
146
180
|
const props = defineProps<{
|
|
@@ -152,9 +186,14 @@ const { t, locale } = useI18n();
|
|
|
152
186
|
|
|
153
187
|
const featureFlags = useRuntimeConfig().public.featureFlags;
|
|
154
188
|
const placeholderOf = useModelPlaceholder();
|
|
189
|
+
const { generateLink } = useGenerateLink();
|
|
190
|
+
const NuxtLink = resolveComponent('NuxtLink');
|
|
155
191
|
|
|
156
192
|
const KNOWN_ICONS = ['facebook', 'instagram', 'tiktok', 'twitter', 'x', 'youtube', 'snapchat'];
|
|
157
193
|
|
|
194
|
+
/** Параметры сводки под описанием, в порядке показа. В группах ниже они не повторяются. */
|
|
195
|
+
const SUMMARY_NAMES = ['guy_type', 'body_build', 'ethnicity', 'role', 'age'];
|
|
196
|
+
|
|
158
197
|
const placeholder = computed(() => placeholderOf(props.card.gender, 'portrait'));
|
|
159
198
|
|
|
160
199
|
/** `.info-model__img` — 160px до sm, 240px дальше. `auto` впереди — см. `UiImg`. */
|
|
@@ -165,6 +204,8 @@ const isFallback = computed(() => imageError.value || !props.card.portraitImg);
|
|
|
165
204
|
const currentImage = computed(() => (isFallback.value ? placeholder.value.webp : props.card.portraitImg));
|
|
166
205
|
const currentAvifSrc = computed(() => (isFallback.value ? placeholder.value.avif : props.card.portraitAvifUrl));
|
|
167
206
|
|
|
207
|
+
const akaNames = computed(() => props.card.akaNames.split(', ').filter(Boolean));
|
|
208
|
+
|
|
168
209
|
const socialItems = computed<ILinkItem[]>(() => (
|
|
169
210
|
Object.entries(props.card.social || {}).map(([key, value]) => ({
|
|
170
211
|
name: value || '',
|
|
@@ -173,20 +214,27 @@ const socialItems = computed<ILinkItem[]>(() => (
|
|
|
173
214
|
}))
|
|
174
215
|
));
|
|
175
216
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
/** Первая группа видна всегда; вторая свёрнутой видна только с sm, остальные — после «Show more». */
|
|
181
|
-
const mainGroups = computed(() => groupedParameters.value.slice(0, 1));
|
|
217
|
+
const summaryItems = computed(() => SUMMARY_NAMES.flatMap((name) => {
|
|
218
|
+
const param = props.card.parameters?.find(p => p.name === name);
|
|
219
|
+
if (!param) return [];
|
|
182
220
|
|
|
183
|
-
|
|
221
|
+
return param.values.map(value => ({
|
|
222
|
+
key: `${name}-${value.name}`,
|
|
223
|
+
title: name === 'age' ? `${value.title} ${t('yo')}` : value.title,
|
|
224
|
+
icon: name === 'age' ? undefined : param.icon,
|
|
225
|
+
to: param.is_filter ? generateLink(modelFilterLink(param, value.name)) : undefined,
|
|
226
|
+
}));
|
|
227
|
+
}));
|
|
184
228
|
|
|
185
|
-
|
|
186
|
-
|
|
229
|
+
/** Страна уже есть в строке рождения флагом — отдельным чипсом её не повторяем. */
|
|
230
|
+
const groupedParameters = computed<IGroupedParameter[]>(() => groupParametersByGroup(
|
|
231
|
+
(props.card.parameters ?? []).filter(p => !SUMMARY_NAMES.includes(p.name)
|
|
232
|
+
&& !(p.name === 'nationality_iso_codes' && props.card.birthSummary)),
|
|
187
233
|
));
|
|
188
234
|
|
|
189
|
-
const
|
|
235
|
+
const isExpanded = ref(false);
|
|
236
|
+
|
|
237
|
+
const hasMore = computed(() => groupedParameters.value.length > 0 || Boolean(props.card.fullDescription));
|
|
190
238
|
|
|
191
239
|
const isShareOpen = ref(false);
|
|
192
240
|
|
|
@@ -236,34 +284,30 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
236
284
|
|
|
237
285
|
<style lang="scss">
|
|
238
286
|
.info-model {
|
|
239
|
-
border: 1px solid $info-model-border;
|
|
240
|
-
border-radius: $radius-xl;
|
|
241
|
-
background-color: $info-model-bg;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.info-model__content {
|
|
245
287
|
display: grid;
|
|
246
288
|
grid-template-columns: 160px minmax(0, 1fr);
|
|
247
|
-
grid-template-rows: auto 1fr;
|
|
289
|
+
grid-template-rows: auto auto auto auto auto 1fr auto;
|
|
248
290
|
grid-template-areas:
|
|
249
291
|
'aside header'
|
|
250
|
-
'aside
|
|
251
|
-
'
|
|
252
|
-
'
|
|
253
|
-
'
|
|
292
|
+
'aside born'
|
|
293
|
+
'aside description'
|
|
294
|
+
'aside summary'
|
|
295
|
+
'aside more'
|
|
296
|
+
'aside .'
|
|
297
|
+
'details details';
|
|
254
298
|
column-gap: 16px;
|
|
255
|
-
padding: 16px;
|
|
256
299
|
|
|
257
300
|
@include from-br(sm) {
|
|
258
301
|
grid-template-columns: 240px minmax(0, 1fr);
|
|
259
|
-
grid-template-rows: auto auto auto auto 1fr;
|
|
302
|
+
grid-template-rows: auto auto auto auto auto auto 1fr;
|
|
260
303
|
grid-template-areas:
|
|
261
304
|
'aside header'
|
|
262
305
|
'aside born'
|
|
263
306
|
'aside description'
|
|
264
|
-
'aside
|
|
265
|
-
'aside
|
|
266
|
-
|
|
307
|
+
'aside summary'
|
|
308
|
+
'aside details'
|
|
309
|
+
'aside more'
|
|
310
|
+
'aside .';
|
|
267
311
|
}
|
|
268
312
|
}
|
|
269
313
|
|
|
@@ -287,7 +331,7 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
287
331
|
@include font-xs;
|
|
288
332
|
|
|
289
333
|
margin-top: 8px;
|
|
290
|
-
color: $info-model-also;
|
|
334
|
+
color: $info-model-also-title;
|
|
291
335
|
line-height: 1.625;
|
|
292
336
|
|
|
293
337
|
@include from-br(sm) {
|
|
@@ -297,8 +341,13 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
297
341
|
}
|
|
298
342
|
}
|
|
299
343
|
|
|
300
|
-
.info-model__also-
|
|
301
|
-
color: $info-model-also
|
|
344
|
+
.info-model__also-name {
|
|
345
|
+
color: $info-model-also;
|
|
346
|
+
white-space: nowrap;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.info-model__also-name:not(:last-of-type)::after {
|
|
350
|
+
content: ',';
|
|
302
351
|
}
|
|
303
352
|
|
|
304
353
|
.info-model__social {
|
|
@@ -312,21 +361,36 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
312
361
|
.info-model__header {
|
|
313
362
|
display: grid;
|
|
314
363
|
grid-area: header;
|
|
315
|
-
|
|
316
|
-
|
|
364
|
+
grid-template-columns: auto auto minmax(0, 1fr);
|
|
365
|
+
align-items: center;
|
|
366
|
+
gap: 8px;
|
|
367
|
+
|
|
368
|
+
@include from-br(sm) {
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: flex-start;
|
|
371
|
+
justify-content: space-between;
|
|
372
|
+
column-gap: 12px;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.info-model__heading {
|
|
377
|
+
display: contents;
|
|
317
378
|
|
|
318
379
|
@include from-br(sm) {
|
|
319
380
|
display: flex;
|
|
320
381
|
flex-wrap: wrap;
|
|
321
382
|
align-items: center;
|
|
322
|
-
|
|
383
|
+
gap: 12px;
|
|
384
|
+
min-width: 0;
|
|
323
385
|
}
|
|
324
386
|
}
|
|
325
387
|
|
|
326
388
|
.info-model__title {
|
|
327
389
|
@include font-base;
|
|
328
390
|
|
|
391
|
+
grid-column: 1 / -1;
|
|
329
392
|
min-width: 0;
|
|
393
|
+
margin-top: -2px;
|
|
330
394
|
font-weight: 700;
|
|
331
395
|
line-height: 1.25;
|
|
332
396
|
text-transform: uppercase;
|
|
@@ -334,27 +398,22 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
334
398
|
|
|
335
399
|
@include from-br(sm) {
|
|
336
400
|
@include font-lg;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
.info-model__actions {
|
|
341
|
-
display: flex;
|
|
342
|
-
align-items: center;
|
|
343
|
-
gap: 8px;
|
|
344
401
|
|
|
345
|
-
|
|
346
|
-
display: contents;
|
|
402
|
+
margin-top: 0;
|
|
347
403
|
}
|
|
348
404
|
}
|
|
349
405
|
|
|
350
406
|
.info-model__count {
|
|
351
|
-
|
|
407
|
+
min-height: 36px;
|
|
408
|
+
padding: 8px 12px;
|
|
352
409
|
flex-shrink: 0;
|
|
353
410
|
}
|
|
354
411
|
|
|
355
412
|
.info-model__share {
|
|
356
413
|
@include button-variant(info-model-button);
|
|
357
414
|
|
|
415
|
+
min-height: 36px;
|
|
416
|
+
|
|
358
417
|
@include to-br(sm) {
|
|
359
418
|
width: 36px;
|
|
360
419
|
padding: 0;
|
|
@@ -368,19 +427,28 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
368
427
|
}
|
|
369
428
|
|
|
370
429
|
.info-model__follow {
|
|
371
|
-
|
|
430
|
+
justify-self: end;
|
|
431
|
+
|
|
432
|
+
@include to-br(sm) {
|
|
433
|
+
width: 36px;
|
|
434
|
+
height: 36px;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
@include from-br(sm) {
|
|
438
|
+
min-height: 36px;
|
|
439
|
+
}
|
|
372
440
|
}
|
|
373
441
|
|
|
374
442
|
.info-model__born {
|
|
375
443
|
@include font-sm;
|
|
376
444
|
|
|
377
445
|
grid-area: born;
|
|
378
|
-
margin-top:
|
|
446
|
+
margin-top: 12px;
|
|
379
447
|
color: $info-model-born;
|
|
448
|
+
line-height: 1.625;
|
|
380
449
|
|
|
381
450
|
@include from-br(sm) {
|
|
382
|
-
margin-top:
|
|
383
|
-
margin-bottom: 12px;
|
|
451
|
+
margin-top: 8px;
|
|
384
452
|
}
|
|
385
453
|
}
|
|
386
454
|
|
|
@@ -404,56 +472,93 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
404
472
|
.info-model__description {
|
|
405
473
|
@include font-sm;
|
|
406
474
|
|
|
407
|
-
--line-clamp: 2;
|
|
408
|
-
|
|
409
475
|
grid-area: description;
|
|
410
|
-
|
|
476
|
+
max-width: 740px;
|
|
477
|
+
margin-top: 8px;
|
|
478
|
+
margin-bottom: -6px;
|
|
411
479
|
line-height: 1.625;
|
|
412
480
|
color: $info-model-description;
|
|
481
|
+
text-wrap: pretty;
|
|
413
482
|
|
|
414
483
|
@include from-br(sm) {
|
|
415
|
-
margin-top:
|
|
484
|
+
margin-top: 12px;
|
|
416
485
|
}
|
|
417
486
|
}
|
|
418
487
|
|
|
419
|
-
.info-
|
|
420
|
-
|
|
421
|
-
|
|
488
|
+
.info-model__summary {
|
|
489
|
+
display: flex;
|
|
490
|
+
flex-wrap: wrap;
|
|
491
|
+
align-items: center;
|
|
492
|
+
grid-area: summary;
|
|
493
|
+
gap: 8px;
|
|
422
494
|
margin-top: 16px;
|
|
495
|
+
}
|
|
423
496
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
497
|
+
.info-model__summary-item {
|
|
498
|
+
@include font-sm;
|
|
499
|
+
|
|
500
|
+
display: flex;
|
|
501
|
+
align-items: center;
|
|
502
|
+
column-gap: 4px;
|
|
503
|
+
height: 36px;
|
|
504
|
+
padding-inline: 10px;
|
|
505
|
+
border-radius: $radius-lg;
|
|
506
|
+
background-color: $info-model-summary-bg;
|
|
507
|
+
color: $info-model-summary-text;
|
|
427
508
|
}
|
|
428
509
|
|
|
429
|
-
.info-
|
|
430
|
-
@include
|
|
431
|
-
|
|
510
|
+
.info-model__summary-item.--link {
|
|
511
|
+
@include hover(background-color) {
|
|
512
|
+
background-color: $info-model-summary-bg--hover;
|
|
432
513
|
}
|
|
433
514
|
}
|
|
434
515
|
|
|
435
|
-
.info-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
516
|
+
.info-model__summary-icon {
|
|
517
|
+
flex-shrink: 0;
|
|
518
|
+
color: $info-model-summary-icon;
|
|
439
519
|
}
|
|
440
520
|
|
|
441
|
-
.info-
|
|
442
|
-
|
|
521
|
+
.info-model__details {
|
|
522
|
+
display: grid;
|
|
523
|
+
grid-area: details;
|
|
524
|
+
row-gap: 20px;
|
|
443
525
|
margin-top: 20px;
|
|
444
526
|
}
|
|
445
527
|
|
|
446
|
-
.info-
|
|
528
|
+
.info-model__details.--collapsed {
|
|
447
529
|
@include to-br(sm) {
|
|
448
530
|
display: none;
|
|
449
531
|
}
|
|
450
532
|
}
|
|
451
533
|
|
|
452
|
-
.info-
|
|
453
|
-
display:
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
534
|
+
.info-model__details.--collapsed:not(.--about) {
|
|
535
|
+
display: none;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.info-model__details.--collapsed .info-model__parameters {
|
|
539
|
+
display: none;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.info-model__about-title {
|
|
543
|
+
@include font-xs;
|
|
544
|
+
|
|
545
|
+
margin-bottom: 12px;
|
|
546
|
+
color: $info-model-about-title;
|
|
547
|
+
font-weight: 600;
|
|
548
|
+
letter-spacing: var(--tracking-wide);
|
|
549
|
+
text-transform: uppercase;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.info-model__about-text {
|
|
553
|
+
@include font-sm;
|
|
554
|
+
|
|
555
|
+
--line-clamp: 3;
|
|
556
|
+
|
|
557
|
+
max-width: 740px;
|
|
558
|
+
margin-block: -6px;
|
|
559
|
+
line-height: 1.625;
|
|
560
|
+
color: $info-model-about-text;
|
|
561
|
+
text-wrap: pretty;
|
|
457
562
|
}
|
|
458
563
|
|
|
459
564
|
.info-model__more {
|
|
@@ -461,7 +566,11 @@ const shareModelCard = computed<IModelCard>(() => ({
|
|
|
461
566
|
|
|
462
567
|
display: flex;
|
|
463
568
|
align-items: center;
|
|
569
|
+
justify-self: start;
|
|
570
|
+
grid-area: more;
|
|
464
571
|
column-gap: 6px;
|
|
572
|
+
margin-top: 16px;
|
|
573
|
+
margin-bottom: -6px;
|
|
465
574
|
color: $info-model-more-text;
|
|
466
575
|
|
|
467
576
|
@include hover(color) {
|
|
@@ -39,15 +39,10 @@ withDefaults(
|
|
|
39
39
|
gap: 8px;
|
|
40
40
|
flex-wrap: wrap;
|
|
41
41
|
align-items: start;
|
|
42
|
-
|
|
43
|
-
@include from-br(sm) {
|
|
44
|
-
margin-inline: 0;
|
|
45
|
-
gap: 12px;
|
|
46
|
-
}
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
.info-socials__item {
|
|
50
|
-
--size:
|
|
45
|
+
--size: 44px;
|
|
51
46
|
|
|
52
47
|
display: grid;
|
|
53
48
|
place-content: center;
|
|
@@ -58,19 +53,8 @@ withDefaults(
|
|
|
58
53
|
border-radius: $radius-full;
|
|
59
54
|
color: $info-socials-icon;
|
|
60
55
|
|
|
61
|
-
@include from-br(sm) {
|
|
62
|
-
--size: 28px;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
56
|
@include hover(background-color) {
|
|
66
57
|
background-color: $info-socials-bg--hover;
|
|
67
58
|
}
|
|
68
59
|
}
|
|
69
|
-
|
|
70
|
-
.info-socials__item img {
|
|
71
|
-
@include from-br(sm) {
|
|
72
|
-
width: 14px;
|
|
73
|
-
height: 14px;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
60
|
</style>
|
|
@@ -93,12 +93,6 @@
|
|
|
93
93
|
class="view-model-videos__other-models"
|
|
94
94
|
hydrate-on-visible
|
|
95
95
|
/>
|
|
96
|
-
|
|
97
|
-
<LazyUiFullDescr
|
|
98
|
-
v-if="isFirstPage && cardInfo?.fullDescription && !isFilteredPage"
|
|
99
|
-
:text="cardInfo.fullDescription"
|
|
100
|
-
hydrate-on-visible
|
|
101
|
-
/>
|
|
102
96
|
</div>
|
|
103
97
|
</template>
|
|
104
98
|
|
|
@@ -243,7 +237,7 @@ const infoVideosCount = computed(() => (props.dataOther && props.dataOther.items
|
|
|
243
237
|
|
|
244
238
|
<style lang="scss">
|
|
245
239
|
.view-model-videos__info {
|
|
246
|
-
margin-bottom:
|
|
240
|
+
margin-bottom: 32px;
|
|
247
241
|
}
|
|
248
242
|
|
|
249
243
|
.view-model-videos__top-chips {
|
|
@@ -404,8 +404,7 @@ watch(() => JSON.stringify(toWebcamFilterPayload(route.query)), () => {
|
|
|
404
404
|
margin-top: 0;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
.
|
|
408
|
-
// ресет обнуляет отступы у `ul[class]`, одиночный класс его не перебивает
|
|
407
|
+
.view-webcams__grid {
|
|
409
408
|
margin-bottom: 40px;
|
|
410
409
|
}
|
|
411
410
|
</style>
|
package/i18n/locales/en-US.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { modelFilterLink } from '../model-filter-link';
|
|
3
|
+
|
|
4
|
+
describe('modelFilterLink', () => {
|
|
5
|
+
it('builds a single filter for a select', () => {
|
|
6
|
+
expect(modelFilterLink({ name: 'hair_color', kind: 'select' }, 'brown')).toBe('/models?filter_hair_color=brown');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('puts a range value into both _from and _to', () => {
|
|
10
|
+
expect(modelFilterLink({ name: 'age', kind: 'range' }, '27')).toBe('/models?filter_age_from=27&filter_age_to=27');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('lowercases and joins words with +', () => {
|
|
14
|
+
expect(modelFilterLink({ name: 'interested_in', kind: 'radio' }, 'Guys And Girls')).toBe('/models?filter_interested_in=guys+and+girls');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IGroupedParameterItem } from '../../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ссылка на каталог моделей, отфильтрованный по значению параметра. У диапазона
|
|
5
|
+
* значение уходит сразу в `_from` и `_to`.
|
|
6
|
+
* @param item - параметр: `name` — ключ фильтра, `kind` — тип
|
|
7
|
+
* @param value - `name` значения параметра
|
|
8
|
+
*/
|
|
9
|
+
export function modelFilterLink(item: Pick<IGroupedParameterItem, 'name' | 'kind'>, value: string) {
|
|
10
|
+
const formattedValue = value.toLowerCase().replace(/\s+/g, '+');
|
|
11
|
+
|
|
12
|
+
if (item.kind === 'range') {
|
|
13
|
+
return `/models?filter_${item.name}_from=${formattedValue}&filter_${item.name}_to=${formattedValue}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `/models?filter_${item.name}=${formattedValue}`;
|
|
17
|
+
}
|