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.
- package/README.md +121 -0
- package/components/cards/f-video-mini-card.vue +49 -0
- package/components/grids/f-grid-categories.vue +20 -0
- package/components/grids/f-grid-channels.vue +23 -0
- package/components/grids/f-grid-models.vue +25 -0
- package/components/grids/f-grid-playlists.vue +21 -0
- package/components/grids/f-grid-videos.vue +33 -0
- package/components/page-components/f-breadcrumbs.vue +44 -0
- package/components/page-components/f-chips-panel.vue +101 -0
- package/components/page-components/f-pagination.vue +206 -0
- package/components/page-components/f-report.vue +221 -0
- package/components/page-components/f-share.vue +96 -0
- package/components/page-components/f-sort.vue +57 -0
- package/components/page-components/f-videos-title.vue +20 -0
- package/components/ui/f-button.vue +50 -0
- package/components/ui/f-checkbox.vue +55 -0
- package/components/ui/f-chips.vue +116 -0
- package/components/ui/f-count.vue +12 -0
- package/components/ui/f-country.vue +26 -0
- package/components/ui/f-dropdown.vue +122 -0
- package/components/ui/f-icon.vue +19 -0
- package/components/ui/f-img.vue +46 -0
- package/components/ui/f-input.vue +162 -0
- package/components/ui/f-label.vue +20 -0
- package/components/ui/f-link.vue +33 -0
- package/components/ui/f-model-tag.vue +28 -0
- package/components/ui/f-notification.vue +77 -0
- package/components/ui/f-popup.vue +136 -0
- package/components/ui/f-radio.vue +56 -0
- package/components/ui/f-select.vue +88 -0
- package/components/ui/f-slider.vue +55 -0
- package/components/ui/f-snackbar.vue +47 -0
- package/components/ui/f-timestamp.vue +51 -0
- package/components/ui/f-toggle.vue +29 -0
- package/composables/use-antiadblock-domains.ts +20 -0
- package/composables/use-auth-popup.ts +25 -0
- package/composables/use-convert-query-categories.ts +7 -0
- package/composables/use-generate-link.ts +30 -0
- package/composables/use-get-pure-route-name.ts +5 -0
- package/composables/use-get-videos-filter-request.ts +30 -0
- package/composables/use-meta.ts +42 -0
- package/composables/use-playlist-edit.ts +36 -0
- package/composables/use-report-popup.ts +21 -0
- package/composables/use-seo-links.ts +87 -0
- package/composables/use-share-popup.ts +23 -0
- package/composables/use-snackbar.ts +52 -0
- package/composables/use-test-composable.ts +3 -0
- package/lib/alphabet-items.ts +2 -0
- package/lib/contact-forms-scheme.ts +98 -0
- package/lib/contacts/report-issue-items.ts +5 -0
- package/lib/contacts/report-malware-items.ts +6 -0
- package/lib/contacts/report-reasons-items.ts +12 -0
- package/lib/contacts/report-wrong-items.ts +6 -0
- package/lib/index.ts +7 -0
- package/lib/report-forms-scheme.ts +205 -0
- package/nuxt.config.ts +20 -0
- package/package.json +53 -0
- package/runtime/enums/async-data.ts +48 -0
- package/runtime/enums/auth-step.ts +5 -0
- package/runtime/enums/contacts-subjects.ts +7 -0
- package/runtime/enums/languages.ts +9 -0
- package/runtime/enums/niche.ts +6 -0
- package/runtime/enums/playlist-step.ts +5 -0
- package/runtime/enums/playlist-type.ts +4 -0
- package/runtime/enums/report-forms-subjects.ts +7 -0
- package/runtime/index.ts +51 -0
- package/runtime/utils/cleaners/clean-category-card.ts +9 -0
- package/runtime/utils/cleaners/clean-category-info.ts +9 -0
- package/runtime/utils/cleaners/clean-channel-card.ts +12 -0
- package/runtime/utils/cleaners/clean-channel-info.ts +13 -0
- package/runtime/utils/cleaners/clean-model-card.ts +9 -0
- package/runtime/utils/cleaners/clean-model-info.ts +11 -0
- package/runtime/utils/cleaners/clean-playlist-card.ts +16 -0
- package/runtime/utils/cleaners/clean-playlist-data.ts +15 -0
- package/runtime/utils/cleaners/clean-playlist-video.ts +12 -0
- package/runtime/utils/cleaners/clean-profile-data.ts +11 -0
- package/runtime/utils/cleaners/clean-user-playlists-card.ts +11 -0
- package/runtime/utils/cleaners/clean-video-card.ts +19 -0
- package/runtime/utils/cleaners/clean-video-data.ts +27 -0
- package/runtime/utils/compress-image.ts +27 -0
- package/runtime/utils/converters/convert-categories-to-chips.ts +13 -0
- package/runtime/utils/converters/convert-categories-to-footer.ts +11 -0
- package/runtime/utils/converters/convert-date-to-timestamp.ts +37 -0
- package/runtime/utils/converters/convert-model-card-to-chips.ts +13 -0
- package/runtime/utils/converters/convert-string.ts +56 -0
- package/runtime/utils/converters/group-categories-by-first-letter.ts +24 -0
- package/runtime/utils/converters/group-objects-by-first-letter.ts +16 -0
- package/runtime/utils/format-date.ts +12 -0
- package/runtime/utils/format-number.ts +12 -0
- package/runtime/utils/format-time-ago.ts +21 -0
- package/runtime/utils/get-duration.ts +17 -0
- package/runtime/utils/get-month.ts +22 -0
- package/runtime/utils/get-multiple-query.ts +26 -0
- package/runtime/utils/get-selected-query.ts +6 -0
- package/runtime/utils/is-mobile.ts +15 -0
- package/runtime/utils/normalize-url.ts +43 -0
- package/runtime/utils/on-backdrop-click.ts +5 -0
- package/runtime/utils/scroll-lock.ts +28 -0
- package/runtime/utils/server/abort-controller.ts +14 -0
- package/runtime/utils/server/api-helper.ts +41 -0
- package/runtime/utils/server/get-url-with-proxied-params.ts +6 -0
- package/runtime/utils/server/parse-api-error.ts +14 -0
- package/runtime/utils/server/server-api-helper.ts +28 -0
- package/runtime/utils/validate-email.ts +4 -0
- package/runtime/utils/validate-password.ts +3 -0
- package/runtime/utils/validate-phone.ts +4 -0
- package/runtime/utils/validate-username.ts +4 -0
- package/runtime/utils/video-data-add-model-icon.ts +20 -0
- package/runtime/utils/vtt-helper.ts +86 -0
- package/types/authorization-forms.d.ts +16 -0
- package/types/breadcrumb-item.d.ts +4 -0
- package/types/button-sizes.d.ts +1 -0
- package/types/button-themes.d.ts +1 -0
- package/types/card-info.d.ts +22 -0
- package/types/category-card.d.ts +8 -0
- package/types/change-email-form.d.ts +3 -0
- package/types/change-password-form.d.ts +4 -0
- package/types/channel-card.d.ts +10 -0
- package/types/chips-item.d.ts +8 -0
- package/types/contacts-form.d.ts +10 -0
- package/types/contacts-scheme.d.ts +14 -0
- package/types/country.d.ts +5 -0
- package/types/css-breakpoints.d.ts +1 -0
- package/types/filter-scheme.d.ts +37 -0
- package/types/fluid-player.d.ts +226 -0
- package/types/gender.d.ts +5 -0
- package/types/group-categories.d.ts +15 -0
- package/types/index.d.ts +59 -0
- package/types/input-types.d.ts +1 -0
- package/types/link-item.d.ts +6 -0
- package/types/model-card.d.ts +7 -0
- package/types/model-filter-payload.d.ts +4 -0
- package/types/model-filter.d.ts +15 -0
- package/types/model-group.d.ts +5 -0
- package/types/model-tag.d.ts +5 -0
- package/types/multi-suggest.d.ts +105 -0
- package/types/navigation-items.d.ts +10 -0
- package/types/paginated-response.d.ts +8 -0
- package/types/parameter-model.d.ts +14 -0
- package/types/playlist-card.d.ts +16 -0
- package/types/playlist-data.d.ts +15 -0
- package/types/playlist-info-type.d.ts +28 -0
- package/types/playlist-video-form.d.ts +9 -0
- package/types/profile-data.d.ts +9 -0
- package/types/raw/raw-category-card.d.ts +23 -0
- package/types/raw/raw-category-info.d.ts +23 -0
- package/types/raw/raw-channel-card.d.ts +29 -0
- package/types/raw/raw-channel-info.d.ts +29 -0
- package/types/raw/raw-model-card.d.ts +53 -0
- package/types/raw/raw-model-info.d.ts +54 -0
- package/types/raw/raw-playlist-card.d.ts +27 -0
- package/types/raw/raw-playlist-data.d.ts +29 -0
- package/types/raw/raw-playlist-user.d.ts +24 -0
- package/types/raw/raw-playlist-video.d.ts +18 -0
- package/types/raw/raw-profile-data.d.ts +22 -0
- package/types/raw/raw-video-card.d.ts +78 -0
- package/types/raw/raw-video-data.d.ts +53 -0
- package/types/recovery-password-form.d.ts +4 -0
- package/types/related-phrases.d.ts +6 -0
- package/types/report-form.d.ts +9 -0
- package/types/report-scheme.d.ts +21 -0
- package/types/request-filters.d.ts +13 -0
- package/types/request-pagination.d.ts +5 -0
- package/types/search-top-models.d.ts +6 -0
- package/types/select-item.d.ts +10 -0
- package/types/tab-item.d.ts +6 -0
- package/types/thumbs-urls.d.ts +13 -0
- package/types/video-card.d.ts +18 -0
- 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') }}:
|
|
14
|
+
<span class="f-slider__range">{{ (between && between[0]) || $attrs.min }}‑{{ (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,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
|
+
});
|