chore: Fix the sign-up form
This commit is contained in:
@@ -17,7 +17,7 @@ const props = defineProps({
|
||||
messageType: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
validator: value => ['info', 'error', 'success'].includes(value),
|
||||
validator: value => !value || ['info', 'error', 'success'].includes(value),
|
||||
},
|
||||
min: { type: String, default: '' },
|
||||
max: { type: String, default: '' },
|
||||
@@ -39,25 +39,26 @@ const uniqueId = computed(() => props.id || `input-${uid}`);
|
||||
const isFocused = ref(false);
|
||||
const inputRef = ref(null);
|
||||
|
||||
const messageClass = computed(() => {
|
||||
switch (props.messageType) {
|
||||
case 'error':
|
||||
return 'text-n-ruby-9 dark:text-n-ruby-9';
|
||||
case 'success':
|
||||
return 'text-n-teal-10 dark:text-n-teal-10';
|
||||
default:
|
||||
return 'text-n-slate-11 dark:text-n-slate-11';
|
||||
}
|
||||
});
|
||||
const MESSAGE_STYLES = {
|
||||
error: 'text-n-ruby-9 dark:text-n-ruby-9',
|
||||
success: 'text-n-teal-10 dark:text-n-teal-10',
|
||||
info: 'text-n-slate-11 dark:text-n-slate-11',
|
||||
};
|
||||
|
||||
const inputOutlineClass = computed(() => {
|
||||
switch (props.messageType) {
|
||||
case 'error':
|
||||
return 'outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 disabled:outline-n-ruby-8 dark:disabled:outline-n-ruby-8';
|
||||
default:
|
||||
return 'outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 disabled:outline-n-weak dark:disabled:outline-n-weak focus:outline-n-brand dark:focus:outline-n-brand';
|
||||
}
|
||||
});
|
||||
const OUTLINE_STYLES = {
|
||||
error:
|
||||
'outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 disabled:outline-n-ruby-8 dark:disabled:outline-n-ruby-8',
|
||||
default:
|
||||
'outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 disabled:outline-n-weak dark:disabled:outline-n-weak focus:outline-n-brand dark:focus:outline-n-brand',
|
||||
};
|
||||
|
||||
const messageClass = computed(
|
||||
() => MESSAGE_STYLES[props.messageType || 'info'] || MESSAGE_STYLES.info
|
||||
);
|
||||
|
||||
const inputOutlineClass = computed(
|
||||
() => OUTLINE_STYLES[props.messageType || 'info'] || OUTLINE_STYLES.default
|
||||
);
|
||||
|
||||
const handleInput = event => {
|
||||
let value = event.target.value;
|
||||
@@ -145,7 +146,7 @@ onMounted(() => {
|
||||
/>
|
||||
<p
|
||||
v-if="message"
|
||||
class="min-w-0 mt-1 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||
class="min-w-0 mt-0.5 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||
:class="messageClass"
|
||||
>
|
||||
{{ message }}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, email, sameAs } from '@vuelidate/validators';
|
||||
import { required, minLength, email } from '@vuelidate/validators';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
import SimpleDivider from '../../../../../components/Divider/SimpleDivider.vue';
|
||||
import FormInput from '../../../../../components/Form/Input.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import { isValidPassword } from 'shared/helpers/Validators';
|
||||
@@ -61,6 +61,25 @@ const rules = computed(() => ({
|
||||
|
||||
const v$ = useVuelidate(rules, { credentials });
|
||||
|
||||
const blurredFields = reactive({
|
||||
fullName: false,
|
||||
accountName: false,
|
||||
email: false,
|
||||
password: false,
|
||||
});
|
||||
|
||||
const shouldShowError = fieldName => {
|
||||
// only when field has content and was blurred
|
||||
const field = v$.value.credentials[fieldName];
|
||||
const hasValue = credentials[fieldName] && credentials[fieldName].length > 0;
|
||||
return hasValue && blurredFields[fieldName] && field.$error;
|
||||
};
|
||||
|
||||
const shouldShowPasswordRequirementError = computed(() => {
|
||||
const hasValue = credentials.password && credentials.password.length > 0;
|
||||
return hasValue && v$.value.credentials.password.$error;
|
||||
});
|
||||
|
||||
const globalConfig = computed(() => store.getters['globalConfig/get']);
|
||||
|
||||
const termsLink = computed(() => {
|
||||
@@ -131,10 +150,6 @@ const passwordRequirementItems = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const passwordRequirementsMet = computed(() => {
|
||||
return Object.values(passwordRequirements.value).every(Boolean);
|
||||
});
|
||||
|
||||
const resetCaptcha = () => {
|
||||
if (!globalConfig.value.hCaptchaSiteKey) return;
|
||||
hCaptcha.value.reset();
|
||||
@@ -152,7 +167,10 @@ const submit = async () => {
|
||||
// For invisible captcha, there is no user input. We have to execute it if when
|
||||
// the user submits the form. If 99.9% Passive mode is enabled, the users won't
|
||||
// be prompted to solve the captcha
|
||||
if (globalConfig.value.hCaptchaSiteKey && !credentials.hCaptchaClientResponse) {
|
||||
if (
|
||||
globalConfig.value.hCaptchaSiteKey &&
|
||||
!credentials.hCaptchaClientResponse
|
||||
) {
|
||||
hCaptcha.value.execute();
|
||||
return;
|
||||
}
|
||||
@@ -170,7 +188,7 @@ const submit = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onRecaptchaVerified = (token) => {
|
||||
const onRecaptchaVerified = token => {
|
||||
credentials.hCaptchaClientResponse = token;
|
||||
didCaptchaReset.value = false;
|
||||
v$.value.$touch();
|
||||
@@ -183,14 +201,37 @@ const onCaptchaExpired = () => {
|
||||
didCaptchaReset.value = true;
|
||||
};
|
||||
|
||||
const onCaptchaError = (error) => {
|
||||
const onCaptchaError = () => {
|
||||
credentials.hCaptchaClientResponse = '';
|
||||
didCaptchaReset.value = true;
|
||||
useAlert(t('SET_NEW_PASSWORD.CAPTCHA.ERROR'));
|
||||
};
|
||||
|
||||
const handleFieldFocus = fieldName => {
|
||||
// Clear blur state when user focuses field again
|
||||
blurredFields[fieldName] = false;
|
||||
};
|
||||
|
||||
const handleFieldBlur = fieldName => {
|
||||
// Mark field as blurred and touch for validation
|
||||
blurredFields[fieldName] = true;
|
||||
v$.value.credentials[fieldName].$touch();
|
||||
};
|
||||
|
||||
const handlePasswordInput = () => {
|
||||
// Touch password field for real-time validation in requirements panel
|
||||
if (credentials.password && credentials.password.length > 0) {
|
||||
v$.value.credentials.password.$touch();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordFocus = () => {
|
||||
handleFieldFocus('password');
|
||||
isPasswordFocused.value = true;
|
||||
};
|
||||
|
||||
const handlePasswordBlur = () => {
|
||||
v$.value.credentials.password.$touch();
|
||||
handleFieldBlur('password');
|
||||
// Delay hiding requirements to allow submit button click to register
|
||||
setTimeout(() => {
|
||||
isPasswordFocused.value = false;
|
||||
@@ -201,81 +242,122 @@ const handlePasswordBlur = () => {
|
||||
<template>
|
||||
<div class="flex-1 px-1 overflow-auto">
|
||||
<form class="space-y-3" @submit.prevent="submit">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<FormInput
|
||||
<div class="flex items-start gap-2">
|
||||
<Input
|
||||
v-model="credentials.fullName"
|
||||
name="full_name"
|
||||
autocomplete="name"
|
||||
class="flex-1"
|
||||
:class="{ error: v$.credentials.fullName.$error }"
|
||||
:label="$t('REGISTER.FULL_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.fullName.$error"
|
||||
:error-message="$t('REGISTER.FULL_NAME.ERROR')"
|
||||
@blur="v$.credentials.fullName.$touch"
|
||||
:message-type="shouldShowError('fullName') ? 'error' : ''"
|
||||
:message="
|
||||
shouldShowError('fullName') ? $t('REGISTER.FULL_NAME.ERROR') : ''
|
||||
"
|
||||
@focus="handleFieldFocus('fullName')"
|
||||
@blur="handleFieldBlur('fullName')"
|
||||
/>
|
||||
<FormInput
|
||||
<Input
|
||||
v-model="credentials.accountName"
|
||||
name="account_name"
|
||||
autocomplete="organization"
|
||||
class="flex-1"
|
||||
:class="{ error: v$.credentials.accountName.$error }"
|
||||
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.COMPANY_NAME.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.accountName.$error"
|
||||
:error-message="$t('REGISTER.COMPANY_NAME.ERROR')"
|
||||
@blur="v$.credentials.accountName.$touch"
|
||||
:message-type="shouldShowError('accountName') ? 'error' : ''"
|
||||
:message="
|
||||
shouldShowError('accountName')
|
||||
? $t('REGISTER.COMPANY_NAME.ERROR')
|
||||
: ''
|
||||
"
|
||||
@focus="handleFieldFocus('accountName')"
|
||||
@blur="handleFieldBlur('accountName')"
|
||||
/>
|
||||
</div>
|
||||
<FormInput
|
||||
<Input
|
||||
v-model="credentials.email"
|
||||
type="email"
|
||||
name="email_address"
|
||||
:class="{ error: v$.credentials.email.$error }"
|
||||
autocomplete="email"
|
||||
:label="$t('REGISTER.EMAIL.LABEL')"
|
||||
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.email.$error"
|
||||
:error-message="$t('REGISTER.EMAIL.ERROR')"
|
||||
@blur="v$.credentials.email.$touch"
|
||||
:message-type="shouldShowError('email') ? 'error' : ''"
|
||||
:message="shouldShowError('email') ? $t('REGISTER.EMAIL.ERROR') : ''"
|
||||
@focus="handleFieldFocus('email')"
|
||||
@blur="handleFieldBlur('email')"
|
||||
/>
|
||||
<FormInput
|
||||
<Input
|
||||
v-model="credentials.password"
|
||||
type="password"
|
||||
name="password"
|
||||
:class="{ error: v$.credentials.password.$error }"
|
||||
autocomplete="new-password"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.password.$error"
|
||||
:message-type="shouldShowError('password') ? 'error' : ''"
|
||||
aria-describedby="password-requirements"
|
||||
@focus="isPasswordFocused = true"
|
||||
@input="handlePasswordInput"
|
||||
@focus="handlePasswordFocus"
|
||||
@blur="handlePasswordBlur"
|
||||
/>
|
||||
<div
|
||||
class="grid transition-all duration-300 ease-out"
|
||||
:class="isPasswordFocused ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
|
||||
class="grid transition-all duration-300 ease-in-out"
|
||||
:class="[
|
||||
isPasswordFocused ? 'grid-rows-[1fr] !mt-1' : 'grid-rows-[0fr] !mt-0',
|
||||
]"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<div
|
||||
v-show="isPasswordFocused"
|
||||
id="password-requirements"
|
||||
class="text-xs rounded-md px-4 py-3 outline outline-1 outline-n-weak bg-n-alpha-black2"
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-in-out"
|
||||
leave-active-class="transition-all duration-300 ease-in-out"
|
||||
enter-from-class="opacity-0 -translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 -translate-y-2"
|
||||
>
|
||||
<ul role="list" class="grid grid-cols-2 gap-1">
|
||||
<li
|
||||
v-for="item in passwordRequirementItems"
|
||||
:key="item.id"
|
||||
class="inline-flex gap-1 items-start"
|
||||
>
|
||||
<Icon
|
||||
class="flex-none flex-shrink-0 w-3 mt-0.5"
|
||||
:icon="item.met ? 'i-lucide-circle-check-big' : 'i-lucide-circle'"
|
||||
:class="item.met ? 'text-n-teal-10' : 'text-n-slate-10'"
|
||||
/>
|
||||
<div
|
||||
v-if="isPasswordFocused"
|
||||
id="password-requirements"
|
||||
class="text-xs rounded-lg px-3 py-2.5 outline outline-1 -outline-offset-1 outline-n-weak bg-n-alpha-black2"
|
||||
>
|
||||
<ul role="list" class="grid grid-cols-2 gap-1">
|
||||
<li
|
||||
v-for="item in passwordRequirementItems"
|
||||
:key="item.id"
|
||||
class="inline-flex gap-1 items-start"
|
||||
>
|
||||
<Icon
|
||||
class="flex-none flex-shrink-0 w-3 mt-0.5"
|
||||
:icon="
|
||||
item.met
|
||||
? 'i-lucide-circle-check'
|
||||
: shouldShowPasswordRequirementError
|
||||
? 'i-lucide-circle-x'
|
||||
: 'i-lucide-circle-dot'
|
||||
"
|
||||
:class="
|
||||
item.met
|
||||
? 'text-n-teal-9'
|
||||
: shouldShowPasswordRequirementError
|
||||
? 'text-n-ruby-9'
|
||||
: 'text-n-slate-10'
|
||||
"
|
||||
/>
|
||||
|
||||
<span :class="item.met ? 'text-n-slate-11' : 'text-n-slate-10'">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span
|
||||
:class="
|
||||
item.met
|
||||
? 'text-n-teal-11'
|
||||
: shouldShowPasswordRequirementError
|
||||
? 'text-n-ruby-11'
|
||||
: 'text-n-slate-11'
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="globalConfig.hCaptchaSiteKey" class="mb-3">
|
||||
|
||||
Reference in New Issue
Block a user