itube-specs 0.0.776 → 0.0.778

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 (144) hide show
  1. package/components/adv/native-banner.vue +135 -0
  2. package/components/adv/native-bottom.vue +25 -0
  3. package/components/adv/native-player.vue +23 -0
  4. package/components/adv/ntv.vue +19 -0
  5. package/components/auth/__tests__/login.test.ts +123 -0
  6. package/components/auth/__tests__/recovery.test.ts +86 -0
  7. package/components/auth/__tests__/register.test.ts +122 -0
  8. package/components/auth/icon.vue +41 -0
  9. package/components/auth/login.vue +183 -0
  10. package/components/auth/popup.vue +87 -0
  11. package/components/auth/recovery.vue +147 -0
  12. package/components/auth/register.vue +245 -0
  13. package/components/card/category-mini.vue +62 -0
  14. package/components/card/category.vue +138 -0
  15. package/components/card/channel-mini.vue +96 -0
  16. package/components/card/channel.vue +219 -0
  17. package/components/card/model-mini.vue +101 -0
  18. package/components/card/model.vue +224 -0
  19. package/components/card/playlist-mini.vue +124 -0
  20. package/components/card/playlist.vue +443 -0
  21. package/components/card/round.vue +127 -0
  22. package/components/card/video/dropdown.vue +142 -0
  23. package/components/card/video/index.vue +460 -0
  24. package/components/card/video/mini.vue +133 -0
  25. package/components/card/video/preview.vue +27 -0
  26. package/components/category/by-letter.vue +55 -0
  27. package/components/category/group.vue +103 -0
  28. package/components/category/letter-groups.vue +38 -0
  29. package/components/category/list.vue +147 -0
  30. package/components/category/nav.vue +45 -0
  31. package/components/filter/btn.vue +69 -0
  32. package/components/filter/chips.vue +57 -0
  33. package/components/filter/main.vue +305 -0
  34. package/components/filter/model-field.vue +101 -0
  35. package/components/filter/model.vue +239 -0
  36. package/components/filter/page.vue +156 -0
  37. package/components/filter/popup.vue +320 -0
  38. package/components/filter/selects.vue +346 -0
  39. package/components/filter/slider.vue +121 -0
  40. package/components/filter/videos-chips.vue +96 -0
  41. package/components/grid/categories.vue +36 -0
  42. package/components/grid/channels.vue +41 -0
  43. package/components/grid/models.vue +38 -0
  44. package/components/grid/playlists.vue +36 -0
  45. package/components/grid/videos.vue +97 -0
  46. package/components/info/grid.vue +188 -0
  47. package/components/info/main.vue +462 -0
  48. package/components/info/socials.vue +74 -0
  49. package/components/layout/burger-item.vue +132 -0
  50. package/components/layout/burger.vue +257 -0
  51. package/components/layout/cookie.vue +52 -0
  52. package/components/layout/extra-links.vue +56 -0
  53. package/components/layout/footer-language.vue +123 -0
  54. package/components/layout/footer.vue +86 -0
  55. package/components/layout/header-user.vue +265 -0
  56. package/components/layout/header.vue +231 -0
  57. package/components/layout/language.vue +371 -0
  58. package/components/layout/logo.vue +39 -0
  59. package/components/layout/nav.vue +173 -0
  60. package/components/layout/skip-link.vue +26 -0
  61. package/components/player/__tests__/like.test.ts +85 -0
  62. package/components/player/aside-videos.vue +43 -0
  63. package/components/player/autoplay.vue +51 -0
  64. package/components/player/channel.vue +139 -0
  65. package/components/player/core.vue +276 -0
  66. package/components/player/follow.vue +90 -0
  67. package/components/player/info.vue +61 -0
  68. package/components/player/interface.vue +230 -0
  69. package/components/player/like.vue +200 -0
  70. package/components/player/main.vue +538 -0
  71. package/components/player/playlist.vue +448 -0
  72. package/components/player/sponsored.vue +95 -0
  73. package/components/playlist/__tests__/add.test.ts +115 -0
  74. package/components/playlist/__tests__/edit.test.ts +91 -0
  75. package/components/playlist/add.vue +409 -0
  76. package/components/playlist/delete-video.vue +118 -0
  77. package/components/playlist/edit.vue +304 -0
  78. package/components/playlist/input.vue +207 -0
  79. package/components/playlist/main.vue +152 -0
  80. package/components/playlist/private-toggle.vue +19 -0
  81. package/components/profile/__tests__/password.test.ts +60 -0
  82. package/components/profile/__tests__/setup.test.ts +100 -0
  83. package/components/profile/delete.vue +61 -0
  84. package/components/profile/guest.vue +65 -0
  85. package/components/profile/password.vue +108 -0
  86. package/components/profile/setup.vue +393 -0
  87. package/components/report/__tests__/main.test.ts +106 -0
  88. package/components/report/main.vue +427 -0
  89. package/components/report/wrapper.vue +15 -0
  90. package/components/search/input.vue +169 -0
  91. package/components/search/item.vue +125 -0
  92. package/components/search/main.vue +178 -0
  93. package/components/search/result.vue +268 -0
  94. package/components/section/categories-island.server.vue +29 -0
  95. package/components/section/categories.vue +24 -0
  96. package/components/section/models.vue +55 -0
  97. package/components/section/nav-links.vue +82 -0
  98. package/components/section/seo-text.vue +74 -0
  99. package/components/section/swiper-cards.vue +91 -0
  100. package/components/section/toolbar.vue +136 -0
  101. package/components/share/main.vue +283 -0
  102. package/components/ui/avatar.vue +51 -0
  103. package/components/ui/breadcrumbs.vue +97 -0
  104. package/components/ui/btn.vue +166 -0
  105. package/components/ui/checkbox.vue +153 -0
  106. package/components/ui/chips.vue +236 -0
  107. package/components/ui/count.vue +31 -0
  108. package/components/ui/dropdown.vue +403 -0
  109. package/components/ui/empty-state.vue +67 -0
  110. package/components/ui/expand-row.vue +200 -0
  111. package/components/ui/full-descr.vue +37 -0
  112. package/components/ui/icon.vue +24 -0
  113. package/components/ui/img.vue +57 -0
  114. package/components/ui/input.vue +354 -0
  115. package/components/ui/label.vue +95 -0
  116. package/components/ui/letter.vue +28 -0
  117. package/components/ui/link.vue +44 -0
  118. package/components/ui/notification.vue +160 -0
  119. package/components/ui/pagination.vue +294 -0
  120. package/components/ui/popup.vue +408 -0
  121. package/components/ui/radio.vue +142 -0
  122. package/components/ui/section-title.vue +55 -0
  123. package/components/ui/segmented.vue +121 -0
  124. package/components/ui/select.vue +225 -0
  125. package/components/ui/slider.vue +395 -0
  126. package/components/ui/snackbar.vue +103 -0
  127. package/components/ui/swiper.vue +189 -0
  128. package/components/ui/toggle.vue +112 -0
  129. package/components/ui/tooltip.vue +109 -0
  130. package/components/view/model-videos.vue +312 -0
  131. package/components/view/models.vue +228 -0
  132. package/components/view/video.vue +263 -0
  133. package/components/view/videos.vue +202 -0
  134. package/composables/__tests__/use-next-video.test.ts +123 -0
  135. package/composables/fetch/use-fetch-video.ts +28 -4
  136. package/composables/use-next-video.ts +120 -0
  137. package/package.json +1 -1
  138. package/runtime/constants/adverts.ts +18 -0
  139. package/runtime/index.ts +1 -0
  140. package/runtime/utils/cleaners/clean-video-data.ts +35 -3
  141. package/types/index.d.ts +1 -0
  142. package/types/raw/raw-video-data.d.ts +3 -2
  143. package/types/site.d.ts +37 -0
  144. package/types/video-data.d.ts +25 -2
@@ -0,0 +1,183 @@
1
+ <template>
2
+ <div :class="{ _loading: loading }"
3
+ class="auth-login"
4
+ >
5
+ <AuthIcon class="auth-login__icon" />
6
+
7
+ <UiInput v-model="form.username"
8
+ :label="t('auth.username')"
9
+ :placeholder="t('auth.username')"
10
+ hide-label
11
+ :error="error.username"
12
+ name="login-name"
13
+ enterkeyhint="next"
14
+ @update:error="(val: boolean) => (error.username = val)"
15
+ @enter="passwordRef?.focus()"
16
+ />
17
+ <UiInput ref="passwordRef"
18
+ v-model="form.password_hash"
19
+ :label="t('auth.password')"
20
+ :placeholder="t('auth.password')"
21
+ hide-label
22
+ :error="error.password_hash"
23
+ type="password"
24
+ enterkeyhint="go"
25
+ @update:error="(val: boolean) => (error.password_hash = val)"
26
+ @enter="login"
27
+ />
28
+
29
+ <div class="auth-login__buttons">
30
+ <UiBtn :disabled="loading"
31
+ class="auth-login__login"
32
+ wide
33
+ size="l"
34
+ theme="primary"
35
+ @click="login"
36
+ >{{ t("auth.log_in") }}
37
+ </UiBtn>
38
+ <button class="auth-login__forgot"
39
+ type="button"
40
+ @click="onForgotClick"
41
+ >
42
+ {{ t("auth.forgot") }}
43
+ </button>
44
+
45
+ <p class="auth-login__back">
46
+ {{ t("register_text") }}
47
+ <button class="auth-login__back-button"
48
+ type="button"
49
+ @click="onCreateClick"
50
+ >
51
+ {{ t("auth.sign_up") }}
52
+ </button>
53
+ </p>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script setup lang="ts">
59
+ import { validatePassword, validateUsername } from '../../runtime';
60
+ import { AuthorizationApiService } from '../../services/api/authorization.service';
61
+
62
+ const { t } = useI18n();
63
+
64
+ const passwordRef = ref();
65
+
66
+ const form = ref({
67
+ username: '',
68
+ password_hash: '',
69
+ });
70
+
71
+ const error = ref({
72
+ username: false,
73
+ password_hash: false,
74
+ });
75
+
76
+ const emit = defineEmits<{
77
+ (eventName: 'forgot'): void;
78
+ (eventName: 'create'): void;
79
+ }>();
80
+
81
+ function onForgotClick() {
82
+ emit('forgot');
83
+ }
84
+
85
+ function onCreateClick() {
86
+ emit('create');
87
+ }
88
+
89
+ const loading = ref(false);
90
+
91
+ const { setErrorState, showError, showSuccess, resetSnackbar } = useSnackbar();
92
+
93
+ async function login() {
94
+ resetSnackbar();
95
+ error.value.username = false;
96
+ error.value.password_hash = false;
97
+
98
+ if (!validateUsername(form.value.username)) {
99
+ error.value.username = true;
100
+ showError('error_username');
101
+ }
102
+ if (!validatePassword(form.value.password_hash)) {
103
+ error.value.password_hash = true;
104
+ showError('error_password');
105
+ }
106
+
107
+ if (!error.value.username && !error.value.password_hash) {
108
+ try {
109
+ loading.value = true;
110
+ await useUser(AuthorizationApiService).login(form.value);
111
+ useAuthPopup().closeAuthPopup();
112
+ showSuccess('Welcome back!');
113
+ } catch (error) {
114
+ setErrorState(error);
115
+ } finally {
116
+ loading.value = false;
117
+ }
118
+ }
119
+ }
120
+ </script>
121
+
122
+ <style lang="scss">
123
+ .auth-login {
124
+ row-gap: 16px;
125
+ display: grid;
126
+ align-items: start;
127
+
128
+ @include from-br(sm) {
129
+ row-gap: 8px;
130
+ }
131
+ }
132
+
133
+ .auth-login._loading {
134
+ @include from-br(sm) {
135
+ cursor: wait;
136
+ }
137
+ }
138
+
139
+ .auth-login__icon {
140
+ margin: 0 auto;
141
+ }
142
+
143
+ .auth-login__buttons {
144
+ display: grid;
145
+ padding-top: 24px;
146
+ border-top: 1px solid $auth-login-border;
147
+ }
148
+
149
+ .auth-login__login {
150
+ margin-bottom: 16px;
151
+ }
152
+
153
+ .auth-login .ui-input:nth-last-child(1 of .ui-input) {
154
+ padding-bottom: 12px;
155
+ }
156
+
157
+ .auth-login__back {
158
+ @include font-sm;
159
+
160
+ text-align: center;
161
+ color: $auth-login-back;
162
+ }
163
+
164
+ .auth-login__back-button {
165
+ color: $auth-login-back-link;
166
+
167
+ @include hover(color) {
168
+ color: $auth-login-back-link--hover;
169
+ }
170
+ }
171
+
172
+ .auth-login__forgot {
173
+ @include font-base;
174
+
175
+ color: $auth-login-forgot;
176
+ margin-bottom: 12px;
177
+ padding-block: 8px;
178
+
179
+ @include hover(color) {
180
+ color: $auth-login-forgot--hover;
181
+ }
182
+ }
183
+ </style>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <div v-if="isAuthPopupOpen"
3
+ class="auth-popup__decorate"
4
+ />
5
+ <transition mode="out-in">
6
+ <UiPopup v-if="currentStep === AuthSteps.Registration && isAuthPopupOpen"
7
+ v-model="isAuthPopupOpen"
8
+ transparent-backdrop
9
+ :back="isBackShow"
10
+ class="auth-popup"
11
+ @back="currentStep -= 1"
12
+ >
13
+ <template #title>{{ $t('auth.create_new') }}</template>
14
+ <AuthRegister :additional-text="additionalText"
15
+ @login="onLoginClick"
16
+ />
17
+ </UiPopup>
18
+ </transition>
19
+
20
+ <transition mode="out-in">
21
+ <UiPopup v-if="currentStep === AuthSteps.Login && isAuthPopupOpen"
22
+ v-model="isAuthPopupOpen"
23
+ transparent-backdrop
24
+ :back="isBackShow"
25
+ class="auth-popup"
26
+ @back="currentStep -= 1"
27
+ >
28
+ <template #title>{{ $t('auth.welcome_back') }}</template>
29
+ <AuthLogin @forgot="onRecoveryClick"
30
+ @create="currentStep -= 1"
31
+ />
32
+ </UiPopup>
33
+ </transition>
34
+
35
+ <transition mode="out-in">
36
+ <UiPopup v-if="currentStep === AuthSteps.Recovery && isAuthPopupOpen"
37
+ v-model="isAuthPopupOpen"
38
+ transparent-backdrop
39
+ :back="isBackShow"
40
+ class="auth-popup"
41
+ @back="currentStep -= 1"
42
+ >
43
+ <template #title>{{ $t('auth.recovery_title')}}</template>
44
+ <AuthRecovery @login="onLoginClick"/>
45
+ </UiPopup>
46
+ </transition>
47
+ </template>
48
+
49
+ <script setup lang="ts">
50
+ import { AuthSteps } from '../../runtime';
51
+
52
+ const { isAuthPopupOpen, currentStep, additionalText } = useAuthPopup();
53
+
54
+ const backShow = ref(false);
55
+
56
+ function onLoginClick(): void {
57
+ currentStep.value = AuthSteps.Login;
58
+ backShow.value = true;
59
+ }
60
+
61
+ function onRecoveryClick(): void {
62
+ currentStep.value = AuthSteps.Recovery;
63
+ backShow.value = true;
64
+ }
65
+
66
+ const isBackShow = computed(() => {
67
+ return currentStep.value !== AuthSteps.Registration && backShow.value;
68
+ });
69
+ </script>
70
+
71
+ <style lang="scss">
72
+ .auth-popup {
73
+ max-width: 448px;
74
+ }
75
+
76
+ .auth-popup__decorate {
77
+ position: fixed;
78
+ inset: 0;
79
+ z-index: $z-index-popup;
80
+ backdrop-filter: blur($blur-sm);
81
+ background-color: $popup-decorate-bg;
82
+ }
83
+
84
+ .auth-popup .ui-input__input::placeholder {
85
+ @include font-base;
86
+ }
87
+ </style>
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <div :class="{'_loading': loading}"
3
+ class="auth-recovery"
4
+ >
5
+
6
+ <AuthIcon class="auth-recovery__icon"
7
+ icon="envelope"
8
+ />
9
+
10
+ <p class="auth-recovery__text">{{ t('auth.recovery_text') }}</p>
11
+ <UiInput v-model="form.email"
12
+ :label="$t('email')"
13
+ :placeholder="$t('email')"
14
+ hide-label
15
+ :error="error.email"
16
+ type="email"
17
+ inputmode="email"
18
+ enterkeyhint="send"
19
+ autocomplete="email"
20
+ @update:error="(val: boolean) => error.email = val"
21
+ @enter="submit"
22
+ />
23
+
24
+ <UiBtn :disabled="loading"
25
+ class="auth-recovery__button"
26
+ wide
27
+ size="l"
28
+ theme="primary"
29
+ @click="submit"
30
+ >{{ t('auth.get_recovery') }}
31
+ </UiBtn>
32
+ <button class="auth-recovery__login"
33
+ type="button"
34
+ @click="onLoginClick"
35
+ >{{ t('auth.back_to_login') }}
36
+ </button>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import type { IPasswordForm } from '../../types';
42
+ import { validateEmail } from '../../runtime';
43
+ import { AuthorizationApiService } from '../../services/api/authorization.service';
44
+
45
+ const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
46
+
47
+ const { t } = useI18n();
48
+
49
+ const form = ref({
50
+ email: '',
51
+ token: '',
52
+ } as IPasswordForm);
53
+
54
+ const error = ref({
55
+ email: false,
56
+ });
57
+
58
+ const emit = defineEmits<{
59
+ (eventName: 'login'): void
60
+ }>();
61
+
62
+ function onLoginClick() {
63
+ emit('login');
64
+ }
65
+
66
+ const loading = ref(false);
67
+
68
+ const { setErrorState, showError, showSuccess, resetSnackbar } = useSnackbar();
69
+
70
+ // Загружаем reCAPTCHA при открытии формы (onMounted) — даёт Google время собрать данные о поведении
71
+ onMounted(async () => {
72
+ try {
73
+ await initRecaptcha();
74
+ console.log('reCAPTCHA готов для этой формы');
75
+ } catch (err) {
76
+ console.error('Ошибка инициализации reCAPTCHA:', err);
77
+ }
78
+ });
79
+
80
+ async function submit() {
81
+ resetSnackbar();
82
+ error.value.email = false;
83
+
84
+ if (!validateEmail(form.value.email)) {
85
+ error.value.email = true;
86
+ showError('error_email');
87
+ return;
88
+ }
89
+
90
+ try {
91
+ loading.value = true;
92
+ form.value.token = await getRecaptchaToken('password_reset');
93
+ await useUser(AuthorizationApiService).password(form.value);
94
+ useAuthPopup().closeAuthPopup();
95
+ showSuccess('Check your email');
96
+ } catch (error) {
97
+ setErrorState(error);
98
+ } finally {
99
+ loading.value = false;
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <style lang="scss">
105
+ .auth-recovery {
106
+ display: grid;
107
+ row-gap: 16px;
108
+ align-items: start;
109
+
110
+ @include from-br(sm) {
111
+ row-gap: 8px;
112
+ }
113
+ }
114
+
115
+ .auth-recovery._loading {
116
+ @include from-br(sm) {
117
+ cursor: wait;
118
+ }
119
+ }
120
+
121
+ .auth-recovery__icon {
122
+ margin: 0 auto;
123
+ }
124
+
125
+ .auth-recovery__button {
126
+ margin-top: 8px;
127
+ }
128
+
129
+ .auth-recovery__text {
130
+ @include font-sm;
131
+
132
+ text-align: center;
133
+ color: $auth-recovery-text;
134
+ }
135
+
136
+ .auth-recovery__login {
137
+ @include font-sm;
138
+
139
+ color: $auth-recovery-login;
140
+ margin-top: 8px;
141
+ padding-block: 8px;
142
+
143
+ @include hover(color) {
144
+ color: $auth-recovery-login--hover;
145
+ }
146
+ }
147
+ </style>
@@ -0,0 +1,245 @@
1
+ <template>
2
+ <ClientOnly>
3
+ <div :class="{'_loading': loading}"
4
+ class="auth-register"
5
+ >
6
+ <AuthIcon class="auth-register__icon" />
7
+ <p v-if="additionalText"
8
+ class="auth-register__text"
9
+ >{{ t(additionalText) }}
10
+ </p>
11
+ <UiInput v-model="form.email"
12
+ :placeholder="t('email')"
13
+ :label="t('email')"
14
+ hide-label
15
+ :error="error.email"
16
+ type="email"
17
+ inputmode="email"
18
+ enterkeyhint="next"
19
+ autocomplete="email"
20
+ @update:error="(val: boolean) => error.email = val"
21
+ @enter="usernameRef?.focus()"
22
+ />
23
+ <UiInput ref="usernameRef"
24
+ v-model="form.username"
25
+ :label="t('login')"
26
+ :placeholder="t('login')"
27
+ hide-label
28
+ :error="error.username"
29
+ name="login-name"
30
+ enterkeyhint="next"
31
+ @update:error="(val: boolean) => error.username = val"
32
+ @enter="passwordRef?.focus()"
33
+ />
34
+ <UiInput ref="passwordRef"
35
+ v-model="form.password_hash"
36
+ :label="t('auth.password')"
37
+ :placeholder="t('auth.password')"
38
+ hide-label
39
+ :error="error.password_hash"
40
+ type="password"
41
+ enterkeyhint="send"
42
+ autocomplete="new-password"
43
+ @update:error="(val: boolean) => error.password_hash = val"
44
+ @enter="submit"
45
+ />
46
+
47
+ <div class="auth-register__buttons">
48
+ <UiBtn :disabled="loading"
49
+ wide
50
+ size="l"
51
+ theme="primary"
52
+ @click="submit"
53
+ >{{ t('auth.sign_up') }}
54
+ </UiBtn>
55
+ <p class="auth-register__login">
56
+ {{ t('login_text') }}
57
+ <button class="auth-register__login-button"
58
+ type="button"
59
+ @click="onLoginClick"
60
+ >
61
+ {{ t('auth.log_in') }}
62
+ </button>
63
+ </p>
64
+ </div>
65
+
66
+ <p class="auth-register__agreement">{{ t('auth.agree_begin') }}
67
+ <NuxtLink class="auth-register__agreement-link"
68
+ to="/terms"
69
+ target="_blank"
70
+ >{{ t('auth.agree_terms') }}
71
+ </NuxtLink>
72
+ {{ t('auth.agree_and') }}
73
+ <NuxtLink class="auth-register__agreement-link"
74
+ to="/policy"
75
+ target="_blank"
76
+ >{{ t('auth.agree_policy') }}
77
+ </NuxtLink>
78
+ </p>
79
+ </div>
80
+ </ClientOnly>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import type { IRegistrateForm } from '../../types';
85
+ import { validateEmail, validatePassword, validateUsername } from '../../runtime';
86
+ import { AuthorizationApiService } from '../../services/api/authorization.service';
87
+
88
+ const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
89
+
90
+ defineProps<{
91
+ additionalText?: string;
92
+ }>();
93
+
94
+ const { t } = useI18n();
95
+
96
+ const usernameRef = ref();
97
+ const passwordRef = ref();
98
+
99
+ const form = ref({
100
+ username: '',
101
+ email: '',
102
+ password_hash: '',
103
+ token: '',
104
+ } as IRegistrateForm);
105
+
106
+ const error = ref({
107
+ username: false,
108
+ email: false,
109
+ password_hash: false,
110
+ });
111
+
112
+ const emit = defineEmits<{
113
+ (eventName: 'login'): void
114
+ }>();
115
+
116
+ function onLoginClick() {
117
+ emit('login');
118
+ }
119
+
120
+ const loading = ref(false);
121
+
122
+ const { setErrorState, showError, showSuccess, resetSnackbar } = useSnackbar();
123
+ // Загружаем reCAPTCHA при открытии формы (onMounted) — даёт Google время собрать данные о поведении
124
+ onMounted(async () => {
125
+ try {
126
+ await initRecaptcha();
127
+ console.log('reCAPTCHA готов для этой формы');
128
+ } catch (err) {
129
+ console.error('Ошибка инициализации reCAPTCHA:', err);
130
+ }
131
+ });
132
+
133
+ function validateForm(): boolean {
134
+ const { username, email, password_hash } = form.value;
135
+
136
+ const errors = {
137
+ username: !validateUsername(username),
138
+ email: !validateEmail(email),
139
+ password_hash: !validatePassword(password_hash),
140
+ };
141
+
142
+ if (!validateEmail(email)) showError('error_email');
143
+ if (!validatePassword(password_hash)) showError('error_password');
144
+ if (!validateUsername(username)) showError('error_username');
145
+
146
+ error.value = errors;
147
+
148
+ return !Object.values(errors).some(Boolean);
149
+ }
150
+
151
+ async function submit() {
152
+ resetSnackbar();
153
+ if (!validateForm()) return;
154
+
155
+ try {
156
+ loading.value = true;
157
+
158
+ // Уникальное действие для каждой формы
159
+ // здесь 'register', в других формах — 'password_reset' или 'contact_form'
160
+ form.value.token = await getRecaptchaToken('register');
161
+
162
+ await useUser(AuthorizationApiService).register(form.value);
163
+
164
+ // Создаем сразу Favorites плейлист, возможно костыль убрать когда будет нормальная логика с беком
165
+ // const { FAVORITES_KEY } = useFavorites();
166
+ // await PlaylistsApiService.postPlaylist(FAVORITES_KEY, PlaylistType.Private);
167
+
168
+ useAuthPopup().closeAuthPopup();
169
+ showSuccess('Registration success');
170
+ } catch (err) {
171
+ setErrorState(err);
172
+ console.error('Ошибка reCAPTCHA или регистрации:', err);
173
+ } finally {
174
+ loading.value = false;
175
+ }
176
+ }
177
+ </script>
178
+
179
+ <style lang="scss">
180
+ .auth-register {
181
+ display: grid;
182
+ row-gap: 16px;
183
+ align-items: start;
184
+
185
+ @include from-br(sm) {
186
+ row-gap: 8px;
187
+ }
188
+ }
189
+
190
+ .auth-register._loading {
191
+ @include from-br(sm) {
192
+ cursor: wait;
193
+ }
194
+ }
195
+
196
+ .auth-register__icon {
197
+ margin: 0 auto;
198
+ }
199
+
200
+ .auth-register__buttons {
201
+ display: grid;
202
+ row-gap: 8px;
203
+ padding-top: 24px;
204
+ border-top: 1px solid $auth-register-border;
205
+ }
206
+
207
+ .auth-register__agreement {
208
+ @include font-xs;
209
+
210
+ font-weight: 400;
211
+ color: $auth-register-agreement;
212
+ text-align: center;
213
+ }
214
+
215
+ .auth-register__agreement-link {
216
+ color: $auth-register-link;
217
+
218
+ @include hover(color) {
219
+ color: $auth-register-link--hover;
220
+ }
221
+ }
222
+
223
+ .auth-register__text {
224
+ text-align: center;
225
+ }
226
+
227
+ .auth-register .ui-input:nth-last-child(1 of .ui-input) {
228
+ padding-bottom: 12px;
229
+ }
230
+
231
+ .auth-register__login {
232
+ @include font-sm;
233
+
234
+ text-align: center;
235
+ color: $auth-register-login;
236
+ }
237
+
238
+ .auth-register__login-button {
239
+ color: $auth-register-link;
240
+
241
+ @include hover(color) {
242
+ color: $auth-register-link--hover;
243
+ }
244
+ }
245
+ </style>