itube-specs 0.0.195

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.
Files changed (169) hide show
  1. package/README.md +121 -0
  2. package/components/cards/f-video-mini-card.vue +49 -0
  3. package/components/grids/f-grid-categories.vue +20 -0
  4. package/components/grids/f-grid-channels.vue +23 -0
  5. package/components/grids/f-grid-models.vue +25 -0
  6. package/components/grids/f-grid-playlists.vue +21 -0
  7. package/components/grids/f-grid-videos.vue +33 -0
  8. package/components/page-components/f-breadcrumbs.vue +44 -0
  9. package/components/page-components/f-chips-panel.vue +101 -0
  10. package/components/page-components/f-pagination.vue +206 -0
  11. package/components/page-components/f-report.vue +221 -0
  12. package/components/page-components/f-share.vue +96 -0
  13. package/components/page-components/f-sort.vue +57 -0
  14. package/components/page-components/f-videos-title.vue +20 -0
  15. package/components/ui/f-button.vue +50 -0
  16. package/components/ui/f-checkbox.vue +55 -0
  17. package/components/ui/f-chips.vue +116 -0
  18. package/components/ui/f-count.vue +12 -0
  19. package/components/ui/f-country.vue +26 -0
  20. package/components/ui/f-dropdown.vue +122 -0
  21. package/components/ui/f-icon.vue +19 -0
  22. package/components/ui/f-img.vue +46 -0
  23. package/components/ui/f-input.vue +162 -0
  24. package/components/ui/f-label.vue +20 -0
  25. package/components/ui/f-link.vue +33 -0
  26. package/components/ui/f-model-tag.vue +28 -0
  27. package/components/ui/f-notification.vue +77 -0
  28. package/components/ui/f-popup.vue +136 -0
  29. package/components/ui/f-radio.vue +56 -0
  30. package/components/ui/f-select.vue +88 -0
  31. package/components/ui/f-slider.vue +55 -0
  32. package/components/ui/f-snackbar.vue +47 -0
  33. package/components/ui/f-timestamp.vue +51 -0
  34. package/components/ui/f-toggle.vue +29 -0
  35. package/composables/use-antiadblock-domains.ts +20 -0
  36. package/composables/use-auth-popup.ts +25 -0
  37. package/composables/use-convert-query-categories.ts +7 -0
  38. package/composables/use-generate-link.ts +30 -0
  39. package/composables/use-get-pure-route-name.ts +5 -0
  40. package/composables/use-get-videos-filter-request.ts +30 -0
  41. package/composables/use-meta.ts +42 -0
  42. package/composables/use-playlist-edit.ts +36 -0
  43. package/composables/use-report-popup.ts +21 -0
  44. package/composables/use-seo-links.ts +87 -0
  45. package/composables/use-share-popup.ts +23 -0
  46. package/composables/use-snackbar.ts +52 -0
  47. package/composables/use-test-composable.ts +3 -0
  48. package/lib/alphabet-items.ts +2 -0
  49. package/lib/contact-forms-scheme.ts +98 -0
  50. package/lib/contacts/report-issue-items.ts +5 -0
  51. package/lib/contacts/report-malware-items.ts +6 -0
  52. package/lib/contacts/report-reasons-items.ts +12 -0
  53. package/lib/contacts/report-wrong-items.ts +6 -0
  54. package/lib/index.ts +7 -0
  55. package/lib/report-forms-scheme.ts +205 -0
  56. package/nuxt.config.ts +20 -0
  57. package/package.json +53 -0
  58. package/runtime/enums/async-data.ts +48 -0
  59. package/runtime/enums/auth-step.ts +5 -0
  60. package/runtime/enums/contacts-subjects.ts +7 -0
  61. package/runtime/enums/languages.ts +9 -0
  62. package/runtime/enums/niche.ts +6 -0
  63. package/runtime/enums/playlist-step.ts +5 -0
  64. package/runtime/enums/playlist-type.ts +4 -0
  65. package/runtime/enums/report-forms-subjects.ts +7 -0
  66. package/runtime/index.ts +51 -0
  67. package/runtime/utils/cleaners/clean-category-card.ts +9 -0
  68. package/runtime/utils/cleaners/clean-category-info.ts +9 -0
  69. package/runtime/utils/cleaners/clean-channel-card.ts +12 -0
  70. package/runtime/utils/cleaners/clean-channel-info.ts +13 -0
  71. package/runtime/utils/cleaners/clean-model-card.ts +9 -0
  72. package/runtime/utils/cleaners/clean-model-info.ts +11 -0
  73. package/runtime/utils/cleaners/clean-playlist-card.ts +16 -0
  74. package/runtime/utils/cleaners/clean-playlist-data.ts +15 -0
  75. package/runtime/utils/cleaners/clean-playlist-video.ts +12 -0
  76. package/runtime/utils/cleaners/clean-profile-data.ts +11 -0
  77. package/runtime/utils/cleaners/clean-user-playlists-card.ts +11 -0
  78. package/runtime/utils/cleaners/clean-video-card.ts +19 -0
  79. package/runtime/utils/cleaners/clean-video-data.ts +27 -0
  80. package/runtime/utils/compress-image.ts +27 -0
  81. package/runtime/utils/converters/convert-categories-to-chips.ts +13 -0
  82. package/runtime/utils/converters/convert-categories-to-footer.ts +11 -0
  83. package/runtime/utils/converters/convert-date-to-timestamp.ts +37 -0
  84. package/runtime/utils/converters/convert-model-card-to-chips.ts +13 -0
  85. package/runtime/utils/converters/convert-string.ts +56 -0
  86. package/runtime/utils/converters/group-categories-by-first-letter.ts +24 -0
  87. package/runtime/utils/converters/group-objects-by-first-letter.ts +16 -0
  88. package/runtime/utils/format-date.ts +12 -0
  89. package/runtime/utils/format-number.ts +12 -0
  90. package/runtime/utils/format-time-ago.ts +21 -0
  91. package/runtime/utils/get-duration.ts +17 -0
  92. package/runtime/utils/get-month.ts +22 -0
  93. package/runtime/utils/get-multiple-query.ts +26 -0
  94. package/runtime/utils/get-selected-query.ts +6 -0
  95. package/runtime/utils/is-mobile.ts +15 -0
  96. package/runtime/utils/normalize-url.ts +43 -0
  97. package/runtime/utils/on-backdrop-click.ts +5 -0
  98. package/runtime/utils/scroll-lock.ts +28 -0
  99. package/runtime/utils/server/abort-controller.ts +14 -0
  100. package/runtime/utils/server/api-helper.ts +41 -0
  101. package/runtime/utils/server/get-url-with-proxied-params.ts +6 -0
  102. package/runtime/utils/server/parse-api-error.ts +14 -0
  103. package/runtime/utils/server/server-api-helper.ts +28 -0
  104. package/runtime/utils/validate-email.ts +4 -0
  105. package/runtime/utils/validate-password.ts +3 -0
  106. package/runtime/utils/validate-phone.ts +4 -0
  107. package/runtime/utils/validate-username.ts +4 -0
  108. package/runtime/utils/video-data-add-model-icon.ts +20 -0
  109. package/runtime/utils/vtt-helper.ts +86 -0
  110. package/types/authorization-forms.d.ts +16 -0
  111. package/types/breadcrumb-item.d.ts +4 -0
  112. package/types/button-sizes.d.ts +1 -0
  113. package/types/button-themes.d.ts +1 -0
  114. package/types/card-info.d.ts +22 -0
  115. package/types/category-card.d.ts +8 -0
  116. package/types/change-email-form.d.ts +3 -0
  117. package/types/change-password-form.d.ts +4 -0
  118. package/types/channel-card.d.ts +10 -0
  119. package/types/chips-item.d.ts +8 -0
  120. package/types/contacts-form.d.ts +10 -0
  121. package/types/contacts-scheme.d.ts +14 -0
  122. package/types/country.d.ts +5 -0
  123. package/types/css-breakpoints.d.ts +1 -0
  124. package/types/filter-scheme.d.ts +37 -0
  125. package/types/fluid-player.d.ts +226 -0
  126. package/types/gender.d.ts +5 -0
  127. package/types/group-categories.d.ts +15 -0
  128. package/types/index.d.ts +59 -0
  129. package/types/input-types.d.ts +1 -0
  130. package/types/link-item.d.ts +6 -0
  131. package/types/model-card.d.ts +7 -0
  132. package/types/model-filter-payload.d.ts +4 -0
  133. package/types/model-filter.d.ts +15 -0
  134. package/types/model-group.d.ts +5 -0
  135. package/types/model-tag.d.ts +5 -0
  136. package/types/multi-suggest.d.ts +105 -0
  137. package/types/navigation-items.d.ts +10 -0
  138. package/types/paginated-response.d.ts +8 -0
  139. package/types/parameter-model.d.ts +14 -0
  140. package/types/playlist-card.d.ts +16 -0
  141. package/types/playlist-data.d.ts +15 -0
  142. package/types/playlist-info-type.d.ts +28 -0
  143. package/types/playlist-video-form.d.ts +9 -0
  144. package/types/profile-data.d.ts +9 -0
  145. package/types/raw/raw-category-card.d.ts +23 -0
  146. package/types/raw/raw-category-info.d.ts +23 -0
  147. package/types/raw/raw-channel-card.d.ts +29 -0
  148. package/types/raw/raw-channel-info.d.ts +29 -0
  149. package/types/raw/raw-model-card.d.ts +53 -0
  150. package/types/raw/raw-model-info.d.ts +54 -0
  151. package/types/raw/raw-playlist-card.d.ts +27 -0
  152. package/types/raw/raw-playlist-data.d.ts +29 -0
  153. package/types/raw/raw-playlist-user.d.ts +24 -0
  154. package/types/raw/raw-playlist-video.d.ts +18 -0
  155. package/types/raw/raw-profile-data.d.ts +22 -0
  156. package/types/raw/raw-video-card.d.ts +78 -0
  157. package/types/raw/raw-video-data.d.ts +53 -0
  158. package/types/recovery-password-form.d.ts +4 -0
  159. package/types/related-phrases.d.ts +6 -0
  160. package/types/report-form.d.ts +9 -0
  161. package/types/report-scheme.d.ts +21 -0
  162. package/types/request-filters.d.ts +13 -0
  163. package/types/request-pagination.d.ts +5 -0
  164. package/types/search-top-models.d.ts +6 -0
  165. package/types/select-item.d.ts +10 -0
  166. package/types/tab-item.d.ts +6 -0
  167. package/types/thumbs-urls.d.ts +13 -0
  168. package/types/video-card.d.ts +18 -0
  169. package/types/video-data.d.ts +36 -0
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <label
3
+ class="f-select"
4
+ aria-label="select"
5
+ :class="[
6
+ {
7
+ '--wide': wide,
8
+ '--active': active
9
+ },
10
+ `--${size}`,
11
+ ]"
12
+ >
13
+ <span class="f-select__label" v-if="label">
14
+ {{ label }}
15
+ <FCount class="f-select__count" v-if="count">{{ count }}</FCount>
16
+ </span>
17
+ <span class="f-select__wrapper">
18
+ <FIcon
19
+ v-if="icon"
20
+ :name="icon"
21
+ size="24"
22
+ class="f-select__pre-icon"
23
+ />
24
+ <select
25
+ class="f-select__button"
26
+ :class="{'--icon': icon}"
27
+ :name="name"
28
+ @change="onChange"
29
+ required
30
+ >
31
+ <option
32
+ v-if="placeholder"
33
+ class="f-select__option"
34
+ value=""
35
+ selected
36
+ disabled
37
+ >{{ capitalize(placeholder) }}</option>
38
+ <option
39
+ v-for="(item, index) in items"
40
+ :value="item.value"
41
+ class="f-select__option"
42
+ :key="`f-select-${name}-${index}`"
43
+ :selected="item.value === modelValue && item.value !== placeholder"
44
+ >{{ capitalize(item.title as string || item.name as string) }}</option>
45
+ </select>
46
+ <FIcon
47
+ class="f-select__icon"
48
+ name="dropdown"
49
+ size="24"
50
+ />
51
+ </span>
52
+ </label>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import type { ISelectItem } from '../../types';
57
+ import type { LocaleObject } from '#internal-i18n-types';
58
+
59
+ withDefaults(defineProps<{
60
+ name: string
61
+ icon?: string
62
+ placeholder?: string
63
+ modelValue: string | undefined
64
+ label?: string
65
+ items: ISelectItem[] | LocaleObject[]
66
+ wide?: boolean
67
+ size?: 'm' | 's'
68
+ count?: string | number
69
+ active?: boolean
70
+ }>(), {
71
+ size: 'm'
72
+ })
73
+
74
+ const emit = defineEmits<{
75
+ (eventName: 'update:modelValue', value: Event): void
76
+ (eventName: 'change', value: Event): void
77
+ }>()
78
+
79
+ function onChange(event: Event) {
80
+ emit('update:modelValue', event.target?.value)
81
+ emit('change', event.target?.value)
82
+ }
83
+
84
+ function capitalize(text: string) {
85
+ if (!text) return '';
86
+ return text.charAt(0).toUpperCase() + text.slice(1);
87
+ }
88
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div
3
+ class="f-slider"
4
+ :class="{'--active': active}"
5
+ @touchstart="onTouchStart"
6
+ @touchmove="onTouchMove"
7
+ >
8
+ <div class="f-slider__header">
9
+ <span class="f-slider__label">
10
+ {{ title }}
11
+ </span>
12
+ <p class="f-slider__subtitle">
13
+ {{ t('between') }}:&nbsp;
14
+ <span class="f-slider__range">{{ (between && between[0]) || $attrs.min }}&#8209;{{ (between && between[1]) || $attrs.max}}</span>
15
+ </p>
16
+ </div>
17
+ <div class="f-slider__wrapper">
18
+ <Slider
19
+ v-model="model"
20
+ v-bind="$attrs"
21
+ />
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import Slider from '@vueform/slider';
28
+ import '@vueform/slider/themes/default.css';
29
+
30
+ const {t} = useI18n();
31
+ const model = defineModel<number | number[]>();
32
+
33
+ defineProps<{
34
+ title: string
35
+ active?: boolean
36
+ between?: string[]
37
+ }>()
38
+
39
+ let startX = 0
40
+
41
+ function onTouchStart(e: TouchEvent) {
42
+ startX = e.touches[0].clientX
43
+ }
44
+
45
+ function onTouchMove(e: TouchEvent) {
46
+ const deltaX = e.touches[0].clientX - startX
47
+ // Если пользователь начал свайп с левого края (например < 30px) и двигает вправо — предотвращаем навигацию
48
+ if (startX < 30 && deltaX > 10) {
49
+ e.preventDefault()
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <style scoped lang="scss">
55
+ </style>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div
3
+ class="f-snackbar"
4
+ :class="`--${snackbarTheme}`"
5
+ >
6
+ <FIcon class="f-snackbar__icon" :name="snackbarIcon" size="24"/>
7
+ {{
8
+ te(`snackbar.${convertSnackKey}`)
9
+ ? t(`snackbar.${convertSnackKey}`)
10
+ : t('snackbar.something_went_wrong')
11
+ }}
12
+ <FButton
13
+ theme="secondary"
14
+ size="s"
15
+ @click="close"
16
+ >{{ t('close') }}
17
+ </FButton>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { convertString } from '../../runtime';
23
+
24
+ const { snackbarText, snackbarIcon, snackbarTheme, snackbarTimer } = useSnackbar();
25
+
26
+ const {t, te} = useI18n();
27
+
28
+ function close() {
29
+ snackbarText.value = ''
30
+ }
31
+
32
+ const convertSnackKey = computed(() =>
33
+ convertString().toSnakeCase(snackbarText.value)
34
+ );
35
+
36
+ let timerId: ReturnType<typeof setTimeout>
37
+
38
+ onMounted(() => {
39
+ timerId = setTimeout(() => {
40
+ close()
41
+ }, snackbarTimer.value)
42
+ })
43
+
44
+ onBeforeUnmount(() => {
45
+ clearTimeout(timerId)
46
+ })
47
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <button
3
+ class="f-timestamp"
4
+ type="button"
5
+ @click="onButtonClick"
6
+ >
7
+ <span class="f-timestamp__preview-wrapper">
8
+ <span
9
+ class="f-timestamp__preview"
10
+ :style="styles"
11
+ />
12
+ </span>
13
+
14
+ <slot/>
15
+
16
+ <FIcon
17
+ class="f-timestamp__icon"
18
+ name="forward-step"
19
+ size="16"
20
+ />
21
+ </button>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import type { IVttItem } from "../../runtime/utils/vtt-helper";
26
+
27
+ const props = defineProps<{
28
+ pictureUrl: string,
29
+ coords: IVttItem['coords'];
30
+ }>();
31
+
32
+ const picture = `url(${props.pictureUrl})`;
33
+ const position = `-${props.coords.x}px -${props.coords.y}px`;
34
+ const w = props.coords.w + 'px';
35
+ const h = props.coords.h + 'px';
36
+
37
+ const emit = defineEmits<{
38
+ (eventName: 'click'): void
39
+ }>();
40
+
41
+ function onButtonClick() {
42
+ emit('click')
43
+ }
44
+
45
+ const styles = computed(() => ({
46
+ '--w': w,
47
+ '--h': h,
48
+ '--picture': picture,
49
+ '--position': position,
50
+ }))
51
+ </script>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <label
3
+ class="f-toggle"
4
+ aria-label="Toggle"
5
+ :class="[
6
+ {'f-toggle--checked': modelValue},
7
+ {'f-toggle--disabled': disabled}
8
+ ]"
9
+ >
10
+ <input
11
+ class="f-toggle__input _visually-hidden"
12
+ type="checkbox"
13
+ :checked="modelValue"
14
+ :value="modelValue"
15
+ @change="$emit('update:modelValue', $event.target?.checked)"
16
+ />
17
+ <span v-if="$slots.default" class="f-toggle__text">
18
+ <slot></slot>
19
+ {{ modelValue}}
20
+ </span>
21
+ </label>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ defineProps<{
26
+ modelValue: boolean
27
+ disabled?: boolean
28
+ }>()
29
+ </script>
@@ -0,0 +1,20 @@
1
+ export const useAntiadBlockDomains = () => {
2
+ const state = useState('antiadblock-domains', () => ({
3
+ data: null,
4
+ isUpdating: false
5
+ }));
6
+
7
+ const update = async () => {
8
+ try {
9
+ state.value.isUpdating = true;
10
+ state.value.data = await $fetch('/api/site/aabd');
11
+ } finally {
12
+ state.value.isUpdating = false;
13
+ }
14
+ };
15
+
16
+ return {
17
+ state,
18
+ update,
19
+ };
20
+ };
@@ -0,0 +1,25 @@
1
+ import { ref } from 'vue';
2
+ import { EAuthSteps } from '../runtime';
3
+
4
+ const isAuthPopupOpen = ref<boolean>(false);
5
+
6
+ const currentStep = ref(EAuthSteps.Registration);
7
+ const additionalText = ref(undefined as string | undefined);
8
+
9
+ const openAuthPopup = (step: EAuthSteps, text?: string) => {
10
+ isAuthPopupOpen.value = true;
11
+ currentStep.value = step;
12
+ additionalText.value = text;
13
+ };
14
+
15
+ const closeAuthPopup = () => {
16
+ isAuthPopupOpen.value = false;
17
+ };
18
+
19
+ export const useAuthPopup = () => ({
20
+ isAuthPopupOpen,
21
+ currentStep,
22
+ openAuthPopup,
23
+ closeAuthPopup,
24
+ additionalText,
25
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * конвертирует список категорий из фильтра страниц с видео роликами из query в одну строку, все слова с заглавной буквы
3
+ */
4
+ export function useConvertQueryCategories() {
5
+ const MAX_CATEGORIES = 3;
6
+ return String(useRoute().query[ 'categories' ])?.split(',').map(item => item.split('_')[ 1 ]).map(item => item?.charAt(0).toUpperCase() + item?.slice(1)).slice(0, MAX_CATEGORIES).join(', ');
7
+ }
@@ -0,0 +1,30 @@
1
+ import { ELanguage, ENiche } from '~/runtime';
2
+
3
+ export const useGenerateLink = () => {
4
+ const niche = useState<string>('niche');
5
+ const localePath = useLocalePath();
6
+
7
+ /**
8
+ * Генерирует путь с учетом ниши и локали
9
+ * @param path - относительный путь (например: '/videos/abc123')
10
+ * @param localeArg - локаль
11
+ * @returns string - корректный путь для <NuxtLink> и navigateTo()
12
+ */
13
+ const generateLink = (path: string, localeArg?: ELanguage): string => {
14
+ const cleanPath = path.startsWith('/') ? path : `/${path}`
15
+
16
+ const defaultNiche = useAppConfig().defaultNiche;
17
+ const projectNiches = useAppConfig().niches as ENiche[];
18
+ const isOneNiche = projectNiches.length === 1;
19
+
20
+ const withNiche =
21
+ niche.value === defaultNiche || isOneNiche
22
+ ? cleanPath
23
+ : `/${niche.value}${cleanPath}`;
24
+
25
+ const locale = localeArg || undefined;
26
+ return localePath(withNiche, locale)
27
+ }
28
+
29
+ return { generateLink }
30
+ }
@@ -0,0 +1,5 @@
1
+ import { useRoute } from 'vue-router';
2
+
3
+ export function useGetPureRouteName() {
4
+ return String(useRoute()?.name)?.replace(/___[a-zA-Z-]+$/, '') || '';
5
+ }
@@ -0,0 +1,30 @@
1
+ import { getMultipleQuery } from '../runtime';
2
+ import type { ISelectItem } from '../types';
3
+ import type { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router';
4
+
5
+ export function useGetVideosFilterRequest(route: RouteLocationNormalized | RouteLocationNormalizedLoaded, selectDurationItems: ISelectItem[], selectAddedItems: ISelectItem[]) {
6
+ // Распарсим categories из query, если есть
7
+ const rawCategories = route.query.categories
8
+ ? (typeof route.query.categories === 'string'
9
+ ? route.query.categories.split(',')
10
+ : route.query.categories.flatMap(cat => cat?.split(',')))
11
+ : [];
12
+
13
+ // Преобразуем из формата 'key_value' в просто 'value'
14
+ const parsedCategories = rawCategories
15
+ .map(cat => {
16
+ const parts = cat?.split('_');
17
+ if (parts && parts?.length > 1) {
18
+ return parts?.slice(1).join('_');
19
+ }
20
+ return cat;
21
+ });
22
+
23
+ const durationQuery = getMultipleQuery(route, selectDurationItems, 'min_duration', 'max_duration');
24
+ const addedQuery = getMultipleQuery(route, selectAddedItems, 'min_added', 'max_added');
25
+ return {
26
+ ...(parsedCategories.length > 0 ? { categories: parsedCategories } : {}),
27
+ ...durationQuery,
28
+ ...addedQuery,
29
+ }
30
+ }
@@ -0,0 +1,42 @@
1
+ import { computed, unref } from 'vue';
2
+ import type { Ref } from 'vue';
3
+ import { useRoute } from 'vue-router';
4
+
5
+ export function useMeta(t, page: string, brandName: string, hasSort?: boolean, text?: Ref<string>, secondText?: Ref<string>) {
6
+ const route = useRoute();
7
+
8
+ const sortType = computed(() => route?.query?.['sort'] || 'trending');
9
+
10
+ function getPath(key: string) {
11
+ const plainSlug = unref(text);
12
+ const secondTextValue = unref(secondText);
13
+ const pageNumber = computed(() => route.query?.['page'] ? Number(route.query['page']) : 1);
14
+ const pageText = unref(pageNumber) === 1 ? null : ` #${unref(pageNumber)}`;
15
+
16
+ return t(
17
+ `meta.${page}.${hasSort ? `${sortType.value}.` : ''}${key}`,
18
+ {
19
+ text: plainSlug,
20
+ secondText: secondTextValue,
21
+ brandName: `| ${brandName}`, //черточка '|' нужна обязательно
22
+ page: pageText,
23
+ }
24
+ );
25
+ }
26
+
27
+ const metaTitle = computed(() => getPath('title'));
28
+ const h1 = computed(() => getPath('h1'));
29
+
30
+ const meta = computed(() => ({
31
+ title: metaTitle.value,
32
+ meta: [
33
+ { name: 'description', content: getPath('meta_description') },
34
+ { name: 'keywords', content: getPath('keywords') },
35
+ ],
36
+ }));
37
+
38
+ return {
39
+ meta,
40
+ h1,
41
+ };
42
+ }
@@ -0,0 +1,36 @@
1
+ import { ref } from 'vue';
2
+ import { EPlaylistStep } from '../runtime';
3
+ import type { IVideoCard, IPlaylistCard } from '../types';
4
+
5
+ const deletedVideo = ref<IVideoCard | null | undefined>(null);
6
+ const selectedPlaylist = ref<IPlaylistCard | undefined>(undefined);
7
+ const isPlaylistEditOpen = ref<boolean>(false);
8
+
9
+ const isBackToEdit = ref(false);
10
+ const currentStep = ref(EPlaylistStep.Edit);
11
+
12
+ const openPlaylistEdit = (step: EPlaylistStep, back: boolean = false, playlist?: IPlaylistCard, videoCard?: IVideoCard) => {
13
+ selectedPlaylist.value = playlist;
14
+ isPlaylistEditOpen.value = true;
15
+ currentStep.value = step;
16
+ isBackToEdit.value = back;
17
+ deletedVideo.value = videoCard;
18
+ };
19
+
20
+ const closePlaylistEdit = () => {
21
+ if (isBackToEdit.value) {
22
+ openPlaylistEdit(EPlaylistStep.Edit, false, selectedPlaylist.value);
23
+ } else {
24
+ isPlaylistEditOpen.value = false;
25
+ isBackToEdit.value = false;
26
+ }
27
+ };
28
+
29
+ export const usePlaylistEdit = () => ({
30
+ isPlaylistEditOpen,
31
+ currentStep,
32
+ openPlaylistEdit,
33
+ closePlaylistEdit,
34
+ selectedPlaylist,
35
+ deletedVideo,
36
+ });
@@ -0,0 +1,21 @@
1
+ import { ref } from 'vue';
2
+ import type { IVideoCard } from '../types';
3
+
4
+ const isReportPopupOpen = ref<boolean>(false);
5
+ const reportedVideoCard = ref<IVideoCard>({} as IVideoCard)
6
+
7
+ const openReportPopup = (card: IVideoCard) => {
8
+ isReportPopupOpen.value = true;
9
+ reportedVideoCard.value = card;
10
+ };
11
+
12
+ const closeReportPopup = () => {
13
+ isReportPopupOpen.value = false;
14
+ };
15
+
16
+ export const useReportPopup = () => ({
17
+ isReportPopupOpen,
18
+ openReportPopup,
19
+ closeReportPopup,
20
+ reportedVideoCard,
21
+ });
@@ -0,0 +1,87 @@
1
+ type I18nMock = {
2
+ locale: { value: string }; // текущий язык
3
+ locales: { value: Array<string | { code: string }> }; // список доступных языков
4
+ };
5
+
6
+ type LocalePathMock = (path: string, localeCode: string) => string;
7
+
8
+ export const useSeoLinks = (baseDomain: string, alonePage: boolean = false, i18n: I18nMock, localePath: LocalePathMock) => {
9
+ const route = useRoute();
10
+
11
+ const allowedParams: string[] = ['page', 'sort', 'categories'];
12
+ const createUrlWithParams = (path: string) => {
13
+ const url = new URL(`${baseDomain}${path}`);
14
+
15
+ Object.entries(route.query).forEach(([key, value]) => {
16
+ // Если alonePage === true, пропускаем sort
17
+ if (allowedParams.includes(key) && !(alonePage && key === 'sort')) {
18
+ if (Array.isArray(value)) {
19
+ value.forEach(v => url.searchParams.append(key, v))
20
+ }
21
+ else if (value != null) {
22
+ url.searchParams.append(key, String(value))
23
+ }
24
+ }
25
+ });
26
+
27
+ return url.toString();
28
+ };
29
+
30
+ const canonicalUrl = computed(() => createUrlWithParams(route.path));
31
+
32
+ const alternateLinks = computed(() => {
33
+ const links = [];
34
+ const currentLocaleCode = i18n.locale.value;
35
+ const currentHreflang = currentLocaleCode === 'jp' ? 'ja' : currentLocaleCode;
36
+
37
+ links.push({
38
+ rel: 'alternate',
39
+ hreflang: currentHreflang,
40
+ href: createUrlWithParams(localePath(route.path, currentLocaleCode)),
41
+ key: `alternate-${currentHreflang}-${route.path}`,
42
+ });
43
+
44
+ if (currentLocaleCode !== 'en') {
45
+ links.push({
46
+ rel: 'alternate',
47
+ hreflang: 'en',
48
+ href: createUrlWithParams(localePath(route.path, 'en')),
49
+ key: `alternate-en-${route.path}`,
50
+ });
51
+ }
52
+
53
+ const otherLocales = i18n.locales.value.filter(
54
+ (locale) => {
55
+ const localeCode = typeof locale === 'string' ? locale : locale.code;
56
+ return localeCode !== currentLocaleCode && localeCode !== 'en';
57
+ }
58
+ );
59
+
60
+ links.push(
61
+ ...otherLocales.map((locale) => {
62
+ const localeCode = typeof locale === 'string' ? locale : locale.code;
63
+ const hreflang = localeCode === 'jp' ? 'ja' : localeCode;
64
+ return {
65
+ rel: 'alternate',
66
+ hreflang,
67
+ href: createUrlWithParams(localePath(route.path, localeCode)),
68
+ key: `alternate-${hreflang}-${route.path}`,
69
+ };
70
+ })
71
+ );
72
+
73
+ links.push({
74
+ rel: 'alternate',
75
+ hreflang: 'x-default',
76
+ href: createUrlWithParams(localePath(route.path, 'en')),
77
+ key: `alternate-x-default-${route.path}`,
78
+ });
79
+
80
+ return links;
81
+ });
82
+
83
+ return {
84
+ canonicalUrl,
85
+ alternateLinks,
86
+ };
87
+ };
@@ -0,0 +1,23 @@
1
+ import { ref } from 'vue';
2
+ import type { IVideoCard } from '../types';
3
+
4
+ const isSharePopupOpen = ref<boolean>(false);
5
+ const sharedVideoCard = ref<IVideoCard>({} as IVideoCard)
6
+
7
+ const openSharePopup = (card?: IVideoCard) => {
8
+ isSharePopupOpen.value = true;
9
+ if (card && Object.keys(card).length) {
10
+ sharedVideoCard.value = card;
11
+ }
12
+ };
13
+
14
+ const closeSharePopup = () => {
15
+ isSharePopupOpen.value = false;
16
+ };
17
+
18
+ export const useSharePopup = () => ({
19
+ isSharePopupOpen,
20
+ openSharePopup,
21
+ closeSharePopup,
22
+ sharedVideoCard,
23
+ });
@@ -0,0 +1,52 @@
1
+ import { parseApiError } from '../runtime';
2
+
3
+ const SUCCESS_TIME = 3000;
4
+ const ERROR_TIME = 5000;
5
+
6
+ const snackbarIcon = ref('check-circle');
7
+ const snackbarText = ref('');
8
+ const snackbarButtonText = ref('');
9
+ const isSnackBarInPopup = ref(false);
10
+ const snackbarTheme = ref('success' as 'success' | 'error' | 'default');
11
+ const snackbarTimer = ref(SUCCESS_TIME);
12
+
13
+ let _timeoutClosure: null | ReturnType<typeof setTimeout> = null;
14
+
15
+ const setErrorState = (error: any) => {
16
+ snackbarText.value = parseApiError(error);
17
+ snackbarTheme.value = 'error';
18
+ snackbarIcon.value = 'exclamation-circle';
19
+ snackbarTimer.value = ERROR_TIME;
20
+ };
21
+
22
+ function showErrorSnack(message: string) {
23
+ if (_timeoutClosure) {
24
+ clearTimeout(_timeoutClosure);
25
+ }
26
+
27
+ snackbarTheme.value = 'error';
28
+ snackbarIcon.value = 'exclamation-circle';
29
+ snackbarText.value = message;
30
+ snackbarTimer.value = ERROR_TIME;
31
+
32
+ _timeoutClosure = setTimeout(resetSnackbar, snackbarTimer.value);
33
+ }
34
+
35
+ function resetSnackbar() {
36
+ snackbarIcon.value = 'check-circle';
37
+ snackbarText.value = '';
38
+ snackbarTheme.value = 'success';
39
+ snackbarTimer.value = SUCCESS_TIME;
40
+ }
41
+
42
+ export const useSnackbar = () => ({
43
+ snackbarIcon,
44
+ snackbarText,
45
+ snackbarButtonText,
46
+ snackbarTheme,
47
+ snackbarTimer,
48
+ setErrorState,
49
+ showErrorSnack,
50
+ isSnackBarInPopup,
51
+ resetSnackbar,
52
+ });
@@ -0,0 +1,3 @@
1
+ export function useTestComposable() {
2
+ console.log('useTestComposable')
3
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export const alphabetItems = ['a', 'b', 'c', 'd', 'e', 'f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];