adata-ui 2.1.41-beta.12 → 2.1.41-beta.13
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/components/forms/input/AInputV2.vue +21 -1
- package/components/forms/input/password/AInputPasswordV2.vue +72 -0
- package/components/forms/password/APasswordRequirements.vue +50 -0
- package/components/modals/id/IdConfirmAccountOtpModal.vue +19 -22
- package/components/modals/id/IdConfirmSuccessfulModal.vue +18 -15
- package/components/modals/id/IdLoginModal.vue +150 -119
- package/components/modals/id/IdModals.vue +66 -24
- package/components/modals/id/IdNewPasswordModal.vue +93 -52
- package/components/modals/id/IdOtpInput.vue +14 -14
- package/components/modals/id/IdPasswordSuccessfulModal.vue +11 -11
- package/components/modals/id/IdRecoveryModal.vue +57 -42
- package/components/modals/id/IdRegistrationModal.vue +136 -94
- package/components/modals/id/IdResetPasswordOtpModal.vue +19 -30
- package/components/modals/id/IdTwoFactorModal.vue +14 -29
- package/icons/google.vue +10 -32
- package/icons/mailru.vue +12 -27
- package/icons/yandex.vue +10 -20
- package/lang/en.ts +30 -4
- package/lang/kk.ts +30 -4
- package/lang/ru.ts +30 -4
- package/package.json +1 -1
- package/utils/apiErrorMessage.ts +22 -0
- package/utils/passwordRules.ts +19 -0
|
@@ -397,7 +397,9 @@ function rowDisplay(item: T): unknown {
|
|
|
397
397
|
:class="iconOffsetClass"
|
|
398
398
|
/>
|
|
399
399
|
|
|
400
|
-
|
|
400
|
+
<!-- iconOffsetClass отдаётся наружу: обёртки (AInputPasswordV2) должны
|
|
401
|
+
центрировать свою кнопку так же, как встроенные иконки поля -->
|
|
402
|
+
<slot name="endButton" :icon-offset-class="iconOffsetClass" />
|
|
401
403
|
</div>
|
|
402
404
|
|
|
403
405
|
<teleport to="body">
|
|
@@ -470,6 +472,24 @@ function rowDisplay(item: T): unknown {
|
|
|
470
472
|
cursor: not-allowed;
|
|
471
473
|
}
|
|
472
474
|
|
|
475
|
+
/* Chrome красит autofill-фоном сам <input>, а видимое поле в v2 — это обёртка
|
|
476
|
+
.input-v2__field, поэтому родная подсветка ложилась узкой полосой внутри поля
|
|
477
|
+
(в v1 фон нёс сам input, и подсветка совпадала с полем). Родной фон иначе не
|
|
478
|
+
убрать — прячем его бесконечной задержкой перехода, а подсвечиваем всю обёртку. */
|
|
479
|
+
.input-v2__input:-webkit-autofill,
|
|
480
|
+
.input-v2__input:-webkit-autofill:hover,
|
|
481
|
+
.input-v2__input:-webkit-autofill:focus,
|
|
482
|
+
.input-v2__input:-webkit-autofill:active {
|
|
483
|
+
-webkit-text-fill-color: var(--text-color-primary);
|
|
484
|
+
caret-color: var(--text-color-primary);
|
|
485
|
+
transition: background-color 600000s 0s;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.input-v2__field:has(.input-v2__input:-webkit-autofill),
|
|
489
|
+
.input-v2__field:has(.input-v2__input:autofill) {
|
|
490
|
+
background-color: var(--background-color-brand-subtle);
|
|
491
|
+
}
|
|
492
|
+
|
|
473
493
|
.input-v2__label {
|
|
474
494
|
font-size: 14px;
|
|
475
495
|
line-height: 1.3;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label?: string
|
|
6
|
+
placeholder?: string
|
|
7
|
+
error?: string | string[]
|
|
8
|
+
size?: 'sm' | 'md' | 'lg'
|
|
9
|
+
variant?: 'default' | 'gray' | 'blue' | 'canvas'
|
|
10
|
+
required?: boolean
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
readonly?: boolean
|
|
13
|
+
loading?: boolean
|
|
14
|
+
startIcon?: Component
|
|
15
|
+
startIconClass?: string
|
|
16
|
+
autocomplete?: string
|
|
17
|
+
inputId?: string
|
|
18
|
+
iconEyeTestId?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
withDefaults(defineProps<Props>(), {
|
|
22
|
+
size: 'sm',
|
|
23
|
+
variant: 'default',
|
|
24
|
+
required: false,
|
|
25
|
+
disabled: false,
|
|
26
|
+
readonly: false,
|
|
27
|
+
loading: false,
|
|
28
|
+
autocomplete: 'current-password',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const modelValue = defineModel<string | number>({ default: '' })
|
|
32
|
+
|
|
33
|
+
const isOpened = ref(false)
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<a-input-v2
|
|
38
|
+
v-model="modelValue"
|
|
39
|
+
:type="isOpened ? 'text' : 'password'"
|
|
40
|
+
:label="label"
|
|
41
|
+
:placeholder="placeholder"
|
|
42
|
+
:error="error"
|
|
43
|
+
:size="size"
|
|
44
|
+
:variant="variant"
|
|
45
|
+
:required="required"
|
|
46
|
+
:disabled="disabled"
|
|
47
|
+
:readonly="readonly"
|
|
48
|
+
:loading="loading"
|
|
49
|
+
:start-icon="startIcon"
|
|
50
|
+
:start-icon-class="startIconClass"
|
|
51
|
+
:autocomplete="autocomplete"
|
|
52
|
+
:input-id="inputId"
|
|
53
|
+
>
|
|
54
|
+
<template #endButton="{ iconOffsetClass }">
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
tabindex="-1"
|
|
58
|
+
class="flex shrink-0 items-center justify-center rounded-full p-0.5 text-icon-secondary transition-colors hover:bg-badge disabled:pointer-events-none"
|
|
59
|
+
:class="iconOffsetClass"
|
|
60
|
+
:disabled="disabled"
|
|
61
|
+
:data-test-id="iconEyeTestId"
|
|
62
|
+
:aria-label="isOpened ? $t('actions.hidePassword') : $t('actions.showPassword')"
|
|
63
|
+
:aria-pressed="isOpened"
|
|
64
|
+
@mousedown.prevent
|
|
65
|
+
@click="isOpened = !isOpened"
|
|
66
|
+
>
|
|
67
|
+
<a-icon-eye-closed v-if="!isOpened" class="size-4" />
|
|
68
|
+
<a-icon-eye-opened v-else class="size-4" />
|
|
69
|
+
</button>
|
|
70
|
+
</template>
|
|
71
|
+
</a-input-v2>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { PASSWORD_MIN_LENGTH, PASSWORD_RULES } from '#adata-ui/utils/passwordRules'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
password?: string
|
|
6
|
+
requireSpecial?: boolean
|
|
7
|
+
}>(), {
|
|
8
|
+
password: '',
|
|
9
|
+
requireSpecial: true,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const { t } = useI18n()
|
|
13
|
+
|
|
14
|
+
const rules = computed(() =>
|
|
15
|
+
PASSWORD_RULES
|
|
16
|
+
.filter(rule => props.requireSpecial || rule.key !== 'special')
|
|
17
|
+
.map(rule => ({
|
|
18
|
+
key: rule.key,
|
|
19
|
+
ok: rule.test(props.password),
|
|
20
|
+
label: t(`forms.password_requirements.${rule.key}`, { length: PASSWORD_MIN_LENGTH }),
|
|
21
|
+
})),
|
|
22
|
+
)
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<ul class="flex flex-col gap-2 rounded-lg bg-muted px-4 py-3">
|
|
27
|
+
<li
|
|
28
|
+
v-for="rule in rules"
|
|
29
|
+
:key="rule.key"
|
|
30
|
+
class="flex items-center gap-2"
|
|
31
|
+
>
|
|
32
|
+
<a-icon-check
|
|
33
|
+
v-if="rule.ok"
|
|
34
|
+
class="size-4 shrink-0 text-icon-success"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
/>
|
|
37
|
+
<span
|
|
38
|
+
v-else
|
|
39
|
+
class="flex size-4 shrink-0 items-center justify-center rounded-full bg-badge"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
>
|
|
42
|
+
<span class="size-1.5 rounded-full bg-(--color-icon-secondary)" />
|
|
43
|
+
</span>
|
|
44
|
+
<span
|
|
45
|
+
class="text-body-md transition-colors"
|
|
46
|
+
:class="rule.ok ? 'text-primary' : 'text-secondary'"
|
|
47
|
+
>{{ rule.label }}</span>
|
|
48
|
+
</li>
|
|
49
|
+
</ul>
|
|
50
|
+
</template>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import IdOtpInput from '#adata-ui/components/modals/id/IdOtpInput.vue'
|
|
3
3
|
import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
|
|
4
4
|
import { writeAccessTokenCookie } from '#adata-ui/composables/useAccessTokenCookie'
|
|
5
|
+
import { apiErrorMessage } from '#adata-ui/utils/apiErrorMessage'
|
|
5
6
|
|
|
6
7
|
const { $toast } = useNuxtApp()
|
|
7
8
|
const { t, locale } = useI18n()
|
|
@@ -104,10 +105,13 @@ async function onResend() {
|
|
|
104
105
|
})
|
|
105
106
|
resetOtp()
|
|
106
107
|
runTimer()
|
|
107
|
-
isResendLoading.value = false
|
|
108
108
|
}
|
|
109
109
|
catch (error) {
|
|
110
|
-
$toast.error(error.
|
|
110
|
+
$toast.error(apiErrorMessage(error, t('error.validation')))
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
// без finally сбой resend оставлял кнопку заблокированной навсегда
|
|
114
|
+
isResendLoading.value = false
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -121,25 +125,14 @@ function runTimer() {
|
|
|
121
125
|
}, 1000)
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
function handleEnter(e: KeyboardEvent) {
|
|
125
|
-
if (e.key === 'Enter') {
|
|
126
|
-
onConfirm()
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
128
|
onMounted(() => {
|
|
131
129
|
runTimer()
|
|
132
|
-
document.addEventListener('keyup', handleEnter)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
onBeforeUnmount(() => {
|
|
136
|
-
document.removeEventListener('keyup', handleEnter)
|
|
137
130
|
})
|
|
138
131
|
</script>
|
|
139
132
|
|
|
140
133
|
<template>
|
|
141
|
-
<div class="flex flex-col items-center gap-4 text-center">
|
|
142
|
-
<h2 class="text-h2">
|
|
134
|
+
<div class="flex flex-col items-center gap-4 p-6 text-center">
|
|
135
|
+
<h2 class="text-h2 text-primary">
|
|
143
136
|
{{ t('modals.id.confirmAccount.title') }}
|
|
144
137
|
</h2>
|
|
145
138
|
|
|
@@ -147,21 +140,23 @@ onBeforeUnmount(() => {
|
|
|
147
140
|
<a-icon-hand-with-phone-light class="size-32 dark:hidden" />
|
|
148
141
|
<a-icon-hand-with-phone-dark class="hidden size-32 dark:block" />
|
|
149
142
|
|
|
150
|
-
<div class="text-body-md">
|
|
143
|
+
<div class="text-body-md text-secondary">
|
|
151
144
|
<p class="mb-1">
|
|
152
145
|
{{ t('modals.id.resetPasswordOtp.content') }}
|
|
153
146
|
</p>
|
|
154
147
|
|
|
155
|
-
<div v-if="timer > 0" class="text-h2">
|
|
148
|
+
<div v-if="timer > 0" class="text-h2 text-primary">
|
|
156
149
|
{{ timer }} {{ t('modals.id.resetPasswordOtp.seconds') }}
|
|
157
150
|
</div>
|
|
158
151
|
<button
|
|
159
152
|
v-else
|
|
160
|
-
|
|
153
|
+
type="button"
|
|
154
|
+
class="mx-auto flex items-center gap-2 text-body-md text-link transition-colors hover:text-link-hover"
|
|
161
155
|
:disabled="isResendLoading"
|
|
162
156
|
@click="onResend"
|
|
163
157
|
>
|
|
164
|
-
|
|
158
|
+
<a-icon-loader-circle v-if="isResendLoading" class="size-4 animate-spin" />
|
|
159
|
+
<span>{{ t('actions.resend') }}</span>
|
|
165
160
|
</button>
|
|
166
161
|
</div>
|
|
167
162
|
|
|
@@ -171,12 +166,14 @@ onBeforeUnmount(() => {
|
|
|
171
166
|
@on-completed="onConfirm"
|
|
172
167
|
/>
|
|
173
168
|
|
|
174
|
-
<a-button
|
|
175
|
-
block
|
|
169
|
+
<a-button-v2
|
|
176
170
|
view="outline"
|
|
171
|
+
color="gray"
|
|
172
|
+
size="lg"
|
|
173
|
+
block
|
|
177
174
|
@click="onClose"
|
|
178
175
|
>
|
|
179
176
|
{{ t('actions.close') }}
|
|
180
|
-
</a-button>
|
|
177
|
+
</a-button-v2>
|
|
181
178
|
</div>
|
|
182
179
|
</template>
|
|
@@ -3,39 +3,42 @@ import { useUrls } from '#adata-ui/composables/useUrls'
|
|
|
3
3
|
|
|
4
4
|
const { t } = useI18n()
|
|
5
5
|
const { confirmSuccessfulModal } = useIdModals()
|
|
6
|
-
const regex = /^\/counterparty\/main\/company\/\d+\/basic-info
|
|
6
|
+
const regex = /^\/counterparty\/main\/company\/\d+\/basic-info$/
|
|
7
7
|
const { pk } = useUrls()
|
|
8
8
|
|
|
9
9
|
function onClose() {
|
|
10
10
|
confirmSuccessfulModal.value = false
|
|
11
11
|
|
|
12
|
-
const path = window.location.pathname
|
|
12
|
+
const path = window.location.pathname
|
|
13
13
|
|
|
14
14
|
if (regex.test(path)) {
|
|
15
|
-
navigateTo(`${pk}/${path.split(
|
|
16
|
-
}
|
|
15
|
+
navigateTo(`${pk}/${path.split('/')[4]}`, { external: true })
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
17
18
|
window.location.reload()
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
</script>
|
|
21
22
|
|
|
22
23
|
<template>
|
|
23
|
-
<div class="flex flex-col gap-4 text-center">
|
|
24
|
-
<h2 class="text-h2">
|
|
24
|
+
<div class="flex flex-col gap-4 p-6 text-center">
|
|
25
|
+
<h2 class="text-h2 text-primary">
|
|
25
26
|
{{ t('modals.id.confirmSuccessful.title') }}
|
|
26
27
|
</h2>
|
|
27
|
-
<
|
|
28
|
+
<div class="mx-auto flex size-24 items-center justify-center rounded-full bg-muted">
|
|
28
29
|
<a-ill-ok />
|
|
29
|
-
</
|
|
30
|
-
<p class="text-body-md">
|
|
30
|
+
</div>
|
|
31
|
+
<p class="text-body-md text-secondary">
|
|
31
32
|
{{ t('modals.id.confirmSuccessful.content') }}
|
|
32
33
|
</p>
|
|
33
|
-
<a-button
|
|
34
|
+
<a-button-v2
|
|
35
|
+
size="lg"
|
|
36
|
+
block
|
|
37
|
+
view="outline"
|
|
38
|
+
color="gray"
|
|
39
|
+
@click="onClose"
|
|
40
|
+
>
|
|
34
41
|
{{ t('actions.close') }}
|
|
35
|
-
</a-button>
|
|
42
|
+
</a-button-v2>
|
|
36
43
|
</div>
|
|
37
44
|
</template>
|
|
38
|
-
|
|
39
|
-
<style scoped>
|
|
40
|
-
|
|
41
|
-
</style>
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
|
|
3
3
|
import { writeAccessTokenCookie } from '#adata-ui/composables/useAccessTokenCookie'
|
|
4
|
+
import { useUrls } from '#adata-ui/composables/useUrls'
|
|
5
|
+
import AIconArrowUpRight from '#adata-ui/icons/arrow/arrow-up-right.vue'
|
|
6
|
+
import AIconGoogle from '#adata-ui/icons/google.vue'
|
|
7
|
+
import AIconLock from '#adata-ui/icons/lock.vue'
|
|
8
|
+
import AIconMail from '#adata-ui/icons/mail.vue'
|
|
9
|
+
import AIconMailru from '#adata-ui/icons/mailru.vue'
|
|
10
|
+
import AIconYandex from '#adata-ui/icons/yandex.vue'
|
|
4
11
|
import { navigateToLocalizedPage } from '#adata-ui/utils/localizedNavigation'
|
|
5
12
|
import * as z from 'zod'
|
|
6
|
-
import { useUrls } from '#adata-ui/composables/useUrls'
|
|
7
13
|
|
|
8
14
|
const { $toast } = useNuxtApp()
|
|
9
15
|
const { commonAuth } = useAppConfig()
|
|
@@ -25,11 +31,28 @@ const authApiURL = commonAuth.authApiURL
|
|
|
25
31
|
const submitted = ref(false)
|
|
26
32
|
const accessToken = ref(null)
|
|
27
33
|
|
|
34
|
+
const uid = useId()
|
|
35
|
+
const emailFieldId = `login-email-${uid}`
|
|
36
|
+
const passwordFieldId = `login-password-${uid}`
|
|
37
|
+
|
|
38
|
+
const socials = [
|
|
39
|
+
{ id: 'google', label: 'Google', icon: AIconGoogle },
|
|
40
|
+
{ id: 'yandex', label: 'Yandex', icon: AIconYandex },
|
|
41
|
+
{ id: 'mailru', label: 'Mail.ru', icon: AIconMailru },
|
|
42
|
+
]
|
|
43
|
+
|
|
28
44
|
export interface ILoginForm {
|
|
29
45
|
username: string
|
|
30
46
|
password: string
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
const loading = ref(false)
|
|
50
|
+
const form: ILoginForm = reactive({
|
|
51
|
+
username: useCookie('username', { domain: COOKIE_DOMAIN }).value || '',
|
|
52
|
+
password: useCookie('password', { domain: COOKIE_DOMAIN }).value || '',
|
|
53
|
+
})
|
|
54
|
+
const rememberMe = ref(false)
|
|
55
|
+
|
|
33
56
|
const loginSchema = z.object({
|
|
34
57
|
username: z.string().nonempty(t('error.required')).email(t('error.email')),
|
|
35
58
|
password: z.string().nonempty(t('error.required')),
|
|
@@ -61,13 +84,6 @@ function clearAuthCookie() {
|
|
|
61
84
|
useCookie('password', options).value = null
|
|
62
85
|
}
|
|
63
86
|
|
|
64
|
-
const loading = ref(false)
|
|
65
|
-
const form: ILoginForm = reactive({
|
|
66
|
-
username: useCookie('username', { domain: COOKIE_DOMAIN }).value || '',
|
|
67
|
-
password: useCookie('password', { domain: COOKIE_DOMAIN }).value || '',
|
|
68
|
-
})
|
|
69
|
-
const rememberMe = ref(false)
|
|
70
|
-
|
|
71
87
|
async function submit() {
|
|
72
88
|
submitted.value = true
|
|
73
89
|
if (!validation.value) {
|
|
@@ -146,7 +162,8 @@ async function submit() {
|
|
|
146
162
|
const companyId = match[1]
|
|
147
163
|
|
|
148
164
|
navigateTo(`${pk}/company/${companyId}`, { external: true })
|
|
149
|
-
}
|
|
165
|
+
}
|
|
166
|
+
else if (redirectUrl) {
|
|
150
167
|
navigateTo(redirectUrl as string, { external: true })
|
|
151
168
|
}
|
|
152
169
|
else {
|
|
@@ -173,7 +190,7 @@ async function submit() {
|
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
function authWithSocial(social: string) {
|
|
176
|
-
const url = new URL(
|
|
193
|
+
const url = new URL(`${removeTrailingSlash(authApiURL)}/login/social`)
|
|
177
194
|
|
|
178
195
|
url.searchParams.set('source', social)
|
|
179
196
|
url.searchParams.set('redirect_url', window.location.origin)
|
|
@@ -201,63 +218,74 @@ onMounted(() => {
|
|
|
201
218
|
rememberMe.value = !!useCookie('username', options).value && !!useCookie('password', options).value
|
|
202
219
|
})
|
|
203
220
|
|
|
204
|
-
function handleEnter(e: KeyboardEvent) {
|
|
205
|
-
if (e.key === 'Enter') {
|
|
206
|
-
submit()
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
221
|
function onForgotPassword() {
|
|
211
222
|
loginModal.value = false
|
|
212
223
|
recoveryModal.value = true
|
|
213
224
|
}
|
|
214
|
-
|
|
215
|
-
onMounted(() => {
|
|
216
|
-
document.addEventListener('keyup', handleEnter)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
onBeforeUnmount(() => {
|
|
220
|
-
document.removeEventListener('keyup', handleEnter)
|
|
221
|
-
})
|
|
222
225
|
</script>
|
|
223
226
|
|
|
224
227
|
<template>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
<!-- Enter отправляет форму нативно: документного keyup-слушателя здесь быть не должно,
|
|
229
|
+
иначе один Enter даёт два запроса -->
|
|
230
|
+
<form
|
|
231
|
+
class="flex flex-col gap-6 p-6"
|
|
232
|
+
novalidate
|
|
233
|
+
@submit.prevent="submit"
|
|
234
|
+
>
|
|
235
|
+
<div class="flex flex-col gap-1">
|
|
236
|
+
<h1 class="text-center text-h2 text-primary" data-test-id="login-form-title">
|
|
237
|
+
{{ t('modals.id.login.title') }}
|
|
238
|
+
</h1>
|
|
239
|
+
<p class="text-center text-body-md text-secondary" data-test-id="login-form-subtitle">
|
|
240
|
+
{{ t('modals.id.login.subtitle') }}
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
232
244
|
<div class="flex flex-col gap-4">
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
<div class="flex flex-col gap-1.5">
|
|
246
|
+
<label :for="emailFieldId" class="text-body-md-strong text-primary">
|
|
247
|
+
{{ t('modals.id.login.labels.email') }}
|
|
248
|
+
</label>
|
|
249
|
+
<a-input-v2
|
|
250
|
+
v-model="form.username"
|
|
251
|
+
:input-id="emailFieldId"
|
|
252
|
+
type="email"
|
|
253
|
+
size="lg"
|
|
254
|
+
autocomplete="email"
|
|
255
|
+
:start-icon="AIconMail"
|
|
256
|
+
:placeholder="t('modals.id.login.placeholders.email')"
|
|
257
|
+
:error="getError('username')"
|
|
258
|
+
data-test-id="login-form-email-input"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="flex flex-col gap-1.5">
|
|
263
|
+
<label :for="passwordFieldId" class="text-body-md-strong text-primary">
|
|
264
|
+
{{ t('modals.id.login.labels.password') }}
|
|
265
|
+
</label>
|
|
266
|
+
<a-input-password-v2
|
|
267
|
+
v-model="form.password"
|
|
268
|
+
:input-id="passwordFieldId"
|
|
269
|
+
size="lg"
|
|
270
|
+
:start-icon="AIconLock"
|
|
271
|
+
:placeholder="t('modals.id.login.placeholders.password')"
|
|
272
|
+
:error="getError('password')"
|
|
273
|
+
data-test-id="login-form-password-input"
|
|
274
|
+
icon-eye-test-id="login-form-password-eye"
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div class="flex items-center justify-between gap-3">
|
|
279
|
+
<a-checkbox-v2
|
|
280
|
+
v-model="rememberMe"
|
|
281
|
+
side="right"
|
|
282
|
+
name="remember_me"
|
|
283
|
+
:label="t('modals.id.login.remember_me')"
|
|
284
|
+
data-test-id="login-form-remember-me-checkbox"
|
|
285
|
+
/>
|
|
259
286
|
<button
|
|
260
|
-
|
|
287
|
+
type="button"
|
|
288
|
+
class="text-body-md text-link transition-colors hover:text-link-hover"
|
|
261
289
|
data-test-id="login-form-forgot-password-button"
|
|
262
290
|
@click="onForgotPassword"
|
|
263
291
|
>
|
|
@@ -265,72 +293,75 @@ onBeforeUnmount(() => {
|
|
|
265
293
|
</button>
|
|
266
294
|
</div>
|
|
267
295
|
</div>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
<div class="flex shrink-0 gap-4">
|
|
271
|
-
<span
|
|
272
|
-
class="cursor-pointer"
|
|
273
|
-
data-test-id="login-form-google-button"
|
|
274
|
-
@click="authWithSocial('google')"
|
|
275
|
-
>
|
|
276
|
-
<a-icon-google />
|
|
277
|
-
</span>
|
|
278
|
-
<span
|
|
279
|
-
class="cursor-pointer"
|
|
280
|
-
data-test-id="login-form-yandex-button"
|
|
281
|
-
@click="authWithSocial('yandex')"
|
|
282
|
-
>
|
|
283
|
-
<a-icon-yandex />
|
|
284
|
-
</span>
|
|
285
|
-
<span
|
|
286
|
-
class="cursor-pointer"
|
|
287
|
-
data-test-id="login-form-mailru-button"
|
|
288
|
-
@click="authWithSocial('mailru')"
|
|
289
|
-
>
|
|
290
|
-
<a-icon-mailru />
|
|
291
|
-
</span>
|
|
292
|
-
</div>
|
|
293
|
-
<div class="h-px w-full bg-(--border-color-default)" />
|
|
294
|
-
</div>
|
|
295
|
-
<a-button
|
|
296
|
-
:loading="loading"
|
|
296
|
+
|
|
297
|
+
<a-button-v2
|
|
297
298
|
type="submit"
|
|
299
|
+
size="lg"
|
|
300
|
+
block
|
|
301
|
+
:loading="loading"
|
|
298
302
|
data-test-id="login-form-login-button"
|
|
299
|
-
@click="submit"
|
|
300
303
|
>
|
|
301
304
|
{{ t('actions.login') }}
|
|
302
|
-
</a-button>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
data-test-id="login-form-register-button"
|
|
312
|
-
@click="onRegister"
|
|
313
|
-
>
|
|
314
|
-
{{ t('actions.register') }}
|
|
315
|
-
</a-button>
|
|
316
|
-
|
|
317
|
-
<a-button
|
|
318
|
-
type="button"
|
|
319
|
-
view="transparent"
|
|
320
|
-
class="w-full"
|
|
321
|
-
data-test-id="login-form-tariffs-button"
|
|
322
|
-
@click="toTariffs"
|
|
323
|
-
>
|
|
324
|
-
{{ t('actions.toTariffs') }}
|
|
325
|
-
</a-button>
|
|
305
|
+
</a-button-v2>
|
|
306
|
+
|
|
307
|
+
<div class="flex items-center gap-3">
|
|
308
|
+
<span class="h-px flex-1 bg-(--border-color-default)" />
|
|
309
|
+
<span class="shrink-0 text-body-sm text-secondary">
|
|
310
|
+
{{ t('modals.id.login.social_divider') }}
|
|
311
|
+
</span>
|
|
312
|
+
<span class="h-px flex-1 bg-(--border-color-default)" />
|
|
313
|
+
</div>
|
|
326
314
|
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
315
|
+
<div class="grid grid-cols-3 gap-2">
|
|
316
|
+
<a-button-v2
|
|
317
|
+
v-for="social in socials"
|
|
318
|
+
:key="social.id"
|
|
319
|
+
view="outline"
|
|
320
|
+
color="gray"
|
|
321
|
+
block
|
|
322
|
+
:icon="social.icon"
|
|
323
|
+
:data-test-id="`login-form-${social.id}-button`"
|
|
324
|
+
@click="authWithSocial(social.id)"
|
|
325
|
+
>
|
|
326
|
+
{{ social.label }}
|
|
327
|
+
</a-button-v2>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div class="flex flex-col gap-4">
|
|
331
|
+
<span class="h-px w-full bg-(--border-color-default)" />
|
|
332
|
+
<p class="text-center text-body-md text-secondary">
|
|
333
|
+
{{ t('modals.id.login.first_time') }}
|
|
334
|
+
</p>
|
|
335
|
+
<a-button-v2
|
|
336
|
+
view="outline"
|
|
337
|
+
color="gray"
|
|
338
|
+
size="lg"
|
|
339
|
+
block
|
|
340
|
+
data-test-id="login-form-register-button"
|
|
341
|
+
@click="onRegister"
|
|
342
|
+
>
|
|
343
|
+
{{ t('actions.register') }}
|
|
344
|
+
</a-button-v2>
|
|
345
|
+
<a-button-v2
|
|
346
|
+
view="ghost"
|
|
347
|
+
color="blue"
|
|
348
|
+
block
|
|
349
|
+
icon-right
|
|
350
|
+
:icon="AIconArrowUpRight"
|
|
351
|
+
data-test-id="login-form-tariffs-button"
|
|
352
|
+
@click="toTariffs"
|
|
353
|
+
>
|
|
354
|
+
{{ t('actions.toTariffs') }}
|
|
355
|
+
</a-button-v2>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div class="flex items-start gap-3 rounded-lg border-l-4 border-brand bg-brand-subtle px-4 py-3">
|
|
359
|
+
<a-icon-shield-check class="mt-0.5 size-5 shrink-0 text-icon-brand" />
|
|
360
|
+
<p class="text-body-sm text-primary">
|
|
361
|
+
{{ t('info.userAgreement') }}
|
|
362
|
+
</p>
|
|
363
|
+
</div>
|
|
364
|
+
</form>
|
|
334
365
|
</template>
|
|
335
366
|
|
|
336
367
|
<style scoped></style>
|