itube-specs 0.0.195 → 0.0.205

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.
@@ -44,7 +44,7 @@
44
44
  </template>
45
45
 
46
46
  <script setup lang="ts">
47
- import { IChipsItem } from '@arkadykid/test/types';
47
+ import type { IChipsItem } from '../../types';
48
48
 
49
49
  const props = defineProps<{
50
50
  items?: IChipsItem[];
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <FButton
3
+ class="f-filter-button"
4
+ :size="size"
5
+ :theme="theme"
6
+ aria-label="Filter"
7
+ :title="t('filter')"
8
+ @click="emit('update:modelValue', true)"
9
+ >
10
+ <FIcon name="adjustments-horizontal" size="16"/>
11
+ <span class="_from-sm"
12
+ >{{ buttonName }}
13
+ </span>
14
+ <FIcon
15
+ class="f-filter-button__icon _to-sm"
16
+ name="chevron-down"
17
+ size="16"
18
+ :class="{'--open': modelValue}"
19
+ />
20
+ <FIcon
21
+ v-if="count > 0"
22
+ class="f-filter-button__reset _from-sm"
23
+ name="close"
24
+ size="16"
25
+ aria-label="reset-filter"
26
+ role="button"
27
+ @click.stop="resetFilter"
28
+ />
29
+ <span
30
+ v-if="count > 0"
31
+ class="f-filter-button__mobile-count _to-sm"
32
+ >{{ mobileCount }}</span>
33
+ </FButton>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import type { ButtonSizes, ButtonThemes } from '~';
38
+
39
+ const props = defineProps<{
40
+ count: number
41
+ modelValue: boolean
42
+ size?: ButtonSizes
43
+ theme?: ButtonThemes
44
+ }>()
45
+
46
+ const emit = defineEmits<{
47
+ (eventName: 'update:modelValue', value: Boolean): void
48
+ }>()
49
+
50
+ const { t } = useI18n()
51
+
52
+ const buttonName = computed(() => {
53
+ return props.count > 0 ? t('plural.item', props.count) : t('filter');
54
+ })
55
+
56
+ function resetFilter() {
57
+ router.push({
58
+ query: {}
59
+ })
60
+ }
61
+
62
+ const mobileCount = computed(() => props.count > 9 ? '9+' : props.count);
63
+
64
+ const router = useRouter()
65
+ </script>
66
+
67
+ <style scoped lang="scss">
68
+ </style>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <FChipsPanel v-if="items.length > 0">
3
+ <FChips
4
+ v-for="(item, index) in items"
5
+ class="f-filter-chips"
6
+ with-close
7
+ :index="`f-filter-page-chips${index}`"
8
+ bright
9
+ :item="item"
10
+ @click="onChipsClick(item)"
11
+ ></FChips>
12
+ </FChipsPanel>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import type { IChipsItem } from '../../types';
17
+
18
+ defineProps<{
19
+ items: IChipsItem[]
20
+ }>();
21
+
22
+ const emit = defineEmits<{
23
+ (eventName: 'click', eventValue: IChipsItem): void
24
+ }>()
25
+
26
+ function onChipsClick(item: IChipsItem) {
27
+ emit('click', item)
28
+ }
29
+ </script>
30
+
31
+ <style scoped lang="scss">
32
+ </style>
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <FFilterChips
3
+ :items="chipsItems"
4
+ @click="(e: IChipsItem)=> onChipsClick(e)"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import type { IChipsItem, IModelFilter } from '../../types';
10
+ import type { LocationQueryValue } from '#vue-router';
11
+ import { useRoute, useRouter } from '#vue-router';
12
+ import { getMonth } from '../../runtime';
13
+
14
+ const props = defineProps<{
15
+ filters: IModelFilter[]
16
+ }>();
17
+
18
+ const { t } = useI18n();
19
+ const route = useRoute();
20
+ const router = useRouter();
21
+
22
+ const chipsItems = computed(() => {
23
+ const queryItems = Object.keys(route.query)
24
+ .filter(item => item.startsWith('filter'))
25
+ .map(item => item.replace('filter_', '')
26
+ .replace(/_/g, ' '));
27
+
28
+ const groups = Object.values(
29
+ queryItems.reduce((acc: Record<string, string[]>, str: string) => {
30
+ const parts = str.split(' ');
31
+ const last = parts.at(-1);
32
+ const key = ['from', 'to'].includes(last)
33
+ ? parts.slice(0, -1).join(' ')
34
+ : str;
35
+
36
+ acc[ key ] ??= [];
37
+
38
+ if (last === 'from') acc[ key ].unshift(str);
39
+ else acc[ key ].push(str);
40
+
41
+ return acc;
42
+ }, {})
43
+ )
44
+
45
+ const title = (item: string, index: number) => {
46
+ const filter = props.filters.find(filter => {
47
+ return filter.name === `${item.replace(/ /g, '_')
48
+ .replace('_from', '')
49
+ .replace('_to', '')
50
+ }`
51
+ });
52
+ const defaultValue = index === 0 ? filter?.options[ 0 ] : filter?.options[ filter.options.length - 1 ]
53
+ const value: LocationQueryValue | LocationQueryValue[] = route.query[ `filter_${item.replace(/ /g, '_')}` ];
54
+ if (item.split(' ').some(item => item === 'month')) {
55
+ return getMonth(t, Number(value) - 1 || Number(defaultValue) - 1)
56
+ }
57
+ return value || Number(defaultValue) - 1;
58
+ }
59
+
60
+ const text = (item: string[]) => [...new Set(item.map((subItem, index) => title(subItem, index)))].join(' - ');
61
+
62
+ const chipsTitle = ((item: string[]) => {
63
+ const key = Array.isArray(item) ? item[0].replace(' from', '') : item;
64
+ const convertedKey = props.filters.find(item => item.name.replace(/_/g, ' ') === key)?.title;
65
+ const textValue = `${convertedKey}: ${text(item)}`;
66
+ return textValue.length > 30 ? text(item) : textValue;
67
+ })
68
+
69
+ return groups.map((item) => ({
70
+ title: chipsTitle(item),
71
+ value: item.map(subItem => `filter_${subItem.replace(/ /g, '_')}`),
72
+ }));
73
+ })
74
+
75
+ function onChipsClick(item: IChipsItem) {
76
+ const query = { ...route.query };
77
+ if (Array.isArray(item.value) && item.value.length > 1) {
78
+ item.value.forEach((text: string) => {
79
+ delete query[ `${text}` ]
80
+ })
81
+ } else {
82
+ delete query[ `${item.value}` ]
83
+ }
84
+ router.replace({ query });
85
+ }
86
+ </script>
87
+
88
+ <style scoped lang="scss">
89
+ </style>
@@ -0,0 +1,229 @@
1
+ <template>
2
+ <div class="f-filter-page">
3
+ <div class="f-filter-page__items-wrapper">
4
+ <details
5
+ v-for="(group, index) in groups"
6
+ :key="`filter-group-${index}`"
7
+ class="f-filter-page__group"
8
+ :open="index === 0 || activeGroup === index + 1"
9
+ >
10
+ <summary
11
+ v-if="filters.length > 0"
12
+ class="f-filter-page__group-title"
13
+ >
14
+ {{ group?.title }}
15
+ <FIcon
16
+ class="f-filter-page__group-title-icon"
17
+ size="32"
18
+ name="chevron-down"
19
+ />
20
+ </summary>
21
+ <div class="f-filter-page__items">
22
+ <template v-for="(item, subIndex) in filters">
23
+ <ClientOnly>
24
+ <FSlider
25
+ v-if="(item.kind === 'range') && (item.group.name === group?.name)"
26
+ :min="getRangeInfo(item.options)?.min"
27
+ :max="getRangeInfo(item.options)?.max"
28
+ :step="getRangeInfo(item.options)?.step"
29
+ :model-value="[
30
+ minRangeValue(item),
31
+ maxRangeValue(item)
32
+ ]"
33
+ :title="item.title"
34
+ :key="`model-filter-range-${subIndex}`"
35
+ :active="isActiveSelect(item.name)"
36
+ :format="formatterSlider(item.options)"
37
+ :between="betweenText(item.options, getRangeInfo(item.options)?.min, getRangeInfo(item.options)?.max)"
38
+ @update:model-value="val => updateRangeFilter(item, val)"
39
+ />
40
+ </ClientOnly>
41
+ <FSelect
42
+ v-if="(item.kind === 'select') && (item.group.name === group?.name)"
43
+ :name="item.name"
44
+ :model-value="getSelectValue(item.name)"
45
+ :items="selectItems(item)"
46
+ :count="getCount(item)"
47
+ :active="isActiveSelect(item.name)"
48
+ :key="`model-filter-select-${subIndex}`"
49
+ :label="item.title"
50
+ @update:model-value="val => updateSelectFilter(item.name, val)"
51
+ />
52
+ </template>
53
+ </div>
54
+ </details>
55
+ </div>
56
+ <div class="f-filter-page__footer _from-sm">
57
+ <p class="f-filter-page__footer-results">
58
+ <FIcon name="filter" size="24"/>
59
+ <span class="f-filter-page__footer-text">{{ `${count} ${t('results_found')}` }}</span>
60
+ </p>
61
+ <div class="f-filter-page__footer-buttons">
62
+ <FButton
63
+ wide
64
+ class="f-filter-page__reset"
65
+ :disabled="!isFiltered"
66
+ @click="onResetClick"
67
+ theme="secondary"
68
+ >{{ t('reset_all') }}
69
+ </FButton>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import type { IModelFilter, IModelFilterOptions } from '../../types';
77
+
78
+ import { useRoute, useRouter } from 'vue-router';
79
+ import { getMonth } from '../../runtime';
80
+
81
+ const props = defineProps<{
82
+ filters: IModelFilter[]
83
+ count: number
84
+ }>();
85
+
86
+ const { t } = useI18n();
87
+ const route = useRoute();
88
+ const router = useRouter();
89
+
90
+ const activeGroup = computed(() => Number(route.query[ 'group' ]));
91
+
92
+ const groups = computed(() => {
93
+ const uniqueNames = [...new Set(props.filters.map(item => item.group.title))];
94
+ return uniqueNames.map(name => {
95
+ return props.filters.find(filter => filter.group.title === name)?.group;
96
+ }).sort((a, b) => a?.order - b?.order)
97
+ });
98
+
99
+ const isFiltered = computed(() => Object.keys(route.query).some(item => item.startsWith('filter')));
100
+
101
+ function selectItems(item: IModelFilter) {
102
+ return [
103
+ ...item.options.map(item => ({
104
+ value: item.name,
105
+ title: `${item.title} (${item.quantity})`,
106
+ }))
107
+ ]
108
+ }
109
+
110
+ function onResetClick() {
111
+ router.replace({ query: {} });
112
+ }
113
+
114
+ function getRangeInfo(options: { name: string; quantity: number; title: string }[]): {
115
+ min: number,
116
+ step: number,
117
+ max: number
118
+ } | null {
119
+ const numericValues = options
120
+ .map(opt => Number(opt.name))
121
+ .filter(v => !isNaN(v))
122
+ .sort((a, b) => a - b);
123
+
124
+ if (numericValues.length === 0) {
125
+ return null;
126
+ }
127
+
128
+ const min = numericValues[ 0 ] || 0;
129
+ const max = numericValues[ numericValues.length - 1 ] || 100;
130
+ const step = numericValues.length > 1 ? 1 : 0;
131
+
132
+ return { min, step, max };
133
+ }
134
+
135
+ function updateRangeFilter(item: IModelFilter, val: string[] | number[]) {
136
+ const rangeInfo = getRangeInfo(item.options);
137
+ if (!rangeInfo) return;
138
+ const [from, to] = val;
139
+ const query = { ...route.query };
140
+
141
+ query[ `filter_${item.name}_from` ] = from;
142
+ query[ `filter_${item.name}_to` ] = to;
143
+
144
+ if (from === rangeInfo.min && to === rangeInfo.max) {
145
+ delete query[ `filter_${item.name}_from` ];
146
+ delete query[ `filter_${item.name}_to` ];
147
+ }
148
+
149
+ router.replace({ query });
150
+ }
151
+
152
+ function updateSelectFilter(name: string, val: any) {
153
+ const query = { ...route.query };
154
+
155
+ if (!val || val === 'all') {
156
+ delete query[ `filter_${name}` ];
157
+ } else {
158
+ query[ `filter_${name}` ] = val;
159
+ }
160
+
161
+ router.replace({ query });
162
+ }
163
+
164
+ function isActiveSelect(name: string) {
165
+ const baseName = name.replace(/_(from|to)$/, '');
166
+ return Object.keys(route.query).some(key => key.startsWith(`filter_${baseName}`));
167
+ }
168
+
169
+ function getSelectValue(name: string) {
170
+ const value = route.query[ `filter_${name}` ];
171
+ if (value) {
172
+ return value;
173
+ }
174
+ const filter = props.filters.find(f => f.name === name);
175
+ if (filter && filter.options.length > 0) {
176
+ return filter.options[ 0 ].name;
177
+ }
178
+ return null;
179
+ }
180
+
181
+ function getRangeValue(name: string, defaultValue: number | undefined) {
182
+ const value = route.query[ `filter_${name}` ];
183
+ if (value !== undefined) {
184
+ return Number(value);
185
+ } else {
186
+ return defaultValue;
187
+ }
188
+ }
189
+
190
+ function getCount(item: IModelFilter) {
191
+ const selectedValue = getSelectValue(item.name);
192
+ const targetValue = selectedValue !== null && selectedValue !== undefined && selectedValue !== '' ? selectedValue : 'all';
193
+ return props.filters.find(subitem => subitem.name === item.name)?.options.find(subitem => subitem.name === targetValue)?.quantity;
194
+ }
195
+
196
+ function minRangeValue(item: IModelFilter) {
197
+ return getRangeValue(`${item.name}_from`, getRangeInfo(item.options)?.min)
198
+ }
199
+
200
+ function maxRangeValue(item: IModelFilter) {
201
+ return getRangeValue(`${item.name}_to`, getRangeInfo(item.options)?.max)
202
+ }
203
+
204
+ function isSliderHasTitle(options: IModelFilterOptions[] | undefined): boolean | undefined {
205
+ return options && options[ 0 ] && isNaN(Number(options[ 0 ].title));
206
+ }
207
+
208
+ function formatterSlider(options: IModelFilterOptions[]) {
209
+ return (event: number) => {
210
+ if (isSliderHasTitle(options)) {
211
+ return getMonth(t, Math.round(event - 1));
212
+ } else {
213
+ return Math.round(event);
214
+ }
215
+ }
216
+ }
217
+
218
+ function betweenText(options: IModelFilterOptions[], min: number | undefined, max: number | undefined): string[] | undefined {
219
+ if (isSliderHasTitle(options)) {
220
+ const getText = (number: number | undefined): string | undefined => {
221
+ return options.find((item: IModelFilterOptions) => Number(item.name) === number)?.title;
222
+ }
223
+ return [getText(min) || '', getText(max) || ''];
224
+ }
225
+ }
226
+ </script>
227
+
228
+ <style scoped lang="scss">
229
+ </style>
@@ -0,0 +1,166 @@
1
+ <template>
2
+ <FPopup
3
+ v-model="isOpen"
4
+ v-if="isOpen"
5
+ sheet
6
+ @close="closePopup"
7
+ >
8
+ <template #title>{{ filterTitle }}</template>
9
+
10
+ <template #fixedContent>
11
+ <div class="f-filter-popup__main-filters">
12
+ <FSelect
13
+ name="duration"
14
+ v-model="durationValue"
15
+ :items="durationItems"
16
+ :placeholder="t('duration')"
17
+ icon="tape"
18
+ />
19
+ <FSelect
20
+ name="added"
21
+ v-model="addedValue"
22
+ :items="addedItems"
23
+ :placeholder="t('added')"
24
+ icon="clock"
25
+ />
26
+ </div>
27
+ </template>
28
+
29
+ <div class="f-filter-popup__filters">
30
+ <template
31
+ v-for="(item, index) in scheme"
32
+ :key="`scheme-${index}`"
33
+ >
34
+ <FInput
35
+ v-if="item.type === 'input'"
36
+ v-model="filterValue[item.title]"
37
+ :label="item?.label"
38
+ />
39
+ <FSelect
40
+ v-if="item.type === 'select'"
41
+ :name="item.title"
42
+ v-model="filterValue[item.title]"
43
+ :items="item.items"
44
+ :label="item?.label"
45
+ :placeholder="item.placeholder"
46
+ />
47
+ </template>
48
+ </div>
49
+
50
+ <template #footer>
51
+ <div class="f-filter-popup__buttons">
52
+ <FButton
53
+ theme="secondary"
54
+ wide
55
+ :disabled="!isFiltered"
56
+ @click="reset"
57
+ >{{ t('reset_all') }}
58
+ </FButton>
59
+ <FButton
60
+ wide
61
+ @click="saveResult"
62
+ >{{ t('apply') }}
63
+ </FButton>
64
+ </div>
65
+ </template>
66
+ </FPopup>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import type { FilterSchemeType, ISelectItem } from '../../types';
71
+
72
+ const { t } = useI18n()
73
+
74
+ const props = defineProps<{
75
+ addedItems: ISelectItem[]
76
+ durationItems: ISelectItem[]
77
+ modelValue: boolean
78
+ scheme: FilterSchemeType
79
+ count: number
80
+ }>()
81
+
82
+ const durationValue = ref('');
83
+ const addedValue = ref('');
84
+ const filterValue = ref({} as Record<string, string>);
85
+
86
+ const route = useRoute();
87
+
88
+ const categoriesFromQuery = route.query.categories
89
+ ? (typeof route.query.categories === 'string'
90
+ ? route.query.categories.split(',')
91
+ : route.query.categories.flatMap(cat => cat.split(',')))
92
+ : [];
93
+
94
+ categoriesFromQuery.forEach(str => {
95
+ const [key, ...rest] = str.split('_');
96
+ if (key && rest.length > 0) {
97
+ filterValue.value[key] = rest.join('_');
98
+ }
99
+ });
100
+
101
+ durationValue.value = route.query['duration'] as string || '';
102
+ addedValue.value = route.query['added'] as string || '';
103
+
104
+ const emit = defineEmits<{
105
+ (eventName: 'update:modelValue', value: boolean): void
106
+ }>()
107
+
108
+ const isOpen = ref(props.modelValue)
109
+
110
+ function closePopup() {
111
+ isOpen.value = false;
112
+ emit('update:modelValue', isOpen.value)
113
+ }
114
+
115
+ const router = useRouter();
116
+
117
+ function saveResult() {
118
+ const oldQuery = { ...route.query };
119
+ delete oldQuery['page'];
120
+
121
+ const mainFiltersValue = {
122
+ ...(durationValue.value ? { duration: durationValue.value } : {}),
123
+ ...(addedValue.value ? { added: addedValue.value } : {})
124
+ };
125
+
126
+ const preserved = {
127
+ ...mainFiltersValue,
128
+ ...(oldQuery.sort ? { sort: oldQuery.sort } : {}),
129
+ ...(oldQuery.page ? { page: oldQuery.page } : {})
130
+ };
131
+
132
+ const categories = Object.entries(filterValue.value)
133
+ .filter(([, value]) => typeof value === 'string' && value.length > 0)
134
+ .map(([key, value]) => `${key}_${value}`);
135
+
136
+ const categoriesString = categories.join(',');
137
+
138
+ router.push({
139
+ query: {
140
+ ...preserved,
141
+ categories: categories.length ? categoriesString : undefined,
142
+ }
143
+ });
144
+
145
+ closePopup();
146
+ }
147
+
148
+ function reset() {
149
+ closePopup();
150
+ router.push({
151
+ query: {
152
+ categories: null,
153
+ }
154
+ });
155
+ }
156
+
157
+ const filterTitle = computed(() => {
158
+ const hasDuration = durationValue.value ? 1 : 0;
159
+ const hasAdded = addedValue.value ? 1 : 0;
160
+ const count = Object.keys(filterValue.value).length + hasAdded + hasDuration;
161
+ const filterText = count > 1 ? t('filters') : t('filter');
162
+ return count > 0 ? `${filterText}: ${count}` : t('filter')
163
+ })
164
+
165
+ const isFiltered = computed(() => !!route.query['categories']);
166
+ </script>
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <FFilterChips
3
+ :items="chipsItems"
4
+ @click="(e: IChipsItem) => onChipsClick(e)"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import type { IChipsItem, ISelectItem } from '../../types';
10
+
11
+ const route = useRoute();
12
+ const router = useRouter();
13
+
14
+ const props = defineProps<{
15
+ addedItems: ISelectItem[]
16
+ durationItems: ISelectItem[]
17
+ }>()
18
+
19
+ const chipsItems = computed(() => {
20
+ const items: IChipsItem[] = [];
21
+
22
+ // categories
23
+ const categoriesParam = route.query.categories;
24
+ if (categoriesParam) {
25
+ const categories = Array.isArray(categoriesParam)
26
+ ? categoriesParam.flatMap(c => c.split(','))
27
+ : categoriesParam.split(',');
28
+ categories.forEach(cat => {
29
+ const title = cat.split('_')[1] ?? cat;
30
+ items.push({
31
+ title,
32
+ value: cat,
33
+ key: 'categories'
34
+ });
35
+ });
36
+ }
37
+
38
+ // duration
39
+ const durationParam = route.query.duration;
40
+ if (durationParam) {
41
+ const title = props.durationItems.find(item => item.value === durationParam)?.title;
42
+ items.push({
43
+ title: `${title}`,
44
+ value: String(durationParam),
45
+ key: 'duration'
46
+ });
47
+ }
48
+
49
+ // added
50
+ const addedParam = route.query.added;
51
+ if (addedParam) {
52
+ const title = props.addedItems.find(item => item.value === addedParam)?.title;
53
+ items.push({
54
+ title: `${title}`,
55
+ value: String(addedParam),
56
+ key: 'added'
57
+ });
58
+ }
59
+
60
+ return items;
61
+ });
62
+
63
+ function onChipsClick(item: IChipsItem & { key?: string }) {
64
+ const query = { ...route.query };
65
+
66
+ if (item.key === 'categories') {
67
+ const categoriesParam = route.query.categories;
68
+ if (!categoriesParam) return;
69
+
70
+ const categories = Array.isArray(categoriesParam)
71
+ ? categoriesParam.flatMap(c => c.split(','))
72
+ : categoriesParam.split(',');
73
+
74
+ const updatedCategories = categories.filter(cat => cat !== item.value);
75
+
76
+ if (updatedCategories.length > 0) {
77
+ query.categories = updatedCategories.join(',');
78
+ } else {
79
+ delete query.categories;
80
+ }
81
+ }
82
+
83
+ if (item.key === 'duration') {
84
+ delete query.duration;
85
+ }
86
+
87
+ if (item.key === 'added') {
88
+ delete query.added;
89
+ }
90
+
91
+ router.replace({ query });
92
+ }
93
+ </script>
94
+
95
+ <style scoped lang="scss">
96
+ </style>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <div class="f-filter">
3
+ <FFilterButton
4
+ :count="filterCount"
5
+ v-model="filterOpen"
6
+ :theme="buttonTheme"
7
+ :size="buttonSize"
8
+ />
9
+
10
+ <transition>
11
+ <FFilterPopup
12
+ v-if="filterOpen"
13
+ v-model="filterOpen"
14
+ :scheme="scheme"
15
+ :count="filterCount"
16
+ :duration-items="durationItems"
17
+ :added-items="addedItems"
18
+ />
19
+ </transition>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import type { ButtonSizes, ButtonThemes, FilterSchemeType } from '../../types';
25
+ import type { LocationQuery } from '#vue-router';
26
+ import type { ISelectItem } from '../../types';
27
+
28
+ const route = useRoute();
29
+ const filterOpen = ref(false);
30
+
31
+ defineProps<{
32
+ scheme: FilterSchemeType
33
+ addedItems: ISelectItem[]
34
+ durationItems: ISelectItem[]
35
+ buttonSize?: ButtonSizes
36
+ buttonTheme?: ButtonThemes
37
+ }>()
38
+
39
+ function getFilteredQuery(query: LocationQuery) {
40
+ const result: string[] = [];
41
+
42
+ const categoriesRaw = query[ 'categories' ];
43
+ if (categoriesRaw) {
44
+ if (Array.isArray(categoriesRaw)) {
45
+ categoriesRaw.forEach(item => {
46
+ if (typeof item === 'string') {
47
+ result.push(...item.split(',').filter(Boolean));
48
+ }
49
+ });
50
+ } else if (typeof categoriesRaw === 'string') {
51
+ result.push(...categoriesRaw.split(',').filter(Boolean));
52
+ }
53
+ }
54
+
55
+ ['duration', 'added'].forEach(key => {
56
+ const val = query[ key ];
57
+ if (typeof val === 'string' && val.length > 0) {
58
+ result.push(val);
59
+ }
60
+ });
61
+
62
+ return result;
63
+ }
64
+
65
+ const filterCount = ref(0);
66
+
67
+ filterCount.value = getFilteredQuery(route.query).length
68
+
69
+ watch(() => route.query, (value) => {
70
+ filterCount.value = getFilteredQuery(value).length
71
+ })
72
+ </script>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <FFilterModelChips :filters="data.filters"/>
3
+ <FFilterPage
4
+ class="f-model-filters__filters _from-sm"
5
+ :filters="data.filters"
6
+ :count="data.total"
7
+ :class="{'_loading': status === 'pending'}"
8
+ />
9
+ <transition>
10
+ <FPopup
11
+ v-if="model"
12
+ v-model="model"
13
+ sheet
14
+ class="f-model-filters__popup"
15
+ >
16
+ <template #title>{{ popupTitle }}</template>
17
+ <FFilterPage
18
+ class="f-model-filters__filters _to-sm"
19
+ :filters="data.filters"
20
+ :count="data.total"
21
+ :class="{'_loading': status === 'pending'}"
22
+ />
23
+ <template #footer>
24
+ <div class="f-model-filters__footer-buttons _to-sm">
25
+ <FButton
26
+ wide
27
+ class="f-model-filters__reset"
28
+ :disabled="!isFiltered"
29
+ @click="onResetClick"
30
+ theme="secondary"
31
+ >{{ t('reset_all') }}
32
+ </FButton>
33
+ <FButton
34
+ wide
35
+ class="_to-sm"
36
+ @click="onShowClick"
37
+ >{{ `${t('show_results')} (${data.total})` }}
38
+ </FButton>
39
+ </div>
40
+ </template>
41
+ </FPopup>
42
+ </transition>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import type { IModelCard, PaginatedResponse } from '../../types';
47
+
48
+ const props = defineProps<{
49
+ data: PaginatedResponse<IModelCard>,
50
+ status: string,
51
+ filterCount: number,
52
+ }>()
53
+
54
+ const model = defineModel<boolean>();
55
+
56
+ const { t } = useI18n();
57
+
58
+ const router = useRouter();
59
+ const route = useRoute();
60
+
61
+ function onResetClick() {
62
+ router.replace({ query: {} });
63
+ }
64
+
65
+ const emit = defineEmits<{
66
+ (eventName: 'update:modelValue', value: boolean): void
67
+ }>()
68
+
69
+ function onShowClick() {
70
+ emit('update:modelValue', false);
71
+ document.documentElement.querySelector('#anchor')?.scrollIntoView({ behavior: 'smooth' });
72
+ }
73
+
74
+ const isFiltered = computed(() => Object.keys(route.query).some(item => item.startsWith('filter')));
75
+
76
+ const popupTitle = computed(() => {
77
+ const countText = props.filterCount ? `: ${props.filterCount}` : '';
78
+ const filterText = props.filterCount > 1 ? t('filters') : t('filter');
79
+ return `${filterText}${countText}`;
80
+ })
81
+ </script>
82
+
83
+ <style scoped lang="scss">
84
+ </style>
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.195",
4
+ "version": "0.0.205",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
8
- "public": "npm version patch && npm publish --access public"
8
+ "prepublish": "npm install && npx nuxi prepare"
9
9
  },
10
10
  "exports": {
11
11
  ".": {