itube-specs 0.0.919 → 0.0.920
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 +30 -5
- package/assets/scss/themes/light/_theme.scss +30 -5
- package/assets/scss/vars/_palette.scss +3 -0
- package/components/card/webcam/index.vue +141 -15
- package/components/card/webcam/languages.vue +7 -3
- package/components/filter/model.vue +56 -1
- package/components/grid/webcams.vue +78 -0
- package/components/ui/switcher.vue +114 -0
- package/components/view/webcams.vue +370 -0
- package/components/webcam/header.vue +1 -43
- package/components/webcam/icon.vue +49 -0
- package/components/webcam/main.vue +8 -62
- package/components/webcam/stripcash-card.vue +2 -2
- package/composables/fetch/use-fetch-models-count.ts +20 -0
- package/composables/fetch/use-fetch-webcam-models.ts +45 -0
- package/lib/index.ts +1 -0
- package/lib/scheme/sort-webcams.ts +22 -0
- package/package.json +1 -1
- package/pages/webcams/[[tag]].vue +75 -0
- package/server/api/models/count.get.ts +18 -0
- package/server/api/webcams/list.post.ts +41 -0
- package/services/api/models.service.ts +20 -0
- package/services/api/webcams.service.ts +34 -0
- package/types/index.d.ts +3 -0
- package/types/switcher-item.d.ts +13 -0
- package/types/webcam-facet.d.ts +7 -0
- package/types/webcam-models-response.d.ts +12 -0
- package/utils/webcams/__tests__/count-filters.test.ts +12 -0
- package/utils/webcams/__tests__/normalize-facets.test.ts +52 -0
- package/utils/webcams/__tests__/to-filter-payload.test.ts +17 -0
- package/utils/webcams/count-filters.ts +14 -0
- package/utils/webcams/facets.ts +31 -0
- package/utils/webcams/normalize-facets.ts +57 -0
- package/utils/webcams/to-filter-payload.ts +16 -0
|
@@ -1059,8 +1059,8 @@ $document-link: $color-indigo-400;
|
|
|
1059
1059
|
$document-link--hover: $color-indigo-300;
|
|
1060
1060
|
|
|
1061
1061
|
// ─── Вебкам-блок ───
|
|
1062
|
-
$webcam-
|
|
1063
|
-
$webcam-
|
|
1062
|
+
$webcam-icon-text: $color-white;
|
|
1063
|
+
$webcam-icon-bg: $color-white-10;
|
|
1064
1064
|
$webcam-header-promo-text: $color-white;
|
|
1065
1065
|
$webcam-header-promo-bg: $color-indigo-600;
|
|
1066
1066
|
$webcam-header-promo-bg--hover: $color-indigo-500;
|
|
@@ -1072,12 +1072,15 @@ $webcam-counters-language-bg: $color-zinc-800-70;
|
|
|
1072
1072
|
$card-webcam-poster-bg: $color-zinc-800;
|
|
1073
1073
|
$card-webcam-online-dot: $color-emerald-400;
|
|
1074
1074
|
$card-webcam-online-ring: $color-black-42;
|
|
1075
|
-
$card-webcam-
|
|
1076
|
-
$card-webcam-
|
|
1077
|
-
$card-webcam-
|
|
1075
|
+
$card-webcam-badge-text: $color-white-90;
|
|
1076
|
+
$card-webcam-badge-bg: $color-black-45;
|
|
1077
|
+
$card-webcam-badge-border: $color-white-25;
|
|
1078
|
+
$card-webcam-badge-shadow: $color-black-90;
|
|
1078
1079
|
$card-webcam-goal-text: $color-white;
|
|
1079
1080
|
$card-webcam-goal-label: $color-zinc-400;
|
|
1080
1081
|
$card-webcam-goal-percent: $color-white-70;
|
|
1082
|
+
$card-webcam-goal-plain-text: $color-zinc-300;
|
|
1083
|
+
$card-webcam-goal-plain-percent: $color-zinc-400;
|
|
1081
1084
|
$card-webcam-goal-track-bg: $color-white-15;
|
|
1082
1085
|
$card-webcam-goal-fill-bg: $color-emerald-400;
|
|
1083
1086
|
$card-webcam-goal-shade-from: $color-black-95;
|
|
@@ -1090,6 +1093,28 @@ $card-webcam-tag-text: $color-zinc-300;
|
|
|
1090
1093
|
$card-webcam-tag-bg: $color-zinc-800;
|
|
1091
1094
|
$card-webcam-tag-text--hover: $color-white;
|
|
1092
1095
|
$card-webcam-tag-bg--hover: $color-zinc-700;
|
|
1096
|
+
$page-webcams-divider: $color-zinc-700;
|
|
1097
|
+
$page-webcams-control-bg: $color-zinc-800-30;
|
|
1098
|
+
$page-webcams-control-border: $color-zinc-700-30;
|
|
1099
|
+
$page-webcams-control-text: $color-white;
|
|
1100
|
+
|
|
1101
|
+
// ─── Switcher ───
|
|
1102
|
+
$switcher-bg: $color-zinc-900-80;
|
|
1103
|
+
$switcher-border: $color-zinc-800;
|
|
1104
|
+
$switcher-text: $color-zinc-400;
|
|
1105
|
+
$switcher-text--hover: $color-white;
|
|
1106
|
+
$switcher-text--active: $color-white;
|
|
1107
|
+
$switcher-bg--active: $color-indigo-600;
|
|
1108
|
+
$switcher-icon: $color-zinc-500;
|
|
1109
|
+
$switcher-icon--active: $color-emerald-400;
|
|
1110
|
+
$switcher-subtitle: $color-zinc-600;
|
|
1111
|
+
$switcher-subtitle--active: $color-white-60;
|
|
1112
|
+
|
|
1113
|
+
// ─── Filter: сброс всех фильтров в облаке чипсов ───
|
|
1114
|
+
$filter-model-clear-text: $color-zinc-500;
|
|
1115
|
+
$filter-model-clear-text--hover: $color-white;
|
|
1116
|
+
|
|
1117
|
+
// ─── Вебкам-карточка (продолжение) ───
|
|
1093
1118
|
$card-webcam-languages-border: $color-white-20;
|
|
1094
1119
|
$card-webcam-languages-fallback-bg: $color-zinc-800;
|
|
1095
1120
|
$card-webcam-languages-fallback-icon: $color-zinc-300;
|
|
@@ -1087,8 +1087,8 @@ $document-link: $color-indigo-400;
|
|
|
1087
1087
|
$document-link--hover: $color-indigo-300;
|
|
1088
1088
|
|
|
1089
1089
|
// ─── Вебкам-блок ───
|
|
1090
|
-
$webcam-
|
|
1091
|
-
$webcam-
|
|
1090
|
+
$webcam-icon-text: $color-gray-700;
|
|
1091
|
+
$webcam-icon-bg: $color-gray-200;
|
|
1092
1092
|
$webcam-header-promo-text: $color-white;
|
|
1093
1093
|
$webcam-header-promo-bg: $color-indigo-600;
|
|
1094
1094
|
$webcam-header-promo-bg--hover: $color-indigo-500;
|
|
@@ -1100,12 +1100,15 @@ $webcam-counters-language-bg: $color-gray-200;
|
|
|
1100
1100
|
$card-webcam-poster-bg: $color-gray-200;
|
|
1101
1101
|
$card-webcam-online-dot: $color-emerald-500;
|
|
1102
1102
|
$card-webcam-online-ring: $color-black-25;
|
|
1103
|
-
$card-webcam-
|
|
1104
|
-
$card-webcam-
|
|
1105
|
-
$card-webcam-
|
|
1103
|
+
$card-webcam-badge-text: $color-white;
|
|
1104
|
+
$card-webcam-badge-bg: $color-black-45;
|
|
1105
|
+
$card-webcam-badge-border: $color-white-25;
|
|
1106
|
+
$card-webcam-badge-shadow: $color-black-90;
|
|
1106
1107
|
$card-webcam-goal-text: $color-white;
|
|
1107
1108
|
$card-webcam-goal-label: $color-gray-300;
|
|
1108
1109
|
$card-webcam-goal-percent: $color-white-70;
|
|
1110
|
+
$card-webcam-goal-plain-text: $color-gray-700;
|
|
1111
|
+
$card-webcam-goal-plain-percent: $color-gray-500;
|
|
1109
1112
|
$card-webcam-goal-track-bg: $color-white-15;
|
|
1110
1113
|
$card-webcam-goal-fill-bg: $color-emerald-400;
|
|
1111
1114
|
$card-webcam-goal-shade-from: $color-black-95;
|
|
@@ -1118,6 +1121,28 @@ $card-webcam-tag-text: $color-gray-700;
|
|
|
1118
1121
|
$card-webcam-tag-bg: $color-gray-200;
|
|
1119
1122
|
$card-webcam-tag-text--hover: $color-gray-950;
|
|
1120
1123
|
$card-webcam-tag-bg--hover: $color-gray-300;
|
|
1124
|
+
$page-webcams-divider: $color-gray-300;
|
|
1125
|
+
$page-webcams-control-bg: $color-gray-100;
|
|
1126
|
+
$page-webcams-control-border: $color-gray-200;
|
|
1127
|
+
$page-webcams-control-text: $color-gray-950;
|
|
1128
|
+
|
|
1129
|
+
// ─── Switcher ───
|
|
1130
|
+
$switcher-bg: $color-gray-100;
|
|
1131
|
+
$switcher-border: $color-gray-200;
|
|
1132
|
+
$switcher-text: $color-gray-500;
|
|
1133
|
+
$switcher-text--hover: $color-gray-950;
|
|
1134
|
+
$switcher-text--active: $color-white;
|
|
1135
|
+
$switcher-bg--active: $color-indigo-600;
|
|
1136
|
+
$switcher-icon: $color-gray-500;
|
|
1137
|
+
$switcher-icon--active: $color-emerald-400;
|
|
1138
|
+
$switcher-subtitle: $color-gray-500;
|
|
1139
|
+
$switcher-subtitle--active: $color-white-60;
|
|
1140
|
+
|
|
1141
|
+
// ─── Filter: сброс всех фильтров в облаке чипсов ───
|
|
1142
|
+
$filter-model-clear-text: $color-gray-500;
|
|
1143
|
+
$filter-model-clear-text--hover: $color-gray-950;
|
|
1144
|
+
|
|
1145
|
+
// ─── Вебкам-карточка (продолжение) ───
|
|
1121
1146
|
$card-webcam-languages-border: $color-gray-300;
|
|
1122
1147
|
$card-webcam-languages-fallback-bg: $color-gray-200;
|
|
1123
1148
|
$card-webcam-languages-fallback-icon: $color-gray-600;
|
|
@@ -5,6 +5,7 @@ $color-zinc-950-0: lab(2.51107% .242703 -.886115 / 0%); // zinc-950 / 0%
|
|
|
5
5
|
$color-zinc-950-75: lab(2.51107% .242703 -.886115 / 75%); // zinc-950 / 75%
|
|
6
6
|
$color-zinc-950-90: lab(2.51107% .242703 -.886115 / 90%); // zinc-950 / 90%
|
|
7
7
|
$color-zinc-900: lab(8.30603% .618205 -2.16572); // #18181b
|
|
8
|
+
$color-zinc-900-80: lab(8.30603% .618205 -2.16572 / 80%);
|
|
8
9
|
$color-zinc-900-70: lab(8.30603% .618205 -2.16572 / 70%); // zinc-900 / 70%
|
|
9
10
|
$color-zinc-900-60: lab(8.30603% .618205 -2.16572 / 60%); // zinc-900 / 60%
|
|
10
11
|
$color-zinc-900-50: lab(8.30603% .618205 -2.16572 / 50%); // #18181b
|
|
@@ -59,6 +60,7 @@ $color-indigo-500-20: lab(48.295% 38.3129 -81.9673 / 20%);
|
|
|
59
60
|
$color-indigo-500-40: lab(48.295% 38.3129 -81.9673 / 40%);
|
|
60
61
|
$color-indigo-500-50: lab(48.295% 38.3129 -81.9673 / 50%);
|
|
61
62
|
$color-indigo-600: lab(38.4009% 52.6132 -92.3857); // #4f39f6
|
|
63
|
+
$color-indigo-600-10: lab(38.4009% 52.6132 -92.3857 / 10%);
|
|
62
64
|
$color-indigo-600-20: lab(38.4009% 52.6132 -92.3857 / 20%);
|
|
63
65
|
$color-indigo-600-90: lab(38.4009% 52.6132 -92.3857 / 90%);
|
|
64
66
|
$color-indigo-700: lab(32.4486% 49.2217 -84.6695); // #432dd7
|
|
@@ -99,6 +101,7 @@ $mono-white-5: lab(100% 0 0 / 5%);
|
|
|
99
101
|
|
|
100
102
|
// $color- duplicates of the mono-black/mono-white opacity scale
|
|
101
103
|
$color-black-100: lab(0% 0 0);
|
|
104
|
+
$color-black-90: lab(0% 0 0 / 90%);
|
|
102
105
|
$color-black-95: lab(0% 0 0 / 95%);
|
|
103
106
|
$color-black-75: lab(0% 0 0 / 75%);
|
|
104
107
|
$color-black-70: lab(0% 0 0 / 70%);
|
|
@@ -51,9 +51,24 @@
|
|
|
51
51
|
/>
|
|
52
52
|
|
|
53
53
|
<span
|
|
54
|
-
v-if="model.isNew"
|
|
55
|
-
class="card-
|
|
56
|
-
>
|
|
54
|
+
v-if="model.isNew || model.viewers"
|
|
55
|
+
class="card-webcam__badge"
|
|
56
|
+
>
|
|
57
|
+
<span
|
|
58
|
+
v-if="model.isNew"
|
|
59
|
+
class="card-webcam__badge-new"
|
|
60
|
+
>{{ t('webcams.new_model') }}</span>
|
|
61
|
+
|
|
62
|
+
<span
|
|
63
|
+
v-if="model.viewers"
|
|
64
|
+
class="card-webcam__badge-viewers"
|
|
65
|
+
>
|
|
66
|
+
<UiIcon
|
|
67
|
+
name="eye"
|
|
68
|
+
size="12"
|
|
69
|
+
/>{{ $n(model.viewers) }}
|
|
70
|
+
</span>
|
|
71
|
+
</span>
|
|
57
72
|
|
|
58
73
|
<span class="card-webcam__online"/>
|
|
59
74
|
|
|
@@ -99,7 +114,29 @@
|
|
|
99
114
|
>{{ t('webcams.years_old', { age: model.age }) }}</span>
|
|
100
115
|
</div>
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
<!-- На узких экранах ховера нет, поэтому цель стримера с прогрессом живёт под
|
|
118
|
+
карточкой; на десктопе она остаётся оверлеем на постере. -->
|
|
119
|
+
<div
|
|
120
|
+
v-if="model.goal"
|
|
121
|
+
class="card-webcam__goal-plain"
|
|
122
|
+
>
|
|
123
|
+
<span class="card-webcam__goal-plain-text _truncate">{{ model.goal.message }}</span>
|
|
124
|
+
|
|
125
|
+
<span class="card-webcam__goal-progress">
|
|
126
|
+
<span class="card-webcam__goal-track">
|
|
127
|
+
<span
|
|
128
|
+
:style="{ width: `${model.goal.percent}%` }"
|
|
129
|
+
class="card-webcam__goal-fill"
|
|
130
|
+
/>
|
|
131
|
+
</span>
|
|
132
|
+
<span class="card-webcam__goal-percent --plain">{{ model.goal.percent }}%</span>
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div
|
|
137
|
+
v-if="model.viewers || model.country"
|
|
138
|
+
class="card-webcam__meta"
|
|
139
|
+
>
|
|
103
140
|
<span
|
|
104
141
|
v-if="model.viewers"
|
|
105
142
|
class="card-webcam__viewers"
|
|
@@ -112,7 +149,7 @@
|
|
|
112
149
|
|
|
113
150
|
<a
|
|
114
151
|
v-if="model.country"
|
|
115
|
-
:href="`${nichePath}/webcams?
|
|
152
|
+
:href="`${nichePath}/webcams?filter_country=${model.country}`"
|
|
116
153
|
class="card-webcam__country _truncate"
|
|
117
154
|
>{{ countryName }}</a>
|
|
118
155
|
</div>
|
|
@@ -367,6 +404,12 @@ onBeforeUnmount(() => {
|
|
|
367
404
|
min-width: 0;
|
|
368
405
|
}
|
|
369
406
|
|
|
407
|
+
// нормализация даёт каждому следующему ребёнку `<article>` отступ 1em — в карточке
|
|
408
|
+
// отступы задаются построчно, поэтому ресет тут гасим
|
|
409
|
+
.card-webcam > * + * {
|
|
410
|
+
margin-top: 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
370
413
|
.card-webcam__poster {
|
|
371
414
|
position: relative;
|
|
372
415
|
display: block;
|
|
@@ -399,26 +442,49 @@ onBeforeUnmount(() => {
|
|
|
399
442
|
opacity: 1;
|
|
400
443
|
}
|
|
401
444
|
|
|
402
|
-
|
|
445
|
+
// одна плашка на постере: признак новой модели и зрители онлайн (на узких экранах
|
|
446
|
+
// меты под карточкой нет, поэтому зрители показываются здесь)
|
|
447
|
+
.card-webcam__badge {
|
|
403
448
|
@include font-4xs;
|
|
404
449
|
|
|
405
450
|
position: absolute;
|
|
406
451
|
top: 8px;
|
|
407
452
|
left: 8px;
|
|
408
453
|
z-index: 2;
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
gap: 6px;
|
|
409
457
|
padding: 2px 6px;
|
|
410
458
|
font-weight: 600;
|
|
411
459
|
line-height: 1;
|
|
412
460
|
text-transform: uppercase;
|
|
413
461
|
letter-spacing: .025em;
|
|
414
|
-
color: $card-webcam-
|
|
462
|
+
color: $card-webcam-badge-text;
|
|
463
|
+
text-shadow: 0 1px 2px $card-webcam-badge-shadow;
|
|
415
464
|
|
|
416
465
|
// лёгкое затенение: на светлом постере плашка иначе сливается с кадром
|
|
417
|
-
background-color: $card-webcam-
|
|
418
|
-
border: 1px solid $card-webcam-
|
|
466
|
+
background-color: $card-webcam-badge-bg;
|
|
467
|
+
border: 1px solid $card-webcam-badge-border;
|
|
419
468
|
border-radius: $radius-small;
|
|
420
469
|
}
|
|
421
470
|
|
|
471
|
+
.card-webcam__badge-viewers {
|
|
472
|
+
display: flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
gap: 4px;
|
|
475
|
+
|
|
476
|
+
@include from-br(xm) {
|
|
477
|
+
display: none;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// на десктопе плашка нужна только под признак новой модели
|
|
482
|
+
.card-webcam__badge:not(:has(.card-webcam__badge-new)) {
|
|
483
|
+
@include from-br(xm) {
|
|
484
|
+
display: none;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
422
488
|
.card-webcam__online {
|
|
423
489
|
position: absolute;
|
|
424
490
|
top: 8px;
|
|
@@ -492,10 +558,34 @@ onBeforeUnmount(() => {
|
|
|
492
558
|
|
|
493
559
|
flex-shrink: 0;
|
|
494
560
|
min-width: 4ch;
|
|
561
|
+
line-height: 16px;
|
|
495
562
|
font-weight: 500;
|
|
496
563
|
font-variant-numeric: tabular-nums;
|
|
497
564
|
text-align: right;
|
|
498
565
|
color: $card-webcam-goal-percent;
|
|
566
|
+
|
|
567
|
+
&.--plain {
|
|
568
|
+
color: $card-webcam-goal-plain-percent;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// цель под карточкой — только там, где нет ховера
|
|
573
|
+
.card-webcam__goal-plain {
|
|
574
|
+
display: grid;
|
|
575
|
+
row-gap: 6px;
|
|
576
|
+
margin-top: 8px;
|
|
577
|
+
|
|
578
|
+
@include from-br(xm) {
|
|
579
|
+
display: none;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.card-webcam__goal-plain-text {
|
|
584
|
+
@include font-xs;
|
|
585
|
+
|
|
586
|
+
--line-clamp: 1;
|
|
587
|
+
|
|
588
|
+
color: $card-webcam-goal-plain-text;
|
|
499
589
|
}
|
|
500
590
|
|
|
501
591
|
.card-webcam__head {
|
|
@@ -525,15 +615,21 @@ onBeforeUnmount(() => {
|
|
|
525
615
|
color: $card-webcam-meta-text;
|
|
526
616
|
}
|
|
527
617
|
|
|
618
|
+
// на узких экранах мету не показываем: зрители уехали в плашку на постере, а
|
|
619
|
+
// место под карточкой занимает цель стримера
|
|
528
620
|
.card-webcam__meta {
|
|
529
621
|
@include font-xs;
|
|
530
622
|
|
|
531
|
-
display:
|
|
623
|
+
display: none;
|
|
532
624
|
align-items: center;
|
|
533
625
|
justify-content: space-between;
|
|
534
626
|
gap: 8px;
|
|
535
627
|
margin-top: 8px;
|
|
536
628
|
color: $card-webcam-meta-text;
|
|
629
|
+
|
|
630
|
+
@include from-br(xm) {
|
|
631
|
+
display: flex;
|
|
632
|
+
}
|
|
537
633
|
}
|
|
538
634
|
|
|
539
635
|
.card-webcam__viewers {
|
|
@@ -545,25 +641,48 @@ onBeforeUnmount(() => {
|
|
|
545
641
|
.card-webcam__country {
|
|
546
642
|
--line-clamp: 1;
|
|
547
643
|
|
|
644
|
+
margin-left: auto;
|
|
645
|
+
|
|
548
646
|
&:is(:hover, :focus) {
|
|
549
647
|
color: $card-webcam-meta-text--hover;
|
|
550
648
|
}
|
|
551
649
|
}
|
|
552
650
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
.card-webcam__tags {
|
|
651
|
+
// селектор с родителем не случайно: ресет обнуляет отступы у `ul[class]`, и
|
|
652
|
+
// одиночного класса ему не хватает
|
|
653
|
+
.card-webcam > .card-webcam__tags {
|
|
558
654
|
display: flex;
|
|
559
655
|
flex-wrap: wrap;
|
|
560
656
|
gap: 6px;
|
|
657
|
+
margin-top: 10px;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// на узких экранах теги в один ряд с горизонтальным скроллом: в две колонки они
|
|
661
|
+
// не влезают, а перенос делал карточки в сетке разной высоты
|
|
662
|
+
.card-webcam > .card-webcam__tags {
|
|
663
|
+
@include to-br(xm) {
|
|
664
|
+
flex-wrap: nowrap;
|
|
665
|
+
overflow-x: auto;
|
|
666
|
+
padding-right: 24px;
|
|
667
|
+
scrollbar-width: none;
|
|
668
|
+
|
|
669
|
+
&::-webkit-scrollbar {
|
|
670
|
+
display: none;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// не сжимаем элементы ряда: иначе подпись внутри чипса переносится и ряд
|
|
676
|
+
// становится двухстрочным
|
|
677
|
+
.card-webcam__tags > li {
|
|
678
|
+
flex-shrink: 0;
|
|
561
679
|
}
|
|
562
680
|
|
|
563
681
|
.card-webcam__tag {
|
|
564
682
|
@include font-xs;
|
|
565
683
|
|
|
566
684
|
display: block;
|
|
685
|
+
white-space: nowrap;
|
|
567
686
|
padding: 4px 8px;
|
|
568
687
|
color: $card-webcam-tag-text;
|
|
569
688
|
background-color: $card-webcam-tag-bg;
|
|
@@ -573,5 +692,12 @@ onBeforeUnmount(() => {
|
|
|
573
692
|
color: $card-webcam-tag-text--hover;
|
|
574
693
|
background-color: $card-webcam-tag-bg--hover;
|
|
575
694
|
}
|
|
695
|
+
|
|
696
|
+
@include to-br(xm) {
|
|
697
|
+
@include font-3xs;
|
|
698
|
+
|
|
699
|
+
padding: 2px 8px;
|
|
700
|
+
line-height: 18px;
|
|
701
|
+
}
|
|
576
702
|
}
|
|
577
703
|
</style>
|
|
@@ -68,9 +68,13 @@ const broken = ref<Record<string, boolean>>({});
|
|
|
68
68
|
const items = computed(() => {
|
|
69
69
|
const first = (props.firstLang || locale.value).slice(0, 2).toLowerCase();
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
// провайдер вместо пустого массива присылает null — дефолт пропа в этом случае
|
|
72
|
+
// не срабатывает, поэтому подстраховываемся на самой границе компонента
|
|
73
|
+
const flags = props.flags || [];
|
|
74
|
+
|
|
75
|
+
const all = (props.languages || []).map((lang, index) => ({
|
|
72
76
|
lang,
|
|
73
|
-
country: (
|
|
77
|
+
country: (flags[index] || LANG_TO_FLAG[lang] || lang).toLowerCase(),
|
|
74
78
|
}));
|
|
75
79
|
|
|
76
80
|
return [...all.filter(item => item.lang === first), ...all.filter(item => item.lang !== first)]
|
|
@@ -81,7 +85,7 @@ const items = computed(() => {
|
|
|
81
85
|
const names = computed(() => {
|
|
82
86
|
const display = new Intl.DisplayNames([locale.value], { type: 'language' });
|
|
83
87
|
|
|
84
|
-
return props.languages.map(lang => display.of(lang) || lang.toUpperCase()).join(', ');
|
|
88
|
+
return (props.languages || []).map(lang => display.of(lang) || lang.toUpperCase()).join(', ');
|
|
85
89
|
});
|
|
86
90
|
</script>
|
|
87
91
|
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
<FilterChips
|
|
5
5
|
:items="chipsItems"
|
|
6
6
|
@click="onChipsClick"
|
|
7
|
-
|
|
7
|
+
>
|
|
8
|
+
<button
|
|
9
|
+
v-if="chipsItems.length > 1"
|
|
10
|
+
class="filter-model__clear"
|
|
11
|
+
type="button"
|
|
12
|
+
@click="onClearAllClick"
|
|
13
|
+
>{{ t('reset_all') }}</button>
|
|
14
|
+
</FilterChips>
|
|
8
15
|
<transition appear>
|
|
9
16
|
<FilterPopup
|
|
10
17
|
v-model="model"
|
|
@@ -18,6 +25,19 @@
|
|
|
18
25
|
@chip-click="onPendingChipsClick"
|
|
19
26
|
@field-update="onFieldUpdate"
|
|
20
27
|
>
|
|
28
|
+
<template #quick-field="{ item, index }">
|
|
29
|
+
<FilterModelField
|
|
30
|
+
v-if="quickFilter(item as { name: string })"
|
|
31
|
+
:field="quickFilter(item as { name: string }) as IModelFilter"
|
|
32
|
+
:index="index"
|
|
33
|
+
:pending-query="pendingQuery"
|
|
34
|
+
:units="units"
|
|
35
|
+
:unit-items="unitItems"
|
|
36
|
+
@update:pending-query="onQueryUpdate"
|
|
37
|
+
@update:units="units = $event"
|
|
38
|
+
/>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
21
41
|
<template #field="{ field, index }">
|
|
22
42
|
<FilterModelField
|
|
23
43
|
:field="field as IModelFilter"
|
|
@@ -147,6 +167,11 @@ watch(units, () => {
|
|
|
147
167
|
pendingQuery.value = next;
|
|
148
168
|
});
|
|
149
169
|
|
|
170
|
+
/** Быстрые поля в модалку приходят уже маппингом — исходный фильтр ищем по имени. */
|
|
171
|
+
function quickFilter(item: { name: string }) {
|
|
172
|
+
return quickFilters.value.find(filter => filter.name === item.name);
|
|
173
|
+
}
|
|
174
|
+
|
|
150
175
|
function selectItems(item: IModelFilter) {
|
|
151
176
|
return item.options.map(option => ({
|
|
152
177
|
value: option.name,
|
|
@@ -218,6 +243,17 @@ function onResetClick() {
|
|
|
218
243
|
pendingQuery.value = {};
|
|
219
244
|
}
|
|
220
245
|
|
|
246
|
+
/** «Сбросить всё» в облаке чипсов: снимает фильтры, сортировку не трогает. */
|
|
247
|
+
function onClearAllClick() {
|
|
248
|
+
const query = { ...route.query };
|
|
249
|
+
Object.keys(query)
|
|
250
|
+
.filter(key => key.startsWith('filter_'))
|
|
251
|
+
.forEach(key => delete query[key]);
|
|
252
|
+
delete query['page'];
|
|
253
|
+
|
|
254
|
+
router.replace({ query });
|
|
255
|
+
}
|
|
256
|
+
|
|
221
257
|
function onApplyClick() {
|
|
222
258
|
model.value = false;
|
|
223
259
|
router.replace({ query: pendingQuery.value });
|
|
@@ -229,6 +265,25 @@ function onApplyClick() {
|
|
|
229
265
|
margin-top: 8px;
|
|
230
266
|
}
|
|
231
267
|
|
|
268
|
+
// в облаке чипсов кнопка идёт последней, а слот `FilterChips` кладёт её первой
|
|
269
|
+
.filter-model__clear {
|
|
270
|
+
@include font-sm;
|
|
271
|
+
|
|
272
|
+
order: 1;
|
|
273
|
+
padding: 6px 8px;
|
|
274
|
+
color: $filter-model-clear-text;
|
|
275
|
+
|
|
276
|
+
&:is(:hover, :focus) {
|
|
277
|
+
color: $filter-model-clear-text--hover;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// диапазон в сетке быстрых фильтров занимает обе колонки: слайдеру нужна вся
|
|
282
|
+
// ширина под шкалу и подписи границ
|
|
283
|
+
.filter-popup__grid > .filter-model-field__slider {
|
|
284
|
+
grid-column: 1 / -1;
|
|
285
|
+
}
|
|
286
|
+
|
|
232
287
|
.filter-model__footer-buttons {
|
|
233
288
|
display: flex;
|
|
234
289
|
justify-content: space-between;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!-- Сетка вебкам-тумб: те же колонки, что у основной сетки видео (2 / 3 / 4).
|
|
2
|
+
|
|
3
|
+
Компонент отвечает только за раскладку и за выбор обёртки провайдера: обёртка
|
|
4
|
+
берётся из мапы адаптеров по конфигу и рендерится через `<component :is>`, так
|
|
5
|
+
что сетка не знает ни про Stripcash, ни про формат его полей.
|
|
6
|
+
|
|
7
|
+
События тумб (клик и появление постера) уходят наружу вместе с индексом — сквозной
|
|
8
|
+
индекс тумбы на странице нужен статистике, а карточка про страницу не знает. -->
|
|
9
|
+
<template>
|
|
10
|
+
<ul class="grid-webcams">
|
|
11
|
+
<li
|
|
12
|
+
v-for="(model, index) in items"
|
|
13
|
+
:key="`webcam-${model.id || model.username}`"
|
|
14
|
+
class="grid-webcams__item"
|
|
15
|
+
>
|
|
16
|
+
<component
|
|
17
|
+
:is="adapter"
|
|
18
|
+
:item="model"
|
|
19
|
+
:touch-preview="isMobile"
|
|
20
|
+
:first-lang="locale"
|
|
21
|
+
@click="emit('model-click', { model, index })"
|
|
22
|
+
@poster="element => emit('model-view', { model, index, element })"
|
|
23
|
+
/>
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import type { IStripcashModel, IWebcamsConfig } from '../../types';
|
|
30
|
+
import { WEBCAMS_DEFAULTS } from '../../runtime';
|
|
31
|
+
import { resolveWebcamAdapter } from '../../utils/webcams/adapters';
|
|
32
|
+
|
|
33
|
+
defineProps<{
|
|
34
|
+
items: IStripcashModel[]
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Клики и показы тумб сетка отдаёт наружу: индексы она знает, а куда их
|
|
39
|
+
* отправлять — дело сайта (у статистики свой контекст страницы и устройства).
|
|
40
|
+
*/
|
|
41
|
+
const emit = defineEmits<{
|
|
42
|
+
'model-click': [payload: { model: IStripcashModel, index: number }]
|
|
43
|
+
'model-view': [payload: { model: IStripcashModel, index: number, element: HTMLElement }]
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
const { locale } = useI18n();
|
|
47
|
+
const isMobile = useState<boolean>('isMobile');
|
|
48
|
+
|
|
49
|
+
const webcamsConfig = computed(() => (useRuntimeConfig().public.webcamsConfig || {}) as IWebcamsConfig);
|
|
50
|
+
|
|
51
|
+
/** Обёртка активного провайдера: ленивый импорт из мапы адаптеров. */
|
|
52
|
+
const provider = computed(() => String(webcamsConfig.value.provider || WEBCAMS_DEFAULTS.provider));
|
|
53
|
+
const adapter = computed(() => defineAsyncComponent(resolveWebcamAdapter(provider.value)));
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style lang="scss">
|
|
57
|
+
.grid-webcams {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: 12px;
|
|
60
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
61
|
+
|
|
62
|
+
@include from-br(xs) {
|
|
63
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@include from-br(xm) {
|
|
67
|
+
gap: 24px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@include from-br(md) {
|
|
71
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.grid-webcams__item {
|
|
76
|
+
min-width: 0;
|
|
77
|
+
}
|
|
78
|
+
</style>
|