itube-specs 0.0.759 → 0.0.760

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.
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="s-grid-categories">
3
- <FCategoryCard
3
+ <SCategoryCard
4
4
  v-for="(item, index) in categories"
5
5
  :key="item.guid"
6
6
  :card="item"
@@ -16,5 +16,5 @@ import type { ICategoryCard } from '../../types';
16
16
  defineProps<{
17
17
  categories: Array<ICategoryCard>,
18
18
  priority?: boolean
19
- }>()
19
+ }>();
20
20
  </script>
@@ -3,7 +3,7 @@
3
3
  id="anchor"
4
4
  class="s-grid-channels"
5
5
  >
6
- <FChannelCard
6
+ <SChannelCard
7
7
  v-for="(item, index) in items"
8
8
  :key="item.guid"
9
9
  class="s-grid-channels__card"
@@ -19,5 +19,5 @@ import type { IChannelCard } from '../../types';
19
19
  defineProps<{
20
20
  items: IChannelCard[]
21
21
  priority?: boolean
22
- }>()
22
+ }>();
23
23
  </script>
@@ -3,7 +3,7 @@
3
3
  class="s-grid-models"
4
4
  :class="{'--footer': footer}"
5
5
  >
6
- <FModelCard
6
+ <SModelCard
7
7
  v-for="(item, index) in items"
8
8
  :key="`model-card-${item.guid}`"
9
9
  class="s-grid-models__item"
@@ -21,5 +21,5 @@ defineProps<{
21
21
  items: IModelCard[]
22
22
  footer?: boolean
23
23
  priority?: boolean
24
- }>()
24
+ }>();
25
25
  </script>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="s-grid-playlists">
3
- <FPlaylistCard
3
+ <SPlaylistCard
4
4
  v-for="(item, index) in items"
5
5
  :key="`user-playlist-${index}`"
6
6
  :card="item"
@@ -17,5 +17,5 @@ defineProps<{
17
17
  items: IPlaylistCard[]
18
18
  topPlaylists?: boolean
19
19
  priority?: boolean
20
- }>()
20
+ }>();
21
21
  </script>
@@ -6,7 +6,7 @@
6
6
  ]"
7
7
  >
8
8
  <slot name="grid-start"/>
9
- <FVideoCard
9
+ <SVideoCard
10
10
  v-for="(item, index) in eagerItems"
11
11
  :key="`video-${item.guid}`"
12
12
  class="s-grid-videos__card"
@@ -18,34 +18,28 @@
18
18
  :playlist="playlist"
19
19
  />
20
20
  <slot/>
21
- <NuxtLazyHydrate when-idle v-if="idleItems.length > 0" :key="`idle-${route.fullPath}`">
22
- <div class="s-grid-videos__idle">
23
- <FVideoCard
24
- v-for="item in idleItems"
25
- :key="`video-idle-${item.guid}`"
26
- class="s-grid-videos__card"
27
- :card="item"
28
- loading="lazy"
29
- :top-chips="topChips"
30
- :playlist-id="playlistId"
31
- :playlist="playlist"
32
- />
33
- </div>
34
- </NuxtLazyHydrate>
35
- <NuxtLazyHydrate when-visible v-if="lazyItems.length > 0" :key="`lazy-${route.fullPath}`">
36
- <div class="s-grid-videos__lazy">
37
- <FVideoCard
38
- v-for="item in lazyItems"
39
- :key="`video-lazy-${item.guid}`"
40
- class="s-grid-videos__card"
41
- :card="item"
42
- loading="lazy"
43
- :top-chips="topChips"
44
- :playlist-id="playlistId"
45
- :playlist="playlist"
46
- />
47
- </div>
48
- </NuxtLazyHydrate>
21
+ <LazySVideoCard
22
+ v-for="item in idleItems"
23
+ :key="`video-idle-${item.guid}`"
24
+ class="s-grid-videos__card"
25
+ :card="item"
26
+ loading="lazy"
27
+ :top-chips="topChips"
28
+ :playlist-id="playlistId"
29
+ :playlist="playlist"
30
+ hydrate-on-idle
31
+ />
32
+ <LazySVideoCard
33
+ v-for="item in lazyItems"
34
+ :key="`video-lazy-${item.guid}`"
35
+ class="s-grid-videos__card"
36
+ :card="item"
37
+ loading="lazy"
38
+ :top-chips="topChips"
39
+ :playlist-id="playlistId"
40
+ :playlist="playlist"
41
+ hydrate-on-visible
42
+ />
49
43
  <slot name="grid-end"/>
50
44
  </div>
51
45
  </template>
@@ -65,7 +59,6 @@ const props = defineProps<{
65
59
  topChips?: string[]
66
60
  }>();
67
61
 
68
- const route = useRoute();
69
62
  const isMobile = useState<boolean>('isMobile');
70
63
  const eagerCount = computed(() => isMobile.value ? 4 : 12);
71
64
 
@@ -19,7 +19,7 @@
19
19
  <span class="s-filter-page__group-title">
20
20
  {{ group?.title }}
21
21
  </span>
22
- <FSegmentedControl
22
+ <SSegmentedControl
23
23
  v-if="group?.name === 'physical'"
24
24
  :items="unitItems"
25
25
  :model-value="units"
@@ -61,7 +61,7 @@
61
61
  :label="item.title"
62
62
  @update:model-value="val => updateFilter(item.name, val)"
63
63
  />
64
- <FSegmentedControl
64
+ <SSegmentedControl
65
65
  v-if="(item.kind === 'radio') && (item.group.name === group?.name)"
66
66
  class="s-filter-page__radio"
67
67
  :items="getRadioItems(item.options)"
@@ -69,13 +69,6 @@
69
69
  :model-value="getValue(item.name)"
70
70
  @update:model-value="val => updateFilter(item.name, val)"
71
71
  />
72
- <FFilterByChips
73
- v-if="(item.kind === 'chips') && (item.group.name === group?.name)"
74
- class="f-filters-main__chips"
75
- :items="item.options"
76
- :title="item.title"
77
- :filter-name="item.name"
78
- />
79
72
  </template>
80
73
  </div>
81
74
  </div>
@@ -8,7 +8,7 @@
8
8
  />
9
9
 
10
10
  <transition>
11
- <SFilterPopup
11
+ <LazySFilterPopup
12
12
  v-if="filterOpen"
13
13
  v-model="filterOpen"
14
14
  :title="$t('filter')"
@@ -39,7 +39,7 @@
39
39
  </SButton>
40
40
  </div>
41
41
  </template>
42
- </SFilterPopup>
42
+ </LazySFilterPopup>
43
43
  </transition>
44
44
  </div>
45
45
  </template>
@@ -32,12 +32,12 @@
32
32
  ]"
33
33
  >
34
34
  <img
35
- v-if="item.name === 'nationality' && isoCode"
35
+ v-if="item.name === 'nationality_iso_codes' && isoCodes"
36
36
  class="s-info-grid__item-flag"
37
- :src="`/icons/flags/${isoCode}.svg`"
37
+ :src="`/icons/flags/${isoCodes[vIndex]}.svg`"
38
38
  width="20"
39
39
  height="14"
40
- :alt="isoCode"
40
+ :alt="isoCodes[vIndex]"
41
41
  />
42
42
  <SIcon v-else-if="item.icon" class="s-info-grid__item-icon" :name="item.icon" prefix="models" size="16"/>
43
43
  {{ value.title }}
@@ -54,7 +54,7 @@ import type { IGroupedParameter, IGroupedParameterItem } from '../../types';
54
54
 
55
55
  defineProps<{
56
56
  groups: IGroupedParameter[]
57
- isoCode?: string
57
+ isoCodes?: string
58
58
  }>();
59
59
 
60
60
  const PLAIN_GROUP_ITEM_NAMES = ['turnons'];
@@ -46,6 +46,7 @@
46
46
  <p
47
47
  v-if="activeCategory.text"
48
48
  class="s-report__form-text"
49
+ :class="{'--error': errorRadio && !hasReasonHeading}"
49
50
  >{{ t(`report_form.${activeCategory.text}`) }}
50
51
  </p>
51
52
  <p
@@ -70,6 +71,12 @@
70
71
  :key="`${subItem.label}-${subIndex}`"
71
72
  :class="[{'--wide': subItem.wide},subItem.marginClass]"
72
73
  >
74
+ <p
75
+ v-if="subItem.type === 'heading'"
76
+ class="s-report__form-text"
77
+ :class="{'--error': errorRadio}"
78
+ >{{ t(`report_form.${subItem.text}`) }}
79
+ </p>
73
80
  <SInput
74
81
  v-if="['text', 'tel', 'textarea'].includes(subItem.type)"
75
82
  v-model="form.data[subItem.value] as string"
@@ -96,8 +103,6 @@
96
103
  :value="subItem.value"
97
104
  :label="t(`report_form.${subItem.text}`)"
98
105
  :required="subItem.required"
99
- :error="errorRadio"
100
- @update:error="(val: boolean) => errorRadio = val"
101
106
  />
102
107
  </div>
103
108
  </form>
@@ -122,7 +127,7 @@
122
127
  </template>
123
128
 
124
129
  <script setup lang="ts">
125
- import { reportFormsScheme } from '../../lib/report-forms-scheme';
130
+ import { reportFormsScheme } from '../../lib';
126
131
  import { EReportFormsSubjects, validateEmail, validatePhone } from '../../runtime';
127
132
  import type { InputTypes, IReportForm, IReportRequest } from '../../types';
128
133
 
@@ -172,12 +177,17 @@ watch(isReportPopupOpen, (val) => {
172
177
  }
173
178
  });
174
179
 
180
+ watch(reasonValue, (val) => {
181
+ if (val) errorRadio.value = false;
182
+ });
183
+
175
184
  const videoGuid = computed(() => reportedVideoCard.value.guid);
176
185
 
177
186
  const loading = ref(false);
178
187
 
179
188
  const activeCategory = computed(() => reportFormsScheme.find((subItem) => subItem.subject === activeStep.value))
180
189
  const requiredFields = computed(() => activeCategory.value?.items?.filter(item => item.required).map(item => item.value));
190
+ const hasReasonHeading = computed(() => activeCategory.value?.items?.some(item => item.type === 'heading'));
181
191
 
182
192
  const { showSuccess, showError, resetSnackbar } = useSnackbar();
183
193
 
@@ -81,6 +81,29 @@ const fullUrl = computed(() =>
81
81
  props.url ? origin.value + props.url : origin.value + route.fullPath
82
82
  );
83
83
 
84
+ const isMobile = useState<boolean>('isMobile');
85
+
86
+ function canNativeShare() {
87
+ return typeof navigator !== 'undefined' && !!navigator.share && isMobile.value;
88
+ }
89
+
90
+ function nativeShare() {
91
+ navigator.share({
92
+ title: document.title,
93
+ text: document.title,
94
+ url: fullUrl.value,
95
+ }).catch((err) => {
96
+ console.warn('Share failed:', err);
97
+ });
98
+ }
99
+
100
+ watch(() => props.modelValue, (val) => {
101
+ if (val && canNativeShare()) {
102
+ emit('update:modelValue', false);
103
+ nativeShare();
104
+ }
105
+ });
106
+
84
107
  function copyUrl() {
85
108
  if (!process.client) return;
86
109
  navigator.clipboard.writeText(fullUrl.value).then(() => {
@@ -119,4 +142,4 @@ const buttons = computed(() => {
119
142
  },
120
143
  ]
121
144
  })
122
- </script>
145
+ </script>
@@ -14,7 +14,7 @@ import type { IChipsItem } from '../types';
14
14
  * @param thirdText - третий подставляемый текст
15
15
  * @param fourthText - четвёртый подставляемый текст
16
16
  */
17
- export function useMeta(t, page: string, brandName: string, sortOptions?: IChipsItem[], text?: Ref<string>, secondText?: Ref<string>, thirdText?: Ref<string>, fourthText?: Ref<string>) {
17
+ export function useMeta(t: (key: string, params?: Record<string, unknown>) => string, page: string, brandName: string, sortOptions?: IChipsItem[], text?: Ref<string>, secondText?: Ref<string>, thirdText?: Ref<string>, fourthText?: Ref<string>) {
18
18
  const route = useRoute();
19
19
 
20
20
  const sortType = computed(() => {
@@ -48,12 +48,20 @@ export function useMeta(t, page: string, brandName: string, sortOptions?: IChips
48
48
  const metaTitle = computed(() => getPath('title'));
49
49
  const h1 = computed(() => getPath('h1'));
50
50
 
51
- const meta = computed(() => ({
52
- title: metaTitle.value,
53
- meta: [
54
- { name: 'description', content: getPath('meta_description') },
55
- ],
56
- }));
51
+ const meta = computed(() => {
52
+ const description = getPath('meta_description');
53
+
54
+ return {
55
+ title: metaTitle.value,
56
+ meta: [
57
+ { name: 'description', content: description },
58
+ { property: 'og:title', content: metaTitle.value },
59
+ { property: 'og:description', content: description },
60
+ { name: 'twitter:title', content: metaTitle.value },
61
+ { name: 'twitter:description', content: description },
62
+ ],
63
+ };
64
+ });
57
65
 
58
66
  return {
59
67
  meta,
@@ -1,14 +1,8 @@
1
- import type { IChipsItem, IModelFilter, IModelFilterOptions } from '../types'
2
- import type { LocationQuery } from '#vue-router'
3
- import { getMonth } from '../runtime'
1
+ import type { IChipsItem, IModelFilter, IModelFilterOptions } from '../types';
2
+ import type { LocationQuery } from '#vue-router';
3
+ import { getMonth } from '../runtime';
4
4
  import type { Ref } from 'vue';
5
5
 
6
- /**
7
- * Формирует вычисляемый список чипсов из активных filter_* параметров в query маршрута.
8
- * @param filters - список схем фильтров модели
9
- * @param route - текущий маршрут с query
10
- * @param t - функция перевода (i18n)
11
- */
12
6
  export function useFilterChipsItems(
13
7
  filters: Ref<IModelFilter[]>,
14
8
  route: { query: LocationQuery },
@@ -21,52 +15,58 @@ export function useFilterChipsItems(
21
15
  item
22
16
  .replace('filter_', '')
23
17
  .replace(/_/g, ' ')
24
- )
18
+ );
25
19
 
26
20
  const groups = Object.values(
27
21
  queryItems.reduce((acc: Record<string, string[]>, str: string) => {
28
- const parts = str.split(' ')
29
- const last = parts.at(-1)
22
+ const parts = str.split(' ');
23
+ const last = parts.at(-1);
30
24
 
31
25
  const key =
32
26
  last === 'from' || last === 'to'
33
27
  ? parts.slice(0, -1).join(' ')
34
- : str
28
+ : str;
35
29
 
36
- acc[key] ??= []
30
+ acc[key] ??= [];
37
31
 
38
- if (last === 'from') acc[key].unshift(str)
39
- else acc[key].push(str)
32
+ if (last === 'from') acc[key].unshift(str);
33
+ else acc[key].push(str);
40
34
 
41
- return acc
35
+ return acc;
42
36
  }, {})
43
- )
37
+ );
38
+
39
+ const findFilter = (items: string[]) => {
40
+ const key = items[0].replace(' from', '').replace(' to', '');
41
+ return filters.value.find(f => f.name.replace(/_/g, ' ') === key);
42
+ };
44
43
 
45
44
  const getValue = (item: string, index: number) => {
46
45
  const filter = filters.value.find(filter =>
47
46
  filter.name ===
48
47
  item
49
- .replace(/ /g, '_')
48
+ .replace(/ /g,
49
+ '_')
50
50
  .replace('_from', '')
51
51
  .replace('_to', '')
52
- )
52
+ );
53
53
 
54
54
  const defaultValue =
55
55
  index === 0
56
56
  ? filter?.options[0]
57
- : filter?.options.at(-1)
57
+ : filter?.options.at(-1);
58
58
 
59
- const value = route.query[`filter_${item.replace(/ /g, '_')}`]
59
+ const value = route.query[`filter_${item.replace(/ /g, '_')}`];
60
60
 
61
61
  if (item.includes('month')) {
62
62
  return getMonth(
63
63
  t,
64
64
  Number(value ?? defaultValue?.name) - 1
65
- )
65
+ );
66
66
  }
67
67
 
68
68
  if (!isNaN(Number(value))) {
69
- return value
69
+ return value;
70
70
  }
71
71
 
72
72
  return (
@@ -75,31 +75,35 @@ export function useFilterChipsItems(
75
75
  option.name === value
76
76
  )?.title ??
77
77
  defaultValue?.title
78
- )
79
- }
78
+ );
79
+ };
80
80
 
81
81
  const getValueText = (items: string[]) =>
82
- [...new Set(items.map(getValue))].join(' - ')
83
-
84
- const chipsTitle = (items: string[]) => {
85
- const key = items[0].replace(' from', '')
86
- const filter = filters.value.find(
87
- f => f.name.replace(/_/g, ' ') === key
88
- )
89
-
90
- const text = `${filter?.title}: ${getValueText(items)}`
91
- return text.length > 30 ? getValueText(items) : text
92
- }
93
-
94
- return groups.map(item => ({
95
- title: chipsTitle(item),
96
- value: item.map(
97
- sub => `filter_${sub.replace(/ /g, '_')}`
98
- ),
99
- }))
100
- })
82
+ [...new Set(items.map(getValue).filter(v => v !== undefined && v !== null && v !== ''))].join(' - ');
83
+
84
+ const chipsTitle = (items: string[], filter: IModelFilter) => {
85
+ const valueText = getValueText(items);
86
+ const text = `${filter.title}: ${valueText}`;
87
+ return text.length > 30 ? valueText : text;
88
+ };
89
+
90
+ return groups
91
+ .map(item => {
92
+ const filter = findFilter(item);
93
+ if (!filter) return null;
94
+ const valueText = getValueText(item);
95
+ if (!valueText) return null;
96
+ return {
97
+ title: chipsTitle(item, filter),
98
+ value: item.map(
99
+ sub => `filter_${sub.replace(/ /g, '_')}`
100
+ ),
101
+ };
102
+ })
103
+ .filter((c): c is IChipsItem => c !== null);
104
+ });
101
105
 
102
106
  return {
103
107
  chipsItems,
104
- }
108
+ };
105
109
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.759",
4
+ "version": "0.0.760",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -1,96 +0,0 @@
1
- <template>
2
- <div class="s-playlist-like">
3
- <button
4
- class="s-playlist-like__control"
5
- :class="{'--active': isLiked}"
6
- type="button"
7
- :title="t('like')"
8
- @click="onReactionClick('like')"
9
- >
10
- <SIcon class="s-playlist-like__control-icon" name="thumbs-up" size="16"/>
11
- {{ formatNumber(Number(resultLikes)) }}
12
- </button>
13
- <div
14
- v-if="bar"
15
- class="s-playlist-like__bar"
16
- >
17
- <div
18
- class="s-playlist-like__bar-likes"
19
- :style="{ width: likePercent + '%' }"
20
- />
21
- <div
22
- class="s-playlist-like__bar-dislikes"
23
- :style="{ width: dislikePercent + '%' }"
24
- />
25
- </div>
26
- <button
27
- class="s-playlist-like__control"
28
- :class="{'--active': isDisliked}"
29
- type="button"
30
- :title="t('dislike')"
31
- @click="onReactionClick('dislike')"
32
- >
33
- <SIcon class="s-playlist-like__control-icon" name="thumbs-down" size="16"/>
34
- {{ formatNumber(Number(resultDislikes)) }}
35
- </button>
36
- </div>
37
- </template>
38
-
39
- <script setup lang="ts">
40
- import { formatNumber } from '../../runtime';
41
-
42
- const { t } = useI18n();
43
-
44
- const props = defineProps<{
45
- playlistId: string
46
- likes: number
47
- dislikes: number
48
- bar?: boolean
49
- }>()
50
-
51
- const emit = defineEmits<{
52
- (eventName: 'like', eventValue: string): void
53
- (eventName: 'dislike', eventValue: string): void
54
- }>()
55
-
56
- const likeStatus = ref<'like' | 'dislike' | null>(null);
57
- const isLiked = computed(() => likeStatus.value === 'like');
58
- const isDisliked = computed(() => likeStatus.value === 'dislike');
59
- const resultLikes = ref(props.likes);
60
- const resultDislikes = ref(props.dislikes);
61
-
62
- onMounted(() => {
63
- const stored = localStorage.getItem(props.playlistId);
64
- if (stored === 'like' || stored === 'dislike') {
65
- likeStatus.value = stored;
66
- if (stored === 'like') resultLikes.value += 1;
67
- if (stored === 'dislike') resultDislikes.value += 1;
68
- }
69
- });
70
-
71
- async function onReactionClick(type: 'like' | 'dislike') {
72
- if (likeStatus.value !== type) {
73
- likeStatus.value = type;
74
- localStorage.setItem(props.playlistId, type);
75
-
76
- const isLike = type === 'like';
77
-
78
- if (isLike) {
79
- if (resultLikes.value < 1000) resultLikes.value += 1;
80
- if (resultDislikes.value > 0) resultDislikes.value -= 1;
81
- emit('like', props.playlistId);
82
- } else {
83
- if (resultDislikes.value < 1000) resultDislikes.value += 1;
84
- if (resultLikes.value > 0) resultLikes.value -= 1;
85
- emit('dislike', props.playlistId);
86
- }
87
- }
88
- }
89
-
90
- const likes = computed(() => resultLikes.value || 0);
91
- const dislikes = computed(() => resultDislikes.value || 0);
92
- const total = computed(() => likes.value + dislikes.value || 1);
93
-
94
- const likePercent = computed(() => (likes.value / total.value) * 100);
95
- const dislikePercent = computed(() => (dislikes.value / total.value) * 100);
96
- </script>
@@ -1,57 +0,0 @@
1
- <template>
2
- <div class="s-playlist-new" :class="{'_loading': loadingPostPlaylist}">
3
- <SInput
4
- v-model="newTitleValue"
5
- :label="$t('title')"
6
- />
7
- <SPlaylistPrivateToggle v-model="isPrivate" />
8
- <div class="s-playlist-new__buttons">
9
- <SButton
10
- wide
11
- theme="secondary"
12
- @click="closePopup"
13
- >{{ $t('cancel') }}
14
- </SButton>
15
- <SButton
16
- wide
17
- theme="primary"
18
- @click="onSaveClick"
19
- >{{ $t('save') }}
20
- </SButton>
21
- </div>
22
- </div>
23
- </template>
24
-
25
- <script setup lang="ts">
26
- import { EPlaylistType } from '../../runtime';
27
- import type { IPlaylistShort } from '../../types';
28
-
29
- defineProps<{
30
- loadingPostPlaylist: boolean
31
- }>()
32
-
33
- const emit = defineEmits<{
34
- (eventName: 'close'): void
35
- (eventName: 'post', eventValue: IPlaylistShort): void
36
- }>()
37
-
38
- const { closePlaylistAdd } = usePlaylistAdd();
39
- const newTitleValue = ref('');
40
- const isPrivate = ref(false);
41
-
42
- function closePopup() {
43
- closePlaylistAdd();
44
- emit('close');
45
- }
46
-
47
- async function onSaveClick() {
48
- try {
49
- emit('post', {
50
- name: newTitleValue.value,
51
- type: isPrivate.value ? EPlaylistType.Private : EPlaylistType.Public
52
- });
53
- } catch(error) {
54
- console.log(error, 'error create playlist');
55
- }
56
- }
57
- </script>