Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import { useImage } from "@vueuse/core";
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
@@ -8,25 +12,37 @@ type Props = {
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { src } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const { isLoading } = useImage({ src: src.value });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarRoot
|
||||
class="inline-flex size-[45px] select-none items-center justify-center rounded-full align-middle"
|
||||
class="flex-center size-full select-none rounded-full align-middle overflow-hidden"
|
||||
>
|
||||
<AvatarImage
|
||||
class="h-full w-full rounded-full object-cover"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
<Skeleton
|
||||
v-if="isLoading"
|
||||
class="w-full !h-[110%] !rounded-full aspect-square"
|
||||
/>
|
||||
<AvatarFallback
|
||||
class="flex-center size-full text-sm font-medium rounded-full"
|
||||
:delay-ms="600"
|
||||
>
|
||||
<div class="size-full rounded-full flex-center">
|
||||
<Icon name="ci:profile" size="16" class="**:stroke-black" />
|
||||
</div>
|
||||
</AvatarFallback>
|
||||
<template v-else>
|
||||
<AvatarImage
|
||||
class="!size-full rounded-full object-cover"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
/>
|
||||
<AvatarFallback
|
||||
class="flex-center size-full text-sm font-medium rounded-full"
|
||||
:delay-ms="600"
|
||||
>
|
||||
<div class="size-full rounded-full flex-center">
|
||||
<Icon name="ci:profile" size="16" class="**:stroke-black" />
|
||||
</div>
|
||||
</AvatarFallback>
|
||||
</template>
|
||||
</AvatarRoot>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
variant: "solid",
|
||||
size: "lg",
|
||||
});
|
||||
const { variant, size } = toRefs(props);
|
||||
const { variant, size, loading } = toRefs(props);
|
||||
|
||||
// computed
|
||||
const classes = computed(() => {
|
||||
@@ -30,12 +30,15 @@ const classes = computed(() => {
|
||||
"btn-lg": size.value === "lg",
|
||||
"btn-md": size.value === "md",
|
||||
},
|
||||
{
|
||||
"pointer-events-none opacity-80 cursor-not-allowed": loading.value,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :class="classes">
|
||||
<button :class="classes" :disabled="loading">
|
||||
<Icon v-if="!loading && startIcon" :name="startIcon" />
|
||||
<slot v-if="!loading" />
|
||||
<Icon v-if="!loading && endIcon" :name="endIcon" />
|
||||
|
||||
@@ -34,7 +34,7 @@ const value = computed({
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<DatePicker
|
||||
format="jYYYY-jMM-jDD"
|
||||
format="YYYY-MM-DD"
|
||||
displayFormat="jYYYY-jMM-jDD"
|
||||
:editable="false"
|
||||
placeholder="وارد نشده"
|
||||
|
||||
@@ -34,9 +34,10 @@ const isHomePage = computed(() => route.path === "/");
|
||||
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
||||
<NuxtLink
|
||||
:to="{ name: 'profile' }"
|
||||
class="size-[1.6rem] flex items-center justify-center relative overflow-hidden rounded-full border-[1.2px] border-black"
|
||||
class="!size-[1.6rem] flex items-center justify-center relative overflow-hidden rounded-full border-[1.2px] border-black"
|
||||
>
|
||||
<Avatar
|
||||
class="!size-[1.6rem]"
|
||||
:src="account.profile_photo"
|
||||
:alt="
|
||||
account.first_name && account.last_name
|
||||
|
||||
@@ -37,7 +37,7 @@ const selectedValue = computed({
|
||||
|
||||
const classes = computed(() => {
|
||||
return [
|
||||
"flex items-center text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 flex-1 w-full outline-none",
|
||||
"flex items-center text-black justify-between cursor-text transition-all border-[1.5px] gap-3 grow-0 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 flex-1 w-full outline-none",
|
||||
{
|
||||
"input-solid": variant.value === "solid",
|
||||
"input-outlined": variant.value === "outlined",
|
||||
|
||||
@@ -13,8 +13,11 @@ defineProps<Props>();
|
||||
<template>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-col items-start">
|
||||
<div class="w-full flex items-center p-5">
|
||||
<div
|
||||
class="w-full flex items-center justify-between h-[3rem] pb-5 px-5"
|
||||
>
|
||||
<span class="typo-sub-h-lg">{{ title }}</span>
|
||||
<slot name="button" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-col border border-slate-200 rounded-xl">
|
||||
|
||||
@@ -23,9 +23,10 @@ withDefaults(defineProps<Props>(), {
|
||||
</div>
|
||||
<slot />
|
||||
<div
|
||||
v-if="error.$error"
|
||||
class="w-full typo-label-xs flex items-center py-1 px-3 rounded-md text-danger-600"
|
||||
>
|
||||
این فیلد الزامی است
|
||||
{{ error.$errors[0].$message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+84
-35
@@ -1,9 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useUpdateAccount from "~/composables/api/account/useUpdateAccount";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
modelValue: File | null;
|
||||
isShow: boolean;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
"update:modelValue": [value: File];
|
||||
"update:isShow": [value: boolean];
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { modelValue, isShow } = toRefs(props);
|
||||
|
||||
// emits
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// state
|
||||
|
||||
const isShow = ref(false);
|
||||
const visible = computed({
|
||||
get: () => isShow.value ?? false,
|
||||
set: (value: boolean) => emit("update:isShow", value),
|
||||
});
|
||||
|
||||
const currentProfile = ref("");
|
||||
const { addToast } = useToast();
|
||||
|
||||
const {
|
||||
open: openFileDialog,
|
||||
reset: resetFileDialog,
|
||||
onChange: onFileDialogChange,
|
||||
} = useFileDialog({
|
||||
accept: ".jpg, .jpeg, .png",
|
||||
directory: false,
|
||||
});
|
||||
|
||||
const avatars = ref([
|
||||
"/avatars/1.jpg",
|
||||
@@ -11,42 +50,41 @@ const avatars = ref([
|
||||
"/avatars/3.jpg",
|
||||
"/avatars/4.jpg",
|
||||
"/avatars/5.jpg",
|
||||
"/avatars/1.jpg",
|
||||
"/avatars/2.jpg",
|
||||
"/avatars/3.jpg",
|
||||
"/avatars/4.jpg",
|
||||
"/avatars/5.jpg",
|
||||
"/avatars/1.jpg",
|
||||
"/avatars/2.jpg",
|
||||
"/avatars/3.jpg",
|
||||
"/avatars/4.jpg",
|
||||
"/avatars/5.jpg",
|
||||
"/avatars/1.jpg",
|
||||
"/avatars/2.jpg",
|
||||
"/avatars/3.jpg",
|
||||
"/avatars/4.jpg",
|
||||
"/avatars/5.jpg",
|
||||
"/avatars/1.jpg",
|
||||
"/avatars/2.jpg",
|
||||
"/avatars/3.jpg",
|
||||
"/avatars/4.jpg",
|
||||
"/avatars/5.jpg",
|
||||
"/avatars/5.jpg",
|
||||
"/avatars/1.jpg",
|
||||
"/avatars/2.jpg",
|
||||
"/avatars/3.jpg",
|
||||
"/avatars/4.jpg",
|
||||
"/avatars/5.jpg",
|
||||
"/avatars/1.jpg",
|
||||
"/avatars/2.jpg",
|
||||
"/avatars/3.jpg",
|
||||
"/avatars/4.jpg",
|
||||
"/avatars/5.jpg",
|
||||
]);
|
||||
|
||||
// queries
|
||||
|
||||
const { isPending: updateAccountIsPending } = useUpdateAccount();
|
||||
|
||||
// computed
|
||||
|
||||
const currentProfile = computed({
|
||||
get: () =>
|
||||
!!modelValue.value ? URL.createObjectURL(modelValue.value) : null,
|
||||
set: (value: File) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
// methods
|
||||
|
||||
onFileDialogChange((files: any) => {
|
||||
const file = files[0];
|
||||
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
addToast({
|
||||
message: "محدودیت حجم فایل حداکثر ۲ مگابایت می باشد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
emit("update:modelValue", file);
|
||||
resetFileDialog();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="isShow" title="عکس پروفایل" icon="bi:image" iconSize="20">
|
||||
<Modal v-model="visible" title="عکس پروفایل" icon="bi:image" iconSize="20">
|
||||
<template #trigger>
|
||||
<button
|
||||
class="bg-black text-slate-100 rounded-full p-2 flex-center absolute -bottom-0 -right-0"
|
||||
@@ -105,7 +143,18 @@ const avatars = ref([
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
<Button class="rounded-full">آپلود عکس شما</Button>
|
||||
<Button
|
||||
class="rounded-full w-[8rem]"
|
||||
@click="openFileDialog"
|
||||
:loading="updateAccountIsPending"
|
||||
size="md"
|
||||
>
|
||||
<Icon
|
||||
v-if="updateAccountIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
/>
|
||||
<span v-else>آپلود عکس شما</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type UpdateAccountRequest = {
|
||||
profile_photo?: File | null;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
phone?: string;
|
||||
gender?: string | undefined;
|
||||
email?: string;
|
||||
birth_date?: string;
|
||||
};
|
||||
|
||||
const useUpdateAccount = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// method
|
||||
|
||||
const handleUpdateAccount = async (params: UpdateAccountRequest) => {
|
||||
const { data } = await axios.patch(
|
||||
API_ENDPOINTS.account.update,
|
||||
{
|
||||
...params,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateAccountRequest) => handleUpdateAccount(data),
|
||||
});
|
||||
};
|
||||
|
||||
export default useUpdateAccount;
|
||||
@@ -0,0 +1,39 @@
|
||||
export const useObjectTrack = (object: Ref) => {
|
||||
// state
|
||||
|
||||
const isNotEqual = ref(false);
|
||||
|
||||
const { history, reset, clear } = useRefHistory(object, {
|
||||
deep: true,
|
||||
});
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => history.value,
|
||||
(newHistory) => {
|
||||
if (newHistory.length < 2) {
|
||||
isNotEqual.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const initial = newHistory[0].snapshot;
|
||||
const current = newHistory[newHistory.length - 1].snapshot;
|
||||
|
||||
const hasChanges = Object.keys(initial).some(
|
||||
(key) =>
|
||||
JSON.stringify(current[key]) !==
|
||||
JSON.stringify(initial[key])
|
||||
);
|
||||
|
||||
isNotEqual.value = hasChanges;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
return {
|
||||
isNotEqual,
|
||||
reset,
|
||||
clear,
|
||||
};
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export const API_ENDPOINTS = {
|
||||
get_all: "/accounts/address/list",
|
||||
delete: "/accounts/address/delete",
|
||||
},
|
||||
update: "/accounts/profile",
|
||||
},
|
||||
product: {
|
||||
comments: "/products/comments",
|
||||
|
||||
@@ -8,7 +8,9 @@ definePageMeta({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="w-full flex flex-col gap-10">
|
||||
<ProfilePageTitle title="آدرس های شما" icon="bi:map" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useVuelidate from "@vuelidate/core";
|
||||
import { helpers, required, minLength, email } from "@vuelidate/validators";
|
||||
import useGetAccount from "~/composables/api/account/useGetAccount";
|
||||
import useUpdateAccount, {
|
||||
type UpdateAccountRequest,
|
||||
} from "~/composables/api/account/useUpdateAccount";
|
||||
import { useObjectTrack } from "~/composables/global/useObjectTrack";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
// meta
|
||||
|
||||
@@ -12,15 +20,26 @@ definePageMeta({
|
||||
|
||||
// state
|
||||
|
||||
const personalData = ref({
|
||||
name: "",
|
||||
last_name: "",
|
||||
phone: "",
|
||||
gender: undefined,
|
||||
email: "",
|
||||
birthDate: "",
|
||||
const { data: account } = useGetAccount();
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const personalData = ref<UpdateAccountRequest>({
|
||||
profile_photo: null,
|
||||
first_name: account.value?.first_name ?? "",
|
||||
last_name: account.value?.last_name ?? "",
|
||||
phone: account.value?.phone ?? "",
|
||||
gender: account.value?.gender ?? undefined,
|
||||
email: account.value?.email ?? "",
|
||||
birth_date: account.value?.birth_date ?? "",
|
||||
});
|
||||
|
||||
const profilePictureModalIsShow = ref(false);
|
||||
|
||||
const { isNotEqual, clear: clearObjectTracker } = useObjectTrack(personalData);
|
||||
|
||||
const alises = ref([
|
||||
"شکارچی",
|
||||
"آیفون باز",
|
||||
@@ -31,7 +50,109 @@ const alises = ref([
|
||||
|
||||
// queries
|
||||
|
||||
const { data: account } = useGetAccount();
|
||||
const { mutateAsync: updateAccount, isPending: updateAccountIsPending } =
|
||||
useUpdateAccount();
|
||||
|
||||
// computed
|
||||
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
first_name: {
|
||||
required: helpers.withMessage("فیلد نام الزامی می باشد", required),
|
||||
minLength: helpers.withMessage(
|
||||
"فیلد نام حداقل ۳ کرکتر می باشد",
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
last_name: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد نام خانوادگی الزامی می باشد",
|
||||
required
|
||||
),
|
||||
minLength: helpers.withMessage(
|
||||
"فیلد نام خانوادگی حداقل ۳ کرکتر می باشد",
|
||||
minLength(3)
|
||||
),
|
||||
},
|
||||
phone: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد شماره تلفن الزامی می باشد",
|
||||
required
|
||||
),
|
||||
phoneValidator: helpers.withMessage(
|
||||
"شماره تلفن وارد شده معتبر نمی باشد",
|
||||
helpers.regex(/^0?[1-9][0-9]{9}$/)
|
||||
),
|
||||
},
|
||||
gender: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد جنسیت الزامی می باشد",
|
||||
required
|
||||
),
|
||||
},
|
||||
email: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد حساب الکترونیکی الزامی می باشد",
|
||||
required
|
||||
),
|
||||
email: helpers.withMessage(
|
||||
"حساب الکترونیکی وارد شده معتبر نمی باشد",
|
||||
email
|
||||
),
|
||||
},
|
||||
birth_date: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد تاریخ تولد الزامی می باشد",
|
||||
required
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const formValidator$ = useVuelidate(formRules, personalData);
|
||||
|
||||
// methods
|
||||
|
||||
const updateData = () => {
|
||||
updateAccount(
|
||||
{ ...personalData.value },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEYS.account],
|
||||
});
|
||||
|
||||
addToast({
|
||||
message: "اطلاعات با موفقیت تغییر یافت",
|
||||
options: {
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
clearObjectTracker();
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در تغییر اطلاعات رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = (withValidation: boolean) => {
|
||||
if (withValidation) {
|
||||
formValidator$.value.$validate();
|
||||
if (!formValidator$.value.$errors.length) {
|
||||
updateData();
|
||||
}
|
||||
} else {
|
||||
updateData();
|
||||
profilePictureModalIsShow.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -57,7 +178,11 @@ const { data: account } = useGetAccount();
|
||||
"
|
||||
/>
|
||||
|
||||
<PictureModal />
|
||||
<ProfilePictureModal
|
||||
v-model:is-show="profilePictureModalIsShow"
|
||||
v-model="personalData.profile_photo!"
|
||||
@update:model-value="() => handleSubmit(false)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
@@ -100,50 +225,88 @@ const { data: account } = useGetAccount();
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSection title="اطلاعات شما">
|
||||
<template #button>
|
||||
<Button
|
||||
v-if="isNotEqual"
|
||||
:loading="updateAccountIsPending"
|
||||
class="rounded-full w-[6.5rem]"
|
||||
@click="handleSubmit(true)"
|
||||
size="md"
|
||||
>
|
||||
<Icon
|
||||
v-if="updateAccountIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
/>
|
||||
<span v-else> ثبت تغییرات </span>
|
||||
</Button>
|
||||
</template>
|
||||
<div
|
||||
class="w-full grid grid-cols-1 lg:grid-cols-2 gap-x-3 gap-y-5"
|
||||
>
|
||||
<PersonalDataField id="personal-data-name" label="نام">
|
||||
<Input v-model="personalData.name" variant="outlined" />
|
||||
<PersonalDataField
|
||||
id="personal-data-name"
|
||||
label="نام"
|
||||
:error="formValidator$.first_name"
|
||||
>
|
||||
<Input
|
||||
v-model="personalData.first_name!"
|
||||
variant="outlined"
|
||||
:error="formValidator$.first_name.$error"
|
||||
/>
|
||||
</PersonalDataField>
|
||||
<PersonalDataField
|
||||
id="personal-data-last-name"
|
||||
label="نام خانوادگی"
|
||||
:error="formValidator$.last_name"
|
||||
>
|
||||
<Input
|
||||
v-model="personalData.last_name"
|
||||
v-model="personalData.last_name!"
|
||||
variant="outlined"
|
||||
:error="formValidator$.last_name.$error"
|
||||
/>
|
||||
</PersonalDataField>
|
||||
<PersonalDataField id="personal-data-gender" label="جنسیت">
|
||||
<PersonalDataField
|
||||
id="personal-data-gender"
|
||||
label="جنسیت"
|
||||
:error="formValidator$.gender"
|
||||
>
|
||||
<Select
|
||||
v-model="personalData.gender"
|
||||
v-model="personalData.gender!"
|
||||
:options="['مرد', 'زن']"
|
||||
variant="outlined"
|
||||
:error="formValidator$.gender.$error"
|
||||
/>
|
||||
</PersonalDataField>
|
||||
<PersonalDataField
|
||||
id="personal-data-birth-date"
|
||||
label="تاریخ تولد"
|
||||
:error="formValidator$.birth_date"
|
||||
>
|
||||
<Datepicker v-model="personalData.birthDate" />
|
||||
<Datepicker
|
||||
v-model="personalData.birth_date!"
|
||||
:error="formValidator$.birth_date.$error"
|
||||
/>
|
||||
</PersonalDataField>
|
||||
<PersonalDataField
|
||||
id="personal-data-phone"
|
||||
label="تلفن همراه"
|
||||
:error="formValidator$.phone"
|
||||
>
|
||||
<Input
|
||||
v-model="personalData.phone"
|
||||
v-model="personalData.phone!"
|
||||
variant="outlined"
|
||||
:error="formValidator$.phone.$error"
|
||||
/>
|
||||
</PersonalDataField>
|
||||
<PersonalDataField
|
||||
id="personal-email"
|
||||
label="حساب الکترونیکی"
|
||||
:error="formValidator$.email"
|
||||
>
|
||||
<Input
|
||||
v-model="personalData.email"
|
||||
v-model="personalData.email!"
|
||||
variant="outlined"
|
||||
:error="formValidator$.email.$error"
|
||||
/>
|
||||
</PersonalDataField>
|
||||
</div>
|
||||
|
||||
+50
-48
@@ -1,5 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// import
|
||||
|
||||
import { helpers, required } from "@vuelidate/validators";
|
||||
@@ -21,7 +20,7 @@ type LoginInfo = {
|
||||
// meta
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["check-is-not-logged-in"]
|
||||
middleware: ["check-is-not-logged-in"],
|
||||
});
|
||||
|
||||
// state
|
||||
@@ -39,13 +38,16 @@ const formRules = computed(() => {
|
||||
return {
|
||||
phone: {
|
||||
required: helpers.withMessage("Phone is required", required),
|
||||
phoneValidator: helpers.regex(/^[1-9][0-9]{9}$/)
|
||||
}
|
||||
phoneValidator: helpers.withMessage(
|
||||
"شماره تلفن وارد شده معتبر نمی باشد",
|
||||
helpers.regex(/^[1-9][0-9]{9}$/)
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const loginInfo = ref<LoginInfo>({
|
||||
phone: ""
|
||||
phone: "",
|
||||
});
|
||||
|
||||
const formValidator$ = useVuelidate(formRules, loginInfo);
|
||||
@@ -54,13 +56,17 @@ const {
|
||||
timer: otpBlockerTimePassed,
|
||||
start: startOtpBlocker,
|
||||
reset: resetOtpBlocker,
|
||||
isPending: isResendOtpBlocked
|
||||
isPending: isResendOtpBlocked,
|
||||
} = useTimer({
|
||||
duration: 5000
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
const { mutateAsync: sendOtp, isPending: sendOtpIsPending } = useOtp();
|
||||
const { mutateAsync: signIn, isPending: signInIsPending, status: signInStatus } = useSignIn();
|
||||
const {
|
||||
mutateAsync: signIn,
|
||||
isPending: signInIsPending,
|
||||
status: signInStatus,
|
||||
} = useSignIn();
|
||||
|
||||
// computed
|
||||
|
||||
@@ -68,24 +74,23 @@ const sendOtpHandler = async () => {
|
||||
if (!sendOtpIsPending.value) {
|
||||
try {
|
||||
await sendOtp({
|
||||
phone: `0${loginInfo.value.phone}`
|
||||
phone: `0${loginInfo.value.phone}`,
|
||||
});
|
||||
|
||||
addToast({
|
||||
message: "کد برای شما ارسال شد",
|
||||
options: {
|
||||
status: "success"
|
||||
}
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
|
||||
showOtp.value = true;
|
||||
|
||||
} catch (e) {
|
||||
addToast({
|
||||
message: "مشکلی پیش آمده",
|
||||
options: {
|
||||
status: "error"
|
||||
}
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -102,19 +107,19 @@ const handleLogin = async () => {
|
||||
try {
|
||||
const response = await signIn({
|
||||
otp: otpCode.value.join(""),
|
||||
phone: `0${loginInfo.value.phone}`
|
||||
phone: `0${loginInfo.value.phone}`,
|
||||
});
|
||||
|
||||
updateToken(response.access);
|
||||
updateRefreshToken(response.refresh);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await refetchAccount();
|
||||
|
||||
addToast({
|
||||
message: "با موفقیت وارد شدید",
|
||||
options: {
|
||||
status: "success"
|
||||
}
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
|
||||
navigateTo("/");
|
||||
@@ -141,22 +146,15 @@ const resetForm = () => {
|
||||
otpCode.value = [];
|
||||
showOtp.value = false;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container min-h-[700px] flex flex-col items-center justify-center">
|
||||
<h1 class="typo-hero-2">
|
||||
فرم ورود
|
||||
</h1>
|
||||
<form
|
||||
@submit.prevent
|
||||
class="max-w-[500px] w-full mt-12"
|
||||
>
|
||||
<div
|
||||
v-if="!showOtp"
|
||||
class="flex items-center gap-2 w-full"
|
||||
>
|
||||
<div
|
||||
class="container min-h-[700px] flex flex-col items-center justify-center"
|
||||
>
|
||||
<h1 class="typo-hero-2">فرم ورود</h1>
|
||||
<form @submit.prevent class="max-w-[500px] w-full mt-12">
|
||||
<div v-if="!showOtp" class="flex items-center gap-2 w-full">
|
||||
<Input
|
||||
data-testid="phone-input"
|
||||
class="w-full"
|
||||
@@ -166,20 +164,28 @@ const resetForm = () => {
|
||||
:error="formValidator$.phone.$error"
|
||||
>
|
||||
<template #startItem>
|
||||
<span class="text-slate-500">
|
||||
+98
|
||||
</span>
|
||||
<span class="text-slate-500"> +98 </span>
|
||||
</template>
|
||||
</Input>
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon class="translate-y-[-1px]" name="twemoji:flag-iran" size="24" />
|
||||
<Icon
|
||||
class="translate-y-[-1px]"
|
||||
name="twemoji:flag-iran"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OtpInput
|
||||
v-else
|
||||
v-model="otpCode"
|
||||
:status="signInStatus === 'success' ? 'success' : signInStatus === 'error' ? 'error' : 'idle'"
|
||||
:status="
|
||||
signInStatus === 'success'
|
||||
? 'success'
|
||||
: signInStatus === 'error'
|
||||
? 'error'
|
||||
: 'idle'
|
||||
"
|
||||
:disabled="signInIsPending || sendOtpIsPending"
|
||||
:autofocus="true"
|
||||
@complete="handleLogin"
|
||||
@@ -197,10 +203,7 @@ const resetForm = () => {
|
||||
ارسال کد
|
||||
</Button>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center w-full gap-4 mt-4"
|
||||
>
|
||||
<div v-else class="flex items-center w-full gap-4 mt-4">
|
||||
<Button
|
||||
class="rounded-full w-full mt-4"
|
||||
type="button"
|
||||
@@ -215,7 +218,11 @@ const resetForm = () => {
|
||||
type="submit"
|
||||
@click="resendOtp"
|
||||
:loading="signInIsPending || sendOtpIsPending"
|
||||
:disabled="signInIsPending || isResendOtpBlocked || sendOtpIsPending"
|
||||
:disabled="
|
||||
signInIsPending ||
|
||||
isResendOtpBlocked ||
|
||||
sendOtpIsPending
|
||||
"
|
||||
>
|
||||
ارسال مجدد کد
|
||||
{{ isResendOtpBlocked ? otpBlockerTimePassed : "" }}
|
||||
@@ -223,13 +230,8 @@ const resetForm = () => {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 justify-center mt-6">
|
||||
<span>
|
||||
بازگشت به فروشگاه
|
||||
</span>
|
||||
<Icon
|
||||
name="ci:left-rotation"
|
||||
size="24"
|
||||
/>
|
||||
<span> بازگشت به فروشگاه </span>
|
||||
<Icon name="ci:left-rotation" size="24" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Vendored
+4
-2
@@ -15,11 +15,13 @@ declare global {
|
||||
};
|
||||
|
||||
type Account = {
|
||||
profile_photo: File | null;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
profile_photo: string;
|
||||
phone: string;
|
||||
gender: string | undefined;
|
||||
email: string;
|
||||
birth_date: string;
|
||||
};
|
||||
|
||||
type Product = {
|
||||
|
||||
Reference in New Issue
Block a user