395 lines
14 KiB
Vue
395 lines
14 KiB
Vue
<template>
|
||
<div
|
||
class="bg-white dark:bg-dark-primary-800 flex items-center justify-center p-4"
|
||
>
|
||
<!-- Container -->
|
||
<div class="w-full max-w-md space-y-6">
|
||
<!-- Header -->
|
||
<div class="text-center space-y-1">
|
||
<div
|
||
class="w-12 h-12 mx-auto mb-3 dark:bg-primary-800 rounded-full flex items-center justify-center"
|
||
>
|
||
<!-- <span class="text-white text-lg font-bold">پ</span> -->
|
||
<img :src="useSystemTheme.logo.value" alt="" class="h-9 w-9" />
|
||
</div>
|
||
<h1 class="text-xl font-medium text-primary-900 dark:text-white">
|
||
ایجاد حساب کاربری
|
||
</h1>
|
||
<!-- <p class="text-xs text-primary-400 dark:text-primary-400">زیستبوم پژوهشگران</p> -->
|
||
</div>
|
||
|
||
<!-- Form -->
|
||
<div class="space-y-3">
|
||
<!-- Name & Last Name -->
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label
|
||
class="block text-xs font-medium text-primary-700 dark:text-primary-300 mb-1"
|
||
>نام</label
|
||
>
|
||
<input
|
||
v-model.trim="name"
|
||
type="text"
|
||
placeholder="نام"
|
||
class="w-full px-3 py-2 text-sm border border-primary-300 dark:border-primary-700 rounded focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-600 focus:border-transparent bg-white dark:bg-primary-800 text-primary-900 dark:text-white placeholder-primary-400 dark:placeholder-primary-500"
|
||
/>
|
||
<p v-if="showError(v$.name)" class="mt-1 text-xs text-red-500">
|
||
الزامی
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<label
|
||
class="block text-xs font-medium text-primary-700 dark:text-primary-300 mb-1"
|
||
>نام خانوادگی</label
|
||
>
|
||
<input
|
||
v-model.trim="last_name"
|
||
type="text"
|
||
placeholder="نام خانوادگی"
|
||
class="w-full px-3 py-2 text-sm border border-primary-300 dark:border-primary-700 rounded focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-600 focus:border-transparent bg-white dark:bg-primary-800 text-primary-900 dark:text-white placeholder-primary-400 dark:placeholder-primary-500"
|
||
/>
|
||
<p v-if="showError(v$.last_name)" class="mt-1 text-xs text-red-500">
|
||
الزامی
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Username -->
|
||
<div>
|
||
<label
|
||
class="block text-xs font-medium text-primary-700 dark:text-primary-300 mb-1"
|
||
>نام کاربری</label
|
||
>
|
||
<input
|
||
v-model.trim="username"
|
||
type="text"
|
||
placeholder="نام کاربری"
|
||
class="w-full px-5 py-2 text-sm border border-primary-300 dark:border-primary-700 rounded focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-600 focus:border-transparent bg-white dark:bg-primary-800 text-primary-900 dark:text-white placeholder-primary-400 dark:placeholder-primary-500"
|
||
dir="rtl"
|
||
/>
|
||
<p v-if="showError(v$.username)" class="mt-1 text-xs text-red-500">
|
||
الزامی
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Email -->
|
||
<div>
|
||
<label
|
||
class="block text-xs font-medium text-primary-700 dark:text-primary-300 mb-1"
|
||
>ایمیل</label
|
||
>
|
||
<input
|
||
v-model.trim="email"
|
||
type="email"
|
||
placeholder="example@domain.com"
|
||
class="w-full px-5 py-2 text-sm border border-primary-300 dark:border-primary-700 rounded focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-600 focus:border-transparent bg-white dark:bg-primary-800 text-primary-900 dark:text-white placeholder-primary-400 dark:placeholder-primary-500"
|
||
dir="rtl"
|
||
/>
|
||
<p v-if="showError(v$.email)" class="mt-1 text-xs text-red-500">
|
||
ایمیل معتبر
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Password -->
|
||
<div>
|
||
<label
|
||
class="block text-xs font-medium text-primary-700 dark:text-primary-300 mb-1"
|
||
>رمز عبور</label
|
||
>
|
||
<div class="relative">
|
||
<input
|
||
v-model.trim="password"
|
||
:type="passwordFieldType"
|
||
placeholder="••••••••"
|
||
class="w-full px-3 py-2 pr-10 text-sm border border-primary-300 dark:border-primary-700 rounded focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-600 focus:border-transparent bg-white dark:bg-primary-800 text-primary-900 dark:text-white placeholder-primary-400 dark:placeholder-primary-500"
|
||
dir="rtl"
|
||
@focus="isFocusedOnPassword = true"
|
||
@blur="isFocusedOnPassword = false"
|
||
/>
|
||
<button
|
||
type="button"
|
||
@click="togglePasswordVisibility"
|
||
class="absolute left-2 top-1/2 transform -translate-y-1/2 text-primary-400 dark:text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||
>
|
||
<!-- <span v-if="isPasswordVisible">👁</span>
|
||
<span v-else>👁🗨</span> -->
|
||
<UIcon
|
||
:name="
|
||
isPasswordVisible
|
||
? 'i-heroicons-eye-slash'
|
||
: 'i-heroicons-eye'
|
||
"
|
||
class="w-4 h-4 mt-1"
|
||
/>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Password rules -->
|
||
<div
|
||
v-if="isFocusedOnPassword"
|
||
class="mt-2 p-2 border border-primary-200 dark:border-primary-700 rounded bg-primary-50 dark:bg-primary-800 text-xs space-y-1"
|
||
>
|
||
<div :class="ruleClass(v$.password.minLength)">
|
||
• حداقل ۸ کاراکتر
|
||
</div>
|
||
<div :class="ruleClass(v$.password.hasLowerCase)">
|
||
• حرف کوچک انگلیسی
|
||
</div>
|
||
<div :class="ruleClass(v$.password.hasUpperCase)">
|
||
• حرف بزرگ انگلیسی
|
||
</div>
|
||
<div :class="ruleClass(v$.password.hasSpecialChar)">
|
||
• کاراکتر خاص (!@#$%^&*)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Repeat Password -->
|
||
<div>
|
||
<label
|
||
class="block text-xs font-medium text-primary-700 dark:text-primary-300 mb-1"
|
||
>تکرار رمز عبور</label
|
||
>
|
||
<div class="relative">
|
||
<input
|
||
v-model.trim="repassword"
|
||
:type="repasswordFieldType"
|
||
placeholder="••••••••"
|
||
class="w-full px-3 py-2 pr-10 text-sm border border-primary-300 dark:border-primary-700 rounded focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-600 focus:border-transparent bg-white dark:bg-primary-800 text-primary-900 dark:text-white placeholder-primary-400 dark:placeholder-primary-500"
|
||
dir="rtl"
|
||
/>
|
||
<button
|
||
type="button"
|
||
@click="toggleRepasswordVisibility"
|
||
class="absolute left-2 top-1/2 transform -translate-y-1/2 text-primary-400 dark:text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||
>
|
||
<!-- <span v-if="isRepasswordVisible">👁</span>
|
||
<span v-else>👁🗨</span> -->
|
||
<UIcon
|
||
:name="
|
||
isRepasswordVisible
|
||
? 'i-heroicons-eye-slash'
|
||
: 'i-heroicons-eye'
|
||
"
|
||
class="w-4 h-4 mt-1"
|
||
/>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CAPTCHA -->
|
||
<div>
|
||
<label
|
||
class="block text-xs font-medium text-primary-700 dark:text-primary-300 mb-1"
|
||
>کد امنیتی</label
|
||
>
|
||
<div class="flex gap-2">
|
||
<div class="flex-1">
|
||
<input
|
||
v-model.trim="captcha"
|
||
type="text"
|
||
placeholder="کد را وارد کنید"
|
||
class="w-full px-3 py-2 text-sm border border-primary-300 dark:border-primary-700 rounded focus:outline-none focus:ring-1 focus:ring-primary-400 dark:focus:ring-primary-600 focus:border-transparent bg-white dark:bg-primary-800 text-primary-900 dark:text-white placeholder-primary-400 dark:placeholder-primary-500 text-center"
|
||
dir="rtl"
|
||
/>
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<div
|
||
class="w-24 h-10 border border-primary-300 dark:border-primary-700 rounded overflow-hidden bg-primary-50 dark:bg-primary-800 flex items-center justify-center"
|
||
>
|
||
<img
|
||
:src="captchaImage"
|
||
alt="کد امنیتی"
|
||
class="w-full h-full object-contain"
|
||
/>
|
||
</div>
|
||
<button
|
||
@click="getCaptcha"
|
||
class="w-8 h-8 border border-primary-300 dark:border-primary-700 rounded flex items-center justify-center hover:bg-primary-50 dark:hover:bg-primary-800 text-primary-700 dark:text-primary-300"
|
||
>
|
||
↻
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p v-if="showError(v$.captcha)" class="mt-1 text-xs text-red-500">
|
||
الزامی
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Errors -->
|
||
<div
|
||
v-if="submitStatus === 'ERROR'"
|
||
class="text-xs text-red-500 space-y-0.5"
|
||
>
|
||
<p v-if="v$.password.required.$invalid">• رمز عبور الزامی است</p>
|
||
<p v-if="v$.password.minLength.$invalid">
|
||
• رمز عبور حداقل ۸ کاراکتر
|
||
</p>
|
||
<p v-if="v$.repassword.required.$invalid">• تکرار رمز الزامی است</p>
|
||
<p v-if="v$.repassword.sameAsPassword.$invalid">
|
||
• رمزها یکسان نیستند
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Register Error -->
|
||
<p v-if="registerError" class="text-xs text-red-500">
|
||
{{ registerError }}
|
||
</p>
|
||
|
||
<!-- Submit Button -->
|
||
<button
|
||
:disabled="loading"
|
||
@click="submitRegister"
|
||
class="w-full py-2.5 bg-primary-900 dark:bg-primary-800 text-white text-sm font-medium rounded hover:bg-primary-800 dark:hover:bg-primary-700 active:bg-primary-900 dark:active:bg-primary-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{{ loading ? "در حال ایجاد حساب..." : "ایجاد حساب" }}
|
||
</button>
|
||
|
||
<!-- Login Link -->
|
||
<div
|
||
class="text-center pt-4 border-t border-primary-200 dark:border-primary-700"
|
||
>
|
||
<p class="text-xs text-primary-500 dark:text-primary-400">
|
||
قبلاً حساب دارید؟
|
||
<a
|
||
href="/login"
|
||
class="text-primary-700 dark:text-primary-300 hover:text-primary-900 dark:hover:text-white font-medium"
|
||
>
|
||
ورود
|
||
</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from "vue";
|
||
import useVuelidate from "@vuelidate/core";
|
||
import { required, minLength, sameAs } from "@vuelidate/validators";
|
||
import { useNuxtApp } from "#app";
|
||
import { navigateTo } from "#imports";
|
||
import { composSystemTheme } from "@/composables/composSystemTheme";
|
||
|
||
const useSystemTheme = composSystemTheme();
|
||
|
||
const name = ref("");
|
||
const last_name = ref("");
|
||
const username = ref("");
|
||
const email = ref("");
|
||
const password = ref("");
|
||
const repassword = ref("");
|
||
const captcha = ref("");
|
||
|
||
const captchaImage = ref("");
|
||
const isFocusedOnPassword = ref(false);
|
||
const isPasswordVisible = ref(false);
|
||
const isRepasswordVisible = ref(false);
|
||
const submitStatus = ref(null);
|
||
const loading = ref(false);
|
||
const registerError = ref("");
|
||
|
||
const rules = {
|
||
name: { required },
|
||
last_name: { required },
|
||
username: { required },
|
||
email: { required },
|
||
password: {
|
||
required,
|
||
minLength: minLength(8),
|
||
hasLowerCase: (v) => /[a-z]/.test(v),
|
||
hasUpperCase: (v) => /[A-Z]/.test(v),
|
||
hasSpecialChar: (v) => /[!@#$%^&*]/.test(v),
|
||
},
|
||
repassword: {
|
||
required,
|
||
sameAsPassword: sameAs(password),
|
||
},
|
||
captcha: { required },
|
||
};
|
||
|
||
const v$ = useVuelidate(rules, {
|
||
name,
|
||
last_name,
|
||
username,
|
||
email,
|
||
password,
|
||
repassword,
|
||
captcha,
|
||
});
|
||
|
||
const passwordFieldType = computed(() =>
|
||
isPasswordVisible.value ? "text" : "password"
|
||
);
|
||
const repasswordFieldType = computed(() =>
|
||
isRepasswordVisible.value ? "text" : "password"
|
||
);
|
||
const togglePasswordVisibility = () =>
|
||
(isPasswordVisible.value = !isPasswordVisible.value);
|
||
const toggleRepasswordVisibility = () =>
|
||
(isRepasswordVisible.value = !isRepasswordVisible.value);
|
||
const showError = (field) => submitStatus.value === "ERROR" && field.$invalid;
|
||
const ruleClass = (rule) => (rule.$invalid ? "text-red-500" : "text-green-600");
|
||
|
||
const getCaptcha = async () => {
|
||
try {
|
||
const { $http } = useNuxtApp();
|
||
const res = await $http.getRequest("/login/captcha/makeimage");
|
||
captchaImage.value = `data:image/jpeg;base64,${res}`;
|
||
} catch (err) {
|
||
console.error(err);
|
||
captchaImage.value = "";
|
||
}
|
||
};
|
||
onMounted(getCaptcha);
|
||
|
||
const submitRegister = async () => {
|
||
const isValid = await v$.value.$validate();
|
||
if (!isValid) {
|
||
submitStatus.value = "ERROR";
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
submitStatus.value = "PENDING";
|
||
registerError.value = "";
|
||
|
||
try {
|
||
const { $http } = useNuxtApp();
|
||
const baseUrl = import.meta.env.VITE_AUTH_BASE_URL;
|
||
|
||
const res = await $http.postRequest("login/user/register", {
|
||
name: name.value,
|
||
last_name: last_name.value,
|
||
username: username.value,
|
||
email: email.value,
|
||
password: password.value,
|
||
captcha: captcha.value,
|
||
});
|
||
|
||
// ذخیره تو localStorage
|
||
const id_token = useStorage("id_token", "");
|
||
id_token.value = res.data.token;
|
||
|
||
const userStorage = useStorage("user", {});
|
||
userStorage.value = res.data;
|
||
|
||
navigateTo({
|
||
name: "DashboardBasePage",
|
||
});
|
||
} catch (err) {
|
||
registerError.value = err?.response?.data?.message || err.message;
|
||
await getCaptcha();
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
input:focus {
|
||
outline: none;
|
||
}
|
||
</style>
|