itube-specs 0.0.935 → 0.0.938
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 +11 -11
- package/assets/scss/themes/light/_theme.scss +8 -8
- package/assets/scss/vars/_tokens.scss +1 -1
- package/components/auth/recovery.vue +0 -1
- package/components/auth/register.vue +0 -1
- package/components/card/video/actions.vue +14 -5
- package/components/filter/chips-row.vue +56 -0
- package/components/filter/chips.vue +48 -28
- package/components/filter/main.vue +53 -106
- package/components/filter/model.vue +25 -71
- package/components/filter/popup.vue +11 -15
- package/components/filter/selects.vue +54 -23
- package/components/filter/videos-chips.vue +51 -20
- package/components/grid/videos.vue +143 -39
- package/components/grid/webcams.vue +6 -20
- package/components/layout/burger.vue +5 -0
- package/components/layout/footer-language.vue +19 -3
- package/components/layout/header-user.vue +1 -2
- package/components/layout/header.vue +1 -4
- package/components/layout/language.vue +21 -6
- package/components/layout/niche.vue +1 -3
- package/components/report/main.vue +1 -3
- package/components/search/main.vue +4 -3
- package/components/section/toolbar.vue +8 -0
- package/components/ui/chips-micro.vue +1 -2
- package/components/ui/chips.vue +1 -2
- package/components/ui/dropdown.vue +2 -4
- package/components/ui/follow-btn.vue +10 -4
- package/components/ui/notification.vue +6 -1
- package/components/ui/popup.vue +38 -22
- package/components/ui/snackbar.vue +1 -1
- package/components/ui/tooltip.vue +4 -6
- package/components/view/models.vue +14 -2
- package/components/view/video.vue +12 -1
- package/components/view/videos.vue +54 -55
- package/components/webcam/thumb.vue +40 -0
- package/composables/__tests__/use-model-filter-chips.test.ts +8 -4
- package/composables/fetch/use-fetch-quick-filter-groups.ts +35 -0
- package/composables/fetch/use-fetch-webcams.ts +5 -2
- package/composables/use-filter-pending.ts +72 -0
- package/composables/use-hover-guard.ts +28 -0
- package/composables/use-model-filter-chips.ts +5 -10
- package/composables/use-overlay-close.ts +92 -0
- package/composables/use-recaptcha.ts +0 -2
- package/package.json +1 -1
- package/pages/contacts.vue +1 -3
- package/pages/index.vue +8 -16
- package/pages/search/[slug]/videos.vue +0 -1
- package/pages/tag/[tag].vue +0 -4
- package/runtime/constants/async-data.ts +1 -1
- package/server/api/categories/quick-groups.get.ts +37 -0
- package/services/api/categories.service.ts +21 -22
- package/types/filter-chip-item.d.ts +12 -0
- package/types/index.d.ts +2 -0
- package/types/model-filter.d.ts +5 -1
- package/types/quick-filter.d.ts +22 -0
- package/utils/filter-chips/__tests__/build-chip-parts.test.ts +28 -0
- package/utils/filter-chips/build-chip-parts.ts +19 -0
- package/utils/filter-model/__tests__/build-model-filter-query.test.ts +48 -0
- package/utils/filter-model/build-model-filter-query.ts +22 -0
- package/utils/filter-model/parse-model-filter-values.ts +19 -0
- package/utils/filter-selects/__tests__/build-categories-query.test.ts +52 -0
- package/utils/filter-selects/__tests__/to-quick-filters.test.ts +82 -0
- package/utils/filter-selects/build-categories-query.ts +32 -0
- package/utils/filter-selects/to-quick-filters.ts +47 -0
- package/utils/webcams/__tests__/insert-positions.test.ts +45 -0
- package/utils/webcams/insert-positions.ts +54 -0
- package/composables/fetch/use-fetch-top-random-categories.ts +0 -21
|
@@ -225,11 +225,10 @@ $video-card-action-bg--hover: $color-zinc-800;
|
|
|
225
225
|
$video-card-action-border: $color-zinc-800-60;
|
|
226
226
|
$video-card-action-icon: $color-zinc-300;
|
|
227
227
|
$video-card-action-active-bg: $color-zinc-800;
|
|
228
|
-
$video-card-action-active-bg--hover: $color-red-600;
|
|
229
228
|
$video-card-action-active-border: $color-zinc-800;
|
|
230
229
|
$video-card-action-active-border--hover: $color-red-600;
|
|
230
|
+
$video-card-action-active-ring--hover: 0 0 0 2px $color-red-600;
|
|
231
231
|
$video-card-action-active-icon: $color-white;
|
|
232
|
-
$video-card-action-active-icon--hover: $color-white;
|
|
233
232
|
|
|
234
233
|
// ─── Video card dropdown ───
|
|
235
234
|
$video-card-dropdown-size--hover: 1;
|
|
@@ -704,11 +703,18 @@ $seo-footer-bg: $color-zinc-800-50;
|
|
|
704
703
|
$empty-text: $color-zinc-500;
|
|
705
704
|
|
|
706
705
|
// ─── Filter chips ───
|
|
707
|
-
$filter-chips-border: $color-zinc-700;
|
|
708
|
-
$filter-chips-
|
|
706
|
+
$filter-chips-border: $color-zinc-700-50;
|
|
707
|
+
$filter-chips-border--hover: $color-zinc-600;
|
|
708
|
+
$filter-chips-bg: $color-zinc-800-60;
|
|
709
709
|
$filter-chips-text: $color-zinc-300;
|
|
710
|
-
|
|
710
|
+
|
|
711
|
+
// на ховер тут ведёт рамка, фон остаётся прежним
|
|
712
|
+
$filter-chips-bg--hover: $color-zinc-800-60;
|
|
711
713
|
$filter-chips-close: $color-zinc-500;
|
|
714
|
+
$filter-chips-close--hover: $color-white;
|
|
715
|
+
$filter-chips-label: $color-zinc-500;
|
|
716
|
+
$filter-chips-row-clear-text: $color-zinc-500;
|
|
717
|
+
$filter-chips-row-clear-text--hover: $color-white;
|
|
712
718
|
|
|
713
719
|
// ─── Filter button ───
|
|
714
720
|
$filter-button-count-bg: $color-zinc-300;
|
|
@@ -852,8 +858,6 @@ $page-model-videos-section-border: $color-zinc-800-50;
|
|
|
852
858
|
// ─── Page videos ───
|
|
853
859
|
$page-videos-title-border: $color-zinc-800-50;
|
|
854
860
|
$page-videos-footer-border: $color-zinc-800;
|
|
855
|
-
$page-videos-categories-border: $color-indigo-600;
|
|
856
|
-
$page-videos-categories-icon: $color-indigo-600;
|
|
857
861
|
|
|
858
862
|
// ─── Градиенты fade ───
|
|
859
863
|
$bg-fade-90: $color-zinc-950-90; // почти непрозрачный
|
|
@@ -1134,10 +1138,6 @@ $switcher-icon--active: $color-emerald-400;
|
|
|
1134
1138
|
$switcher-subtitle: $color-zinc-600;
|
|
1135
1139
|
$switcher-subtitle--active: $color-white-60;
|
|
1136
1140
|
|
|
1137
|
-
// ─── Filter: сброс всех фильтров в облаке чипсов ───
|
|
1138
|
-
$filter-model-clear-text: $color-zinc-500;
|
|
1139
|
-
$filter-model-clear-text--hover: $color-white;
|
|
1140
|
-
|
|
1141
1141
|
// ─── Вебкам-карточка (продолжение) ───
|
|
1142
1142
|
$card-webcam-languages-border: $color-white-20;
|
|
1143
1143
|
$card-webcam-languages-fallback-bg: $color-zinc-800;
|
|
@@ -283,11 +283,10 @@ $video-card-action-bg--hover: $color-white;
|
|
|
283
283
|
$video-card-action-border: $color-gray-300-50;
|
|
284
284
|
$video-card-action-icon: $color-gray-700;
|
|
285
285
|
$video-card-action-active-bg: $color-white;
|
|
286
|
-
$video-card-action-active-bg--hover: $color-red-600;
|
|
287
286
|
$video-card-action-active-border: $color-gray-300;
|
|
288
287
|
$video-card-action-active-border--hover: $color-red-600;
|
|
288
|
+
$video-card-action-active-ring--hover: 0 0 0 2px $color-red-600;
|
|
289
289
|
$video-card-action-active-icon: $color-gray-900;
|
|
290
|
-
$video-card-action-active-icon--hover: $color-white;
|
|
291
290
|
|
|
292
291
|
// ─── Video menu (меню действий над видео) ───
|
|
293
292
|
$video-menu-preview-border: transparent;
|
|
@@ -754,10 +753,17 @@ $empty-text: $color-gray-500;
|
|
|
754
753
|
|
|
755
754
|
// ─── Filter chips ───
|
|
756
755
|
$filter-chips-border: transparent;
|
|
756
|
+
|
|
757
|
+
// рамки нет — на ховер ведёт фон
|
|
758
|
+
$filter-chips-border--hover: transparent;
|
|
757
759
|
$filter-chips-bg: $color-gray-200;
|
|
758
760
|
$filter-chips-text: $color-gray-700;
|
|
759
761
|
$filter-chips-bg--hover: $color-gray-300;
|
|
760
762
|
$filter-chips-close: $color-gray-500;
|
|
763
|
+
$filter-chips-close--hover: $color-gray-950;
|
|
764
|
+
$filter-chips-label: $color-gray-500;
|
|
765
|
+
$filter-chips-row-clear-text: $color-gray-500;
|
|
766
|
+
$filter-chips-row-clear-text--hover: $color-gray-950;
|
|
761
767
|
|
|
762
768
|
// ─── Filter button ───
|
|
763
769
|
$filter-button-count-bg: $color-white-20;
|
|
@@ -878,8 +884,6 @@ $page-model-videos-section-border: transparent;
|
|
|
878
884
|
// ─── Page videos ───
|
|
879
885
|
$page-videos-title-border: $color-gray-200-50;
|
|
880
886
|
$page-videos-footer-border: transparent;
|
|
881
|
-
$page-videos-categories-border: $color-indigo-600;
|
|
882
|
-
$page-videos-categories-icon: $color-indigo-600;
|
|
883
887
|
|
|
884
888
|
// ─── Градиенты fade ───
|
|
885
889
|
$bg-fade-90: $color-gray-950-90; // почти непрозрачный
|
|
@@ -1160,10 +1164,6 @@ $switcher-icon--active: $color-emerald-400;
|
|
|
1160
1164
|
$switcher-subtitle: $color-gray-500;
|
|
1161
1165
|
$switcher-subtitle--active: $color-white-60;
|
|
1162
1166
|
|
|
1163
|
-
// ─── Filter: сброс всех фильтров в облаке чипсов ───
|
|
1164
|
-
$filter-model-clear-text: $color-gray-500;
|
|
1165
|
-
$filter-model-clear-text--hover: $color-gray-950;
|
|
1166
|
-
|
|
1167
1167
|
// ─── Вебкам-карточка (продолжение) ───
|
|
1168
1168
|
$card-webcam-languages-border: $color-gray-300;
|
|
1169
1169
|
$card-webcam-languages-fallback-bg: $color-gray-200;
|
|
@@ -76,7 +76,6 @@ const { setErrorState, showError, show, resetSnackbar } = useSnackbar();
|
|
|
76
76
|
onMounted(async () => {
|
|
77
77
|
try {
|
|
78
78
|
await initRecaptcha();
|
|
79
|
-
console.log('reCAPTCHA готов для этой формы');
|
|
80
79
|
} catch (err) {
|
|
81
80
|
console.error('Ошибка инициализации reCAPTCHA:', err);
|
|
82
81
|
}
|
|
@@ -134,7 +134,6 @@ const { setErrorState, showError, show, resetSnackbar } = useSnackbar();
|
|
|
134
134
|
onMounted(async () => {
|
|
135
135
|
try {
|
|
136
136
|
await initRecaptcha();
|
|
137
|
-
console.log('reCAPTCHA готов для этой формы');
|
|
138
137
|
} catch (err) {
|
|
139
138
|
console.error('Ошибка инициализации reCAPTCHA:', err);
|
|
140
139
|
}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
:aria-pressed="later"
|
|
23
23
|
:class="['card-video-actions__btn', '--watch-later', { '--active': later }]"
|
|
24
24
|
type="button"
|
|
25
|
-
|
|
25
|
+
v-bind="hoverGuard"
|
|
26
|
+
@click="onWatchLaterClick"
|
|
26
27
|
>
|
|
27
28
|
<UiIcon
|
|
28
29
|
:name="later ? 'clock-filled' : 'clock'"
|
|
@@ -44,9 +45,17 @@ const props = defineProps<{
|
|
|
44
45
|
const { t } = useI18n();
|
|
45
46
|
|
|
46
47
|
const { isFavorite, isWatchLater, toggleFavorite, toggleWatchLater } = useVideoCollections();
|
|
48
|
+
const { blockHover, hoverGuard } = useHoverGuard();
|
|
47
49
|
|
|
48
50
|
const favorite = computed(() => isFavorite(props.card.guid));
|
|
49
51
|
const later = computed(() => isWatchLater(props.card.guid));
|
|
52
|
+
|
|
53
|
+
/* The heart is only rendered while the video is in favorites, so a click always unmounts it — only
|
|
54
|
+
«watch later» stays under the cursor and needs the hover muted. */
|
|
55
|
+
function onWatchLaterClick() {
|
|
56
|
+
blockHover();
|
|
57
|
+
toggleWatchLater(props.card.guid, props.card.title);
|
|
58
|
+
}
|
|
50
59
|
</script>
|
|
51
60
|
|
|
52
61
|
<style lang="scss">
|
|
@@ -97,6 +106,7 @@ $action-size: 36px;
|
|
|
97
106
|
var(--transition) opacity,
|
|
98
107
|
var(--transition) background-color,
|
|
99
108
|
var(--transition) border-color,
|
|
109
|
+
var(--transition) box-shadow,
|
|
100
110
|
var(--transition) color;
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -104,14 +114,13 @@ $action-size: 36px;
|
|
|
104
114
|
opacity: 1;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
.card-video-actions__btn:is(:hover, :focus-visible) {
|
|
117
|
+
.card-video-actions__btn:not(.--active):is(:hover, :focus-visible) {
|
|
108
118
|
background-color: $video-card-action-bg--hover;
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
.card-video-actions__btn.--active:is(:hover, :focus-visible) {
|
|
112
|
-
background-color: $video-card-action-active-bg--hover;
|
|
121
|
+
.card-video-actions__btn.--active:not(.--no-hover):is(:hover, :focus-visible) {
|
|
113
122
|
border-color: $video-card-action-active-border--hover;
|
|
114
|
-
|
|
123
|
+
box-shadow: $video-card-action-active-ring--hover;
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- Облако чипсов активных фильтров с кнопкой сброса -->
|
|
2
|
+
<template>
|
|
3
|
+
<div
|
|
4
|
+
v-if="items.length > 0"
|
|
5
|
+
class="filter-chips-row"
|
|
6
|
+
>
|
|
7
|
+
<slot/>
|
|
8
|
+
<FilterChips
|
|
9
|
+
v-for="(item, index) in items"
|
|
10
|
+
:key="`filter-chips-${index}`"
|
|
11
|
+
:item="item"
|
|
12
|
+
@click="emit('click', item)"
|
|
13
|
+
/>
|
|
14
|
+
<button
|
|
15
|
+
v-if="items.length > 1"
|
|
16
|
+
class="filter-chips-row__clear"
|
|
17
|
+
type="button"
|
|
18
|
+
@click="emit('clear')"
|
|
19
|
+
>{{ t('reset_all') }}</button>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import type { IFilterChipItem } from '../../types';
|
|
25
|
+
|
|
26
|
+
defineProps<{
|
|
27
|
+
items: IFilterChipItem[]
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const { t } = useI18n();
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(eventName: 'click', eventValue: IFilterChipItem): void
|
|
34
|
+
(eventName: 'clear'): void
|
|
35
|
+
}>();
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<style lang="scss">
|
|
39
|
+
.filter-chips-row {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-wrap: wrap;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 8px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.filter-chips-row__clear {
|
|
47
|
+
@include font-sm;
|
|
48
|
+
|
|
49
|
+
padding: 6px 8px;
|
|
50
|
+
color: $filter-chips-row-clear-text;
|
|
51
|
+
|
|
52
|
+
@include hover(color) {
|
|
53
|
+
color: $filter-chips-row-clear-text--hover;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -1,58 +1,78 @@
|
|
|
1
|
-
<!--
|
|
1
|
+
<!-- Чипса выбранного фильтра: приглушённое название фильтра, значение и сброс по клику -->
|
|
2
2
|
<template>
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
<button
|
|
4
|
+
class="filter-chips"
|
|
5
|
+
type="button"
|
|
6
|
+
@click="emit('click')"
|
|
7
|
+
>
|
|
8
|
+
<span class="filter-chips__text">
|
|
9
|
+
<span
|
|
10
|
+
v-if="item.label && !withoutLabel"
|
|
11
|
+
class="filter-chips__label"
|
|
12
|
+
>{{ item.label }}:</span>{{ item.title }}
|
|
13
|
+
</span>
|
|
14
|
+
<UiIcon
|
|
15
|
+
class="filter-chips__close"
|
|
16
|
+
name="close"
|
|
17
|
+
size="16"
|
|
15
18
|
/>
|
|
16
|
-
</
|
|
19
|
+
</button>
|
|
17
20
|
</template>
|
|
18
21
|
|
|
19
22
|
<script setup lang="ts">
|
|
20
|
-
import type {
|
|
23
|
+
import type { IFilterChipItem } from '../../types';
|
|
21
24
|
|
|
22
25
|
defineProps<{
|
|
23
|
-
|
|
26
|
+
item: IFilterChipItem
|
|
27
|
+
/** Без названия фильтра — в попапе фильтр уже подписан своим полем. */
|
|
28
|
+
withoutLabel?: boolean
|
|
24
29
|
}>();
|
|
25
30
|
|
|
26
31
|
const emit = defineEmits<{
|
|
27
|
-
(eventName: 'click'
|
|
32
|
+
(eventName: 'click'): void
|
|
28
33
|
}>();
|
|
29
|
-
|
|
30
|
-
function onChipsClick(item: IChipsItem) {
|
|
31
|
-
emit('click', item);
|
|
32
|
-
}
|
|
33
34
|
</script>
|
|
34
35
|
|
|
35
36
|
<style lang="scss">
|
|
36
37
|
.filter-chips {
|
|
37
38
|
@include font-sm;
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
column-gap: 8px;
|
|
43
|
+
padding-block: 6px;
|
|
44
|
+
padding-left: 12px;
|
|
45
|
+
|
|
46
|
+
// справа отступ меньше — там иконка сброса со своим зазором
|
|
47
|
+
padding-right: 8px;
|
|
40
48
|
border: 1px solid $filter-chips-border;
|
|
49
|
+
border-radius: $radius-md;
|
|
41
50
|
background-color: $filter-chips-bg;
|
|
42
|
-
pointer-events: all;
|
|
43
51
|
color: $filter-chips-text;
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
text-transform: capitalize;
|
|
44
54
|
|
|
45
|
-
@include hover(background-color) {
|
|
55
|
+
@include hover(background-color, border-color) {
|
|
46
56
|
background-color: $filter-chips-bg--hover;
|
|
57
|
+
border-color: $filter-chips-border--hover;
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
// подпись и значение читаются одной фразой, поэтому не отдельный флекс-элемент
|
|
62
|
+
.filter-chips__label {
|
|
63
|
+
padding-right: 4px;
|
|
64
|
+
color: $filter-chips-label;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.filter-chips__close {
|
|
52
68
|
color: $filter-chips-close;
|
|
69
|
+
transition: var(--transition) color;
|
|
70
|
+
}
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
// иконка подсвечивается вместе с чипсой, а не сама по себе
|
|
73
|
+
@media (hover: hover) {
|
|
74
|
+
.filter-chips:is(:hover, :focus-visible) .filter-chips__close {
|
|
75
|
+
color: $filter-chips-close--hover;
|
|
56
76
|
}
|
|
57
77
|
}
|
|
58
78
|
</style>
|
|
@@ -43,8 +43,7 @@
|
|
|
43
43
|
</template>
|
|
44
44
|
|
|
45
45
|
<script setup lang="ts">
|
|
46
|
-
import type { ButtonSizes, ButtonThemes,
|
|
47
|
-
import type { LocationQuery } from '#vue-router';
|
|
46
|
+
import type { ButtonSizes, ButtonThemes, IFilterChipItem, IFilterScheme, ISelectItem } from '../../types';
|
|
48
47
|
import { parseFilterCategories } from '../../utils/parse-filter-categories';
|
|
49
48
|
import { parseCategoryFilterValues } from '../../utils/filter-main/parse-category-filter-values';
|
|
50
49
|
import { getActiveFilterValues } from '../../utils/filter-main/get-active-filter-values';
|
|
@@ -64,23 +63,40 @@ const emit = defineEmits<{
|
|
|
64
63
|
|
|
65
64
|
const { t } = useI18n();
|
|
66
65
|
const route = useRoute();
|
|
67
|
-
const router = useRouter();
|
|
68
66
|
|
|
69
67
|
const filterOpen = ref(false);
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
// Длительность и дата добавления живут в query своими параметрами, но в попапе это такие же
|
|
70
|
+
// поля, как категории — держим их в одном наборе выбора и разводим обратно при применении
|
|
71
|
+
const DURATION_FIELD = 'duration';
|
|
72
|
+
const ADDED_FIELD = 'added';
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
values: filterValue,
|
|
76
|
+
count: activeCount,
|
|
77
|
+
isFiltered,
|
|
78
|
+
set: setField,
|
|
79
|
+
remove: removeField,
|
|
80
|
+
reset,
|
|
81
|
+
apply: saveResult,
|
|
82
|
+
} = useFilterPending({
|
|
83
|
+
open: filterOpen,
|
|
84
|
+
decode: query => ({
|
|
85
|
+
...parseCategoryFilterValues(parseFilterCategories(query)),
|
|
86
|
+
...(query[DURATION_FIELD] ? { [DURATION_FIELD]: String(query[DURATION_FIELD]) } : {}),
|
|
87
|
+
...(query[ADDED_FIELD] ? { [ADDED_FIELD]: String(query[ADDED_FIELD]) } : {}),
|
|
88
|
+
}),
|
|
89
|
+
encode: (values, query) => {
|
|
90
|
+
const { [DURATION_FIELD]: durationValue, [ADDED_FIELD]: addedValue, ...categories } = values;
|
|
91
|
+
|
|
92
|
+
return buildFilterQuery({
|
|
93
|
+
currentQuery: query,
|
|
94
|
+
filterValue: categories,
|
|
95
|
+
durationValue: durationValue || '',
|
|
96
|
+
addedValue: addedValue || '',
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
});
|
|
84
100
|
|
|
85
101
|
watch(filterOpen, (value) => {
|
|
86
102
|
if (value) emit('open');
|
|
@@ -92,14 +108,6 @@ watch(() => route.query, (value) => {
|
|
|
92
108
|
filterCount.value = getActiveFilterValues(value).length;
|
|
93
109
|
});
|
|
94
110
|
|
|
95
|
-
const activeCount = computed(() => {
|
|
96
|
-
const hasDuration = durationValue.value ? 1 : 0;
|
|
97
|
-
const hasAdded = addedValue.value ? 1 : 0;
|
|
98
|
-
return Object.keys(filterValue.value).length + hasAdded + hasDuration;
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const isFiltered = computed(() => !!route.query['categories'] || Object.keys(filterValue.value).length > 0 || !!durationValue.value || !!addedValue.value);
|
|
102
|
-
|
|
103
111
|
const QUICK_NAMES = ['action', 'guy type', 'ppl involved', 'ethnicity & nationality'];
|
|
104
112
|
const PERFORMER_NAMES = [
|
|
105
113
|
'age',
|
|
@@ -180,9 +188,9 @@ const groupItems = computed(() => {
|
|
|
180
188
|
name: 'duration',
|
|
181
189
|
title: t('duration'),
|
|
182
190
|
label: t('duration'),
|
|
183
|
-
value:
|
|
191
|
+
value: filterValue.value[DURATION_FIELD] ?? '',
|
|
184
192
|
items: translateTitle(props.durationItems),
|
|
185
|
-
active: !!
|
|
193
|
+
active: !!filterValue.value[DURATION_FIELD],
|
|
186
194
|
icon: 'time',
|
|
187
195
|
placeholder: t('select'),
|
|
188
196
|
} as any,
|
|
@@ -191,9 +199,9 @@ const groupItems = computed(() => {
|
|
|
191
199
|
name: 'added',
|
|
192
200
|
title: t('added'),
|
|
193
201
|
label: t('added'),
|
|
194
|
-
value:
|
|
202
|
+
value: filterValue.value[ADDED_FIELD] ?? '',
|
|
195
203
|
items: translateTitle(props.addedItems),
|
|
196
|
-
active: !!
|
|
204
|
+
active: !!filterValue.value[ADDED_FIELD],
|
|
197
205
|
icon: 'date',
|
|
198
206
|
placeholder: t('select'),
|
|
199
207
|
} as any,
|
|
@@ -207,93 +215,32 @@ const groupItems = computed(() => {
|
|
|
207
215
|
];
|
|
208
216
|
});
|
|
209
217
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
for (const str of parseFilterCategories(route.query)) {
|
|
217
|
-
const [key, ...rest] = str.split('_');
|
|
218
|
-
const value = rest.join('_');
|
|
219
|
-
if (!key || !value) continue;
|
|
220
|
-
const schemeItem = props.scheme.find(s => s.name.toLowerCase() === key);
|
|
221
|
-
const matched = schemeItem?.items.find(i => i.value === value);
|
|
222
|
-
chips.push({ title: matched?.title || value, key });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const duration = route.query.duration as string;
|
|
226
|
-
if (duration) {
|
|
227
|
-
const item = props.durationItems.find(i => i.value === duration);
|
|
228
|
-
chips.push({ title: item?.title ? t(item.title) : duration, key: DURATION_CHIP_KEY });
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const added = route.query.added as string;
|
|
232
|
-
if (added) {
|
|
233
|
-
const item = props.addedItems.find(i => i.value === added);
|
|
234
|
-
chips.push({ title: item?.title ? t(item.title) : added, key: ADDED_CHIP_KEY });
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return chips;
|
|
238
|
-
});
|
|
218
|
+
// Чипсы попапа показывают выбор, а не применённое: иначе только что отмеченный фильтр
|
|
219
|
+
// появится в них лишь после «Применить», хотя счётчик рядом уже посчитан
|
|
220
|
+
const activeChips = computed<IFilterChipItem[]>(() => Object.entries(filterValue.value)
|
|
221
|
+
.map(([key, value]) => ({ title: fieldValueTitle(key, value), key }))
|
|
222
|
+
);
|
|
239
223
|
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
224
|
+
function fieldValueTitle(name: string, value: string) {
|
|
225
|
+
if (name === DURATION_FIELD || name === ADDED_FIELD) {
|
|
226
|
+
const items = name === DURATION_FIELD ? props.durationItems : props.addedItems;
|
|
227
|
+
const item = items.find(i => i.value === value);
|
|
243
228
|
|
|
244
|
-
|
|
245
|
-
delete nextQuery.duration;
|
|
246
|
-
} else if (item.key === ADDED_CHIP_KEY) {
|
|
247
|
-
delete nextQuery.added;
|
|
248
|
-
} else if (item.key) {
|
|
249
|
-
const remaining = parseFilterCategories(route.query).filter(str => {
|
|
250
|
-
const [k] = str.split('_');
|
|
251
|
-
return k !== item.key;
|
|
252
|
-
});
|
|
253
|
-
if (remaining.length) {
|
|
254
|
-
nextQuery.categories = remaining.join(',');
|
|
255
|
-
} else {
|
|
256
|
-
delete nextQuery.categories;
|
|
257
|
-
}
|
|
229
|
+
return item?.title ? t(item.title) : value;
|
|
258
230
|
}
|
|
259
231
|
|
|
260
|
-
|
|
261
|
-
}
|
|
232
|
+
const schemeItem = props.scheme.find(s => s.name.toLowerCase() === name);
|
|
262
233
|
|
|
263
|
-
|
|
264
|
-
if (name === 'duration') {
|
|
265
|
-
durationValue.value = value;
|
|
266
|
-
} else if (name === 'added') {
|
|
267
|
-
addedValue.value = value;
|
|
268
|
-
} else {
|
|
269
|
-
if (!value) {
|
|
270
|
-
const next = { ...filterValue.value };
|
|
271
|
-
delete next[name];
|
|
272
|
-
filterValue.value = next;
|
|
273
|
-
} else {
|
|
274
|
-
filterValue.value = { ...filterValue.value, [name]: value };
|
|
275
|
-
}
|
|
276
|
-
}
|
|
234
|
+
return schemeItem?.items.find(i => i.value === value)?.title || value;
|
|
277
235
|
}
|
|
278
236
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
currentQuery: route.query,
|
|
283
|
-
filterValue: filterValue.value,
|
|
284
|
-
durationValue: durationValue.value,
|
|
285
|
-
addedValue: addedValue.value,
|
|
286
|
-
}),
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
filterOpen.value = false;
|
|
237
|
+
// Снятие чипсы — тоже выбор: уйдёт в query вместе с остальным по «Применить»
|
|
238
|
+
function onChipClick(item: IFilterChipItem) {
|
|
239
|
+
if (item.key) removeField(item.key);
|
|
290
240
|
}
|
|
291
241
|
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
addedValue.value = '';
|
|
295
|
-
filterValue.value = {};
|
|
296
|
-
router.push({ query: { categories: null } });
|
|
242
|
+
function onFieldUpdate({ name, value }: { name: string, value: any }) {
|
|
243
|
+
setField(name, value);
|
|
297
244
|
}
|
|
298
245
|
</script>
|
|
299
246
|
|