|
|
@@ -10,57 +10,56 @@
|
|
|
|
|
|
<p class="login-title">{{ $t("auth.enter_code_title") }}</p>
|
|
|
|
|
|
- <div class="email-box q-mb-lg">
|
|
|
- <q-icon name="mdi-email-outline" size="16px" class="q-mr-xs" />
|
|
|
- <span>{{ email }}</span>
|
|
|
+ <div class="code-input-container q-mb-md" @click="focusCodeInput">
|
|
|
+ <div class="code-slots row items-end justify-center">
|
|
|
+ <div
|
|
|
+ v-for="i in 6"
|
|
|
+ :key="i"
|
|
|
+ class="code-slot"
|
|
|
+ :class="{
|
|
|
+ 'code-slot--filled': (form.codigo?.length ?? 0) >= i,
|
|
|
+ 'code-slot--active': inputFocused && (form.codigo?.length ?? 0) === i - 1,
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <span class="code-char">{{ form.codigo?.[i - 1] ?? "" }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <input
|
|
|
+ ref="codeInputRef"
|
|
|
+ v-model="form.codigo"
|
|
|
+ class="code-hidden-input"
|
|
|
+ maxlength="6"
|
|
|
+ inputmode="numeric"
|
|
|
+ autocomplete="one-time-code"
|
|
|
+ :disabled="loading"
|
|
|
+ @focus="inputFocused = true"
|
|
|
+ @blur="inputFocused = false"
|
|
|
+ @input="onCodeInput"
|
|
|
+ @keydown="filterNonNumeric"
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
- <q-form
|
|
|
- ref="formRef"
|
|
|
- class="full-width"
|
|
|
- autocomplete="off"
|
|
|
- @submit="onSubmit"
|
|
|
- >
|
|
|
- <DefaultInput
|
|
|
- v-model="form.codigo"
|
|
|
- v-model:error="validationErrors.codigo"
|
|
|
- autofocus
|
|
|
- :label="$t('common.terms.code')"
|
|
|
- mask="######"
|
|
|
- :rules="[inputRules.required, (v) => (v && v.length === 6) || $t('validation.rules.code_length')]"
|
|
|
- >
|
|
|
- <template #append>
|
|
|
- <q-icon name="mdi-numeric" color="grey-5" />
|
|
|
- </template>
|
|
|
- </DefaultInput>
|
|
|
-
|
|
|
- <q-btn
|
|
|
- class="full-width q-mt-sm login-btn"
|
|
|
- color="violet-normal"
|
|
|
- :label="$t('auth.verify')"
|
|
|
- type="submit"
|
|
|
- unelevated
|
|
|
- :loading
|
|
|
- >
|
|
|
- <template #loading><q-spinner /></template>
|
|
|
- </q-btn>
|
|
|
- </q-form>
|
|
|
+ <div v-if="loading" class="q-mb-md">
|
|
|
+ <q-spinner color="violet-normal" size="24px" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <p class="email-text q-py-md">{{ email }}</p>
|
|
|
|
|
|
<q-btn
|
|
|
- flat
|
|
|
+ unelevated
|
|
|
:label="$t('common.actions.resend_email')"
|
|
|
color="violet-normal"
|
|
|
- class="q-mt-md resend-btn"
|
|
|
+ class="resend-btn"
|
|
|
:loading="resending"
|
|
|
@click="onResend"
|
|
|
/>
|
|
|
|
|
|
- <div class="column items-center q-mt-sm">
|
|
|
- <a href="#" class="login-link" @click.prevent="goToLogin">
|
|
|
+ <div class="column items-center q-mt-sm q-pt-md">
|
|
|
+ <a href="#" class="login-link q-py-md" @click.prevent="goToLogin">
|
|
|
{{ $t("auth.remember_password") }}
|
|
|
<strong>{{ $t("auth.do_login") }}</strong>
|
|
|
</a>
|
|
|
- <a href="#" class="login-link q-mt-xs" @click.prevent="goBack">
|
|
|
+ <a href="#" class="login-link q-mt-xs q-pt-md" @click.prevent="goBack">
|
|
|
<q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
|
|
|
{{ $t("auth.back_to_site") }}
|
|
|
</a>
|
|
|
@@ -71,42 +70,51 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, useTemplateRef } from "vue";
|
|
|
+import { ref, nextTick } from "vue";
|
|
|
import { useRouter, useRoute } from "vue-router";
|
|
|
import { useQuasar } from "quasar";
|
|
|
-import { useInputRules } from "src/composables/useInputRules";
|
|
|
import { useSubmitHandler } from "src/composables/useSubmitHandler";
|
|
|
import { verifyCode, forgotPassword } from "src/api/auth";
|
|
|
|
|
|
import Logo from "src/assets/logo_serprati.svg";
|
|
|
-import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
import UserTypeBadge from "./components/UserTypeBadge.vue";
|
|
|
|
|
|
const router = useRouter();
|
|
|
const route = useRoute();
|
|
|
const $q = useQuasar();
|
|
|
-const { inputRules } = useInputRules();
|
|
|
|
|
|
-const email = route.query.email ?? "";
|
|
|
-const tipo = route.query.tipo ?? "administrador";
|
|
|
+const email = route.query.email ?? "";
|
|
|
+const tipo = route.query.tipo ?? "administrador";
|
|
|
|
|
|
-const formRef = useTemplateRef("formRef");
|
|
|
-const resending = ref(false);
|
|
|
+const codeInputRef = ref(null);
|
|
|
+const inputFocused = ref(false);
|
|
|
+const resending = ref(false);
|
|
|
+const form = ref({ codigo: "" });
|
|
|
|
|
|
-const {
|
|
|
- loading,
|
|
|
- validationErrors,
|
|
|
- execute: submitForm,
|
|
|
-} = useSubmitHandler({
|
|
|
+const { loading, execute: submitForm } = useSubmitHandler({
|
|
|
onSuccess: () =>
|
|
|
router.push({ name: "ResetPasswordPage", query: { email, tipo, codigo: form.value.codigo } }),
|
|
|
- formRef,
|
|
|
+ onError: () => {
|
|
|
+ $q.notify({ type: "negative", message: "Código inválido. Verifique e tente novamente." });
|
|
|
+ form.value.codigo = "";
|
|
|
+ nextTick(() => codeInputRef.value?.focus());
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
-const form = ref({ codigo: null });
|
|
|
+const focusCodeInput = () => codeInputRef.value?.focus();
|
|
|
|
|
|
-const onSubmit = async () => {
|
|
|
- await submitForm(() => verifyCode(email, form.value.codigo));
|
|
|
+const filterNonNumeric = (e) => {
|
|
|
+ const allowed = ["Backspace", "Delete", "Tab", "ArrowLeft", "ArrowRight"];
|
|
|
+ if (!allowed.includes(e.key) && !/^\d$/.test(e.key)) {
|
|
|
+ e.preventDefault();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onCodeInput = async () => {
|
|
|
+ form.value.codigo = (form.value.codigo ?? "").replace(/\D/g, "").slice(0, 6);
|
|
|
+ if (form.value.codigo.length === 6) {
|
|
|
+ await submitForm(() => verifyCode(email, form.value.codigo));
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
const onResend = async () => {
|
|
|
@@ -171,33 +179,90 @@ const goBack = () => router.push({ name: "LoginPage" });
|
|
|
color: map.get($colors, "violet-normal");
|
|
|
font-size: 18px;
|
|
|
font-weight: 700;
|
|
|
- margin: 0 0 16px;
|
|
|
+ margin: 0 0 24px;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
-.email-box {
|
|
|
+.code-input-container {
|
|
|
+ position: relative;
|
|
|
+ cursor: text;
|
|
|
+ width: 100%;
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- background: map.get($colors, "violet-light");
|
|
|
- border-radius: 8px;
|
|
|
- padding: 8px 16px;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.code-hidden-input {
|
|
|
+ position: absolute;
|
|
|
+ opacity: 0;
|
|
|
+ pointer-events: none;
|
|
|
+ width: 1px;
|
|
|
+ height: 1px;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.code-slots {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.code-slot {
|
|
|
+ width: 40px;
|
|
|
+ height: 52px;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: center;
|
|
|
+ padding-bottom: 6px;
|
|
|
+ border-bottom: 2.5px solid map.get($colors, "violet-normal");
|
|
|
+ position: relative;
|
|
|
+ transition: border-color 0.15s;
|
|
|
+
|
|
|
+ &--filled {
|
|
|
+ border-bottom-color: map.get($colors, "violet-normal-hover");
|
|
|
+ }
|
|
|
+
|
|
|
+ &--active {
|
|
|
+ border-bottom-color: map.get($colors, "violet-normal-active");
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ bottom: 6px;
|
|
|
+ width: 1.5px;
|
|
|
+ height: 20px;
|
|
|
+ background: map.get($colors, "violet-normal");
|
|
|
+ animation: blink-cursor 1s step-end infinite;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.code-char {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: map.get($colors, "violet-normal");
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes blink-cursor {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0; }
|
|
|
+}
|
|
|
+
|
|
|
+.email-text {
|
|
|
color: map.get($colors, "violet-normal");
|
|
|
font-size: 14px;
|
|
|
font-weight: 500;
|
|
|
+ text-align: center;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
|
|
|
-.login-btn {
|
|
|
+.resend-btn {
|
|
|
+ font-size: 13px;
|
|
|
height: 44px;
|
|
|
- font-size: 15px;
|
|
|
font-weight: 600;
|
|
|
letter-spacing: 0.5px;
|
|
|
}
|
|
|
|
|
|
-.resend-btn {
|
|
|
- font-size: 13px;
|
|
|
- text-decoration: underline;
|
|
|
-}
|
|
|
-
|
|
|
.login-link {
|
|
|
color: map.get($colors, "violet-normal");
|
|
|
font-size: 13px;
|