auth
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
VITE_TOKEN_NAME = 'dzone_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
|
||||
# VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||
VITE_BASE_URL = 'http://192.168.1.109:4000'
|
||||
@@ -19,6 +19,7 @@ export default tseslint.config(
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-hooks/exhaustive-deps': false,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
|
||||
Generated
+349
-263
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -17,12 +17,14 @@
|
||||
"formik": "^2.4.6",
|
||||
"i18next": "^24.2.1",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"js-cookie": "^3.0.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-loading": "^2.0.3",
|
||||
"react-multi-date-picker": "^4.5.2",
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"react-toastify": "^11.0.3",
|
||||
"swiper": "^11.2.1",
|
||||
@@ -32,6 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/swiper": "^5.4.3",
|
||||
@@ -46,5 +49,6 @@
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677"
|
||||
}
|
||||
|
||||
Generated
+4124
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
+112
-7
@@ -5,6 +5,15 @@ import { I18nextProvider } from 'react-i18next'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import 'swiper/swiper-bundle.css';
|
||||
import FaJson from './langs/fa.json'
|
||||
import { useState } from 'react';
|
||||
import { Pages } from './config/Pages';
|
||||
import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
|
||||
import { useEffect } from 'react';
|
||||
import AuthRouter from './router/Auth';
|
||||
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { IApiErrorRepsonse } from './types/error.types';
|
||||
import { refreshToken } from './pages/auth/service/AuthService';
|
||||
import ToastContainer from './components/Toast';
|
||||
i18next.init({
|
||||
interpolation: { escapeValue: false },
|
||||
lng: 'fa',
|
||||
@@ -15,16 +24,112 @@ i18next.init({
|
||||
}
|
||||
})
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
isRefreshingToken?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
||||
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
||||
useEffect(() => {
|
||||
const checkLogin = async () => {
|
||||
const token = await getToken();
|
||||
if (token) {
|
||||
setIsLogin('isLogin')
|
||||
} else {
|
||||
setIsLogin('isNotLogin')
|
||||
if (window.location.href.split('auth').length === 1) {
|
||||
window.location.href = Pages.auth.login
|
||||
}
|
||||
}
|
||||
}
|
||||
checkLogin();
|
||||
}, []);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: async (error: IApiErrorRepsonse) => {
|
||||
if (error?.response?.status === 401) {
|
||||
try {
|
||||
// Use a flag to track if refresh is in progress
|
||||
if (window.isRefreshingToken) {
|
||||
// Wait for the refresh to complete
|
||||
await new Promise(resolve => {
|
||||
const checkComplete = setInterval(() => {
|
||||
if (!window.isRefreshingToken) {
|
||||
clearInterval(checkComplete);
|
||||
resolve(true);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the flag to indicate refresh is in progress
|
||||
window.isRefreshingToken = true;
|
||||
|
||||
try {
|
||||
const refreshTokenValue = await getRefreshToken();
|
||||
const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' });
|
||||
|
||||
if (data?.accessToken?.token) {
|
||||
// Save the new token
|
||||
await setToken(data?.accessToken?.token);
|
||||
|
||||
// If refresh token also returned, update it
|
||||
if (data.refreshToken?.token) {
|
||||
await setRefreshToken(data.refreshToken?.token);
|
||||
}
|
||||
|
||||
// Retry the failed request
|
||||
queryClient.invalidateQueries();
|
||||
return;
|
||||
} else {
|
||||
throw new Error('Invalid token response');
|
||||
}
|
||||
} finally {
|
||||
window.isRefreshingToken = false;
|
||||
}
|
||||
} catch (refreshError: unknown) {
|
||||
console.error('Token refresh failed:', refreshError);
|
||||
// Clear tokens and redirect to login
|
||||
await removeToken();
|
||||
await removeRefreshToken();
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false
|
||||
// staleTime: 86400000 // 1 day in milliseconds
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
<MainRouter />
|
||||
</I18nextProvider>
|
||||
</BrowserRouter>
|
||||
</>
|
||||
<>
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
{
|
||||
isLogin === 'checking' ?
|
||||
null
|
||||
:
|
||||
isLogin === 'isLogin' ?
|
||||
<MainRouter />
|
||||
:
|
||||
<AuthRouter />
|
||||
}
|
||||
<ToastContainer />
|
||||
</I18nextProvider>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
</>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type?: 'success' | 'error' | 'info';
|
||||
isExiting?: boolean;
|
||||
}
|
||||
|
||||
let addToast: (toast: Toast) => void;
|
||||
|
||||
const ToastContainer: React.FC = () => {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
// ثبت toast جدید
|
||||
const showToast = (toast: Toast) => {
|
||||
setToasts((prev) => [...prev, toast]);
|
||||
|
||||
// حذف خودکار بعد از ۳ ثانیه
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.map(t =>
|
||||
t.id === toast.id ? { ...t, isExiting: true } : t
|
||||
));
|
||||
|
||||
// حذف از DOM بعد از اتمام انیمیشن
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== toast.id));
|
||||
}, 300); // مدت زمان انیمیشن خروج
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// تخصیص تابع نمایش toast به متغیر سراسری
|
||||
useEffect(() => {
|
||||
addToast = showToast;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`px-4 flex items-center gap-2 backdrop-blur-2xl h-16 min-w-[300px] rounded-2xl shadow-md
|
||||
${toast.isExiting ? 'animate-slide-out-right' : 'animate-slide-in-right'} ${toast.type === 'success'
|
||||
? 'bg-white/70 text-black'
|
||||
: toast.type === 'error'
|
||||
? 'bg-white/70 text-black'
|
||||
: 'bg-white/70 text-black'
|
||||
}`}
|
||||
style={{
|
||||
animationFillMode: 'forwards',
|
||||
animationDuration: '0.3s',
|
||||
animationTimingFunction: 'ease-out'
|
||||
}}
|
||||
>
|
||||
{
|
||||
toast.type === 'success' ? <TickCircle className='size-5' color='green' /> :
|
||||
toast.type === 'error' ? <CloseCircle className='size-5' color='red' /> :
|
||||
<InfoCircle className='size-5' color='blue' />
|
||||
}
|
||||
{toast.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const toast = (message?: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
addToast({ id: Date.now().toString(), message: message || '', type });
|
||||
};
|
||||
|
||||
export default ToastContainer;
|
||||
+4
-8
@@ -1,5 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { Pages } from "./Pages";
|
||||
import { getToken } from "./func";
|
||||
// import { Pages } from "./Pages";
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_URL,
|
||||
@@ -8,18 +9,13 @@ const axiosInstance = axios.create({
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response.status === 401) {
|
||||
localStorage.removeItem(import.meta.env.VITE_TOKEN_NAME);
|
||||
window.location.href = Pages.auth.login;
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.request.use(async function (config) {
|
||||
const token = localStorage.getItem(import.meta.env.VITE_TOKEN_NAME)
|
||||
? localStorage.getItem(import.meta.env.VITE_TOKEN_NAME)
|
||||
: "";
|
||||
const tokenValue = await getToken();
|
||||
const token = tokenValue ? tokenValue : "";
|
||||
|
||||
config.headers.Authorization = "Bearer " + token;
|
||||
return config;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import Cookie from "js-cookie";
|
||||
|
||||
export function isEmail(input: string): boolean {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(input);
|
||||
}
|
||||
|
||||
export function NumberFormat(number: number): string {
|
||||
return Intl.NumberFormat("fa-IR").format(number);
|
||||
// return Intl.NumberFormat("fa-IR").format(number);
|
||||
}
|
||||
|
||||
export const timeAgo = (date: string | Date): string => {
|
||||
const now = new Date();
|
||||
const past = new Date(date);
|
||||
const diff = Math.floor((now.getTime() - past.getTime()) / 1000); // اختلاف بر حسب ثانیه
|
||||
|
||||
const units = [
|
||||
{ name: "سال", value: 60 * 60 * 24 * 365 },
|
||||
{ name: "ماه", value: 60 * 60 * 24 * 30 },
|
||||
{ name: "روز", value: 60 * 60 * 24 },
|
||||
{ name: "ساعت", value: 60 * 60 },
|
||||
{ name: "دقیقه", value: 60 },
|
||||
{ name: "ثانیه", value: 1 },
|
||||
];
|
||||
|
||||
for (const unit of units) {
|
||||
const amount = Math.floor(diff / unit.value);
|
||||
if (amount >= 1) return `${amount} ${unit.name} پیش`;
|
||||
}
|
||||
|
||||
return "همین الان";
|
||||
};
|
||||
|
||||
export const getToken = () => {
|
||||
return Cookie.get(import.meta.env.VITE_TOKEN_NAME);
|
||||
};
|
||||
|
||||
export const getRefreshToken = () => {
|
||||
return Cookie.get(import.meta.env.VITE_REFRESH_TOKEN_NAME);
|
||||
};
|
||||
|
||||
const isProduction = import.meta.env.MODE === "production";
|
||||
const domain = isProduction ? ".danakcorp.com" : undefined;
|
||||
|
||||
export const setToken = (token: string) => {
|
||||
Cookie.set(import.meta.env.VITE_TOKEN_NAME, token, { domain });
|
||||
};
|
||||
|
||||
export const setRefreshToken = (refreshToken: string) => {
|
||||
Cookie.set(import.meta.env.VITE_REFRESH_TOKEN_NAME, refreshToken, { domain });
|
||||
};
|
||||
|
||||
export const removeToken = () => {
|
||||
Cookie.remove(import.meta.env.VITE_TOKEN_NAME, { domain, path: "/" });
|
||||
};
|
||||
|
||||
export const removeRefreshToken = () => {
|
||||
Cookie.remove(import.meta.env.VITE_REFRESH_TOKEN_NAME, { domain, path: "/" });
|
||||
};
|
||||
|
||||
export const getRedirectUrl = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get("redirect");
|
||||
};
|
||||
+20
-10
@@ -1,10 +1,20 @@
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
|
||||
export type XOR<T, U> = T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U;
|
||||
|
||||
export type ItemsSelectType = {
|
||||
value: any,
|
||||
label: string,
|
||||
}
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
|
||||
export type XOR<T, U> = T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U;
|
||||
|
||||
export type ItemsSelectType = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type ErrorType = Error & {
|
||||
response?: {
|
||||
data?: {
|
||||
error: {
|
||||
message: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
+11
-11
@@ -1,11 +1,11 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* Concatenates truthy classes into a space-separated string.
|
||||
*
|
||||
* @param classes - The classes to concatenate.
|
||||
* @returns The concatenated classes.
|
||||
*/
|
||||
export const clx = (...classes: (string | boolean | undefined)[]): string => {
|
||||
return twMerge(classes.filter(Boolean).join(" "));
|
||||
};
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* Concatenates truthy classes into a space-separated string.
|
||||
*
|
||||
* @param classes - The classes to concatenate.
|
||||
* @returns The concatenated classes.
|
||||
*/
|
||||
export const clx = (...classes: (string | boolean | undefined)[]): string => {
|
||||
return twMerge(classes.filter(Boolean).join(" "));
|
||||
};
|
||||
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface ICountDown {
|
||||
m: number;
|
||||
s: number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param minutes how many minutes.
|
||||
* @param autoplay by default true. if you want play coundDown after an action, you can set it to false and manually play it.
|
||||
* @param onEnd you can pass a callback function that will trigger at the END.
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export function useCountDown(
|
||||
minutes: number,
|
||||
autoplay = true,
|
||||
onEnd = () => {}
|
||||
) {
|
||||
const [countDown, setCountDown] = useState<ICountDown>({ m: minutes, s: 0 });
|
||||
const [stop, setStop] = useState<boolean>(!autoplay);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
if (countDown.m !== 0 || countDown.s !== 0) {
|
||||
setCountDown(calculateCountDown(countDown));
|
||||
} else {
|
||||
onEnd();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [countDown]);
|
||||
|
||||
const reset = () => {
|
||||
setCountDown({ m: minutes, s: 0 });
|
||||
};
|
||||
const pause = () => {
|
||||
setStop(true);
|
||||
};
|
||||
const play = () => {
|
||||
setStop(false);
|
||||
setCountDown({ ...countDown });
|
||||
};
|
||||
|
||||
const calculateCountDown = (st: ICountDown): ICountDown => {
|
||||
if (stop) {
|
||||
return countDown;
|
||||
}
|
||||
const timeLeft = { m: 0, s: 0 };
|
||||
const remain = st.m * 60 + st.s - 1;
|
||||
if (remain > 0) {
|
||||
timeLeft.m = Math.floor(remain / 60);
|
||||
timeLeft.s = remain - timeLeft.m * 60;
|
||||
return timeLeft;
|
||||
} else {
|
||||
if (remain === 0 && countDown.s === 1) {
|
||||
return timeLeft;
|
||||
} else {
|
||||
return countDown;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
value: `${countDown.m > 9 ? countDown.m : "0" + countDown.m}:${
|
||||
countDown.s > 9 ? countDown.s : "0" + countDown.s
|
||||
}`,
|
||||
reset,
|
||||
pause,
|
||||
play,
|
||||
};
|
||||
}
|
||||
+4
-1
@@ -66,7 +66,10 @@
|
||||
"enter_password_step3": "رمز عبور را وارد کنید",
|
||||
"enter_your_password": "رمز عبور خود را وارد کنید.",
|
||||
"forgot_password": "فراموشی رمز عبور",
|
||||
"login_with_otp": "ورود با رمز عبور یکبار مصرف"
|
||||
"login_with_otp": "ورود با رمز عبور یکبار مصرف",
|
||||
"mobile_number": "شماره موبایل",
|
||||
"enter_mobile_number": "شماره موبایل خود را وارد کنید",
|
||||
"enter_verify_code": "کد تایید خود را وارد کنید"
|
||||
},
|
||||
"errors": {
|
||||
"required": "این فیلد اجباری می باشد"
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
import { FC } from 'react'
|
||||
import LogoImage from '../../assets/images/logo.svg'
|
||||
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||
import { useAuthStore } from './store/AuthStore'
|
||||
import LoginStep1 from './components/LoginStep1'
|
||||
import LoginStep2 from './components/LoginStep2'
|
||||
import LoginStep3 from './components/LoginStep3'
|
||||
|
||||
const Login: FC = () => {
|
||||
|
||||
const { stepLogin, phone, email } = useAuthStore()
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||
<div className='flex-1 h-full flex flex-col'>
|
||||
|
||||
<div className='flex relative flex-1 flex-col h-full justify-center'>
|
||||
<img src={LogoSmallImage} className='w-8 hidden xl:block' />
|
||||
<img src={LogoImage} className='w-44 right-0 left-0 mx-auto absolute top-10 xl:hidden' />
|
||||
{
|
||||
stepLogin === 1 ?
|
||||
<LoginStep1 />
|
||||
:
|
||||
stepLogin === 2 && !!phone ?
|
||||
<LoginStep2 />
|
||||
: stepLogin === 2 && !!email ?
|
||||
<LoginStep3 />
|
||||
: stepLogin === 3 ?
|
||||
<LoginStep3 />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||
<img src={LogoImage} className='h-20' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
@@ -0,0 +1,70 @@
|
||||
import { FC } from 'react'
|
||||
import LogoImage from '../../assets/images/logo.svg'
|
||||
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import RegisterStep1 from './components/RegisterStep1'
|
||||
import { useAuthStore } from './store/AuthStore'
|
||||
import RegisterStep2 from './components/RegisterStep2'
|
||||
|
||||
|
||||
const Register: FC = () => {
|
||||
|
||||
const { stepLogin } = useAuthStore()
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||
<div className='flex-1 h-full flex flex-col'>
|
||||
|
||||
|
||||
<div className='flex relative flex-1 flex-col h-full justify-center'>
|
||||
<img src={LogoSmallImage} className='w-8 hidden xl:block' />
|
||||
<div className=''>
|
||||
<img src={LogoImage} className='w-44 mx-auto my-20 xl:hidden' />
|
||||
</div>
|
||||
<div className=''>
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.welcome')}
|
||||
</h2>
|
||||
<p className='text-description text-sm mt-2'>
|
||||
{t('auth.enter_information')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{
|
||||
stepLogin === 1 ?
|
||||
<RegisterStep1 />
|
||||
:
|
||||
<RegisterStep2 />
|
||||
}
|
||||
|
||||
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.before_registered')}
|
||||
</p>
|
||||
<Link to={Pages.auth.login}>
|
||||
<div>{t('auth.login')}</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||
<img src={LogoImage} className='h-20' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Register
|
||||
@@ -0,0 +1,108 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import { isEmail } from '../../../config/func'
|
||||
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from '../../../components/Toast'
|
||||
const LoginStep1: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore()
|
||||
const loginWithOtp = useLoginWithOtp()
|
||||
const checkHasAccount = useCheckHasAccount()
|
||||
|
||||
const formik = useFormik<LoginType>({
|
||||
initialValues: {
|
||||
phone_email: phone,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone_email: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
if (isEmail(values.phone_email)) {
|
||||
setEmail(values.phone_email)
|
||||
checkHasAccount.mutate({ email: values.phone_email }, {
|
||||
onSuccess() {
|
||||
setStepLogin(2)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error.response?.data?.error.message?.[0], 'error')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setPhone(values.phone_email)
|
||||
loginWithOtp.mutate({ phone: values.phone_email }, {
|
||||
onSuccess() {
|
||||
setStepLogin(2)
|
||||
toast(t('auth.otp_sent'), 'success')
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.welcome')}
|
||||
</h2>
|
||||
<p className='text-description text-sm mt-2'>
|
||||
{t('auth.enter_info_login')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<Input
|
||||
label={t('auth.mobile_or_email')}
|
||||
placeholder={t('auth.enter_mobile_or_email')}
|
||||
type='text'
|
||||
className='text-right'
|
||||
name='phone_email'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.phone_email}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.touched.phone_email && formik.errors.phone_email &&
|
||||
<Error
|
||||
errorText={formik.errors.phone_email}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
|
||||
/>
|
||||
|
||||
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.have_account')}
|
||||
</p>
|
||||
<Link to={Pages.auth.register}>
|
||||
<div>{t('auth.register')}</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginStep1
|
||||
@@ -0,0 +1,186 @@
|
||||
import { FC, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { OtpVerifyType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Button from '../../../components/Button'
|
||||
import OTPInput from 'react-otp-input'
|
||||
import { useCountDown } from '../../../hooks/useCountDown'
|
||||
// import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { setRefreshToken } from '../../../config/func'
|
||||
import { setToken } from '../../../config/func'
|
||||
import { getRedirectUrl } from '../../../config/func'
|
||||
import { toast } from '../../../components/Toast'
|
||||
const LoginStep2: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { phone, setStepLogin } = useAuthStore()
|
||||
const { value, reset } = useCountDown(2, true)
|
||||
const otpVerify = useOtpVerify()
|
||||
const loginWithOtp = useLoginWithOtp()
|
||||
|
||||
const formik = useFormik<OtpVerifyType>({
|
||||
initialValues: {
|
||||
phone: phone,
|
||||
code: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
code: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
const params: OtpVerifyType = {
|
||||
phone: phone,
|
||||
code: values.code
|
||||
}
|
||||
otpVerify.mutate(params, {
|
||||
onSuccess(data) {
|
||||
setToken(data?.data?.accessToken?.token)
|
||||
setRefreshToken(data?.data?.refreshToken?.token)
|
||||
// Check if there's a redirect parameter in the URL
|
||||
const redirectUrl = getRedirectUrl();
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl;
|
||||
return; // Prevent the default navigation
|
||||
}
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// بررسی پشتیبانی از Web OTP API
|
||||
if (!("OTPCredential" in window)) return;
|
||||
|
||||
const ac = new AbortController();
|
||||
|
||||
navigator.credentials
|
||||
.get({
|
||||
otp: { transport: ["sms"] },
|
||||
signal: ac.signal,
|
||||
} as CredentialRequestOptions) // اطمینان از تطابق تایپها
|
||||
.then((otpCredential) => {
|
||||
if (otpCredential && "code" in otpCredential) {
|
||||
formik.setFieldValue("code", (otpCredential).code);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("SMS AutoFill Error:", err));
|
||||
|
||||
return () => ac.abort();
|
||||
}, []);
|
||||
|
||||
const reSend = () => {
|
||||
loginWithOtp.mutate({ phone: phone }, {
|
||||
onSuccess: () => {
|
||||
reset()
|
||||
toast(t('auth.toast_resend_code'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (formik.values.code.length === 5) {
|
||||
formik.handleSubmit()
|
||||
}
|
||||
|
||||
}, [formik.values.code])
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='lg:text-2xl font-bold'>
|
||||
{t('auth.enter_otp')}
|
||||
</h2>
|
||||
<p className='text-description flex lg:flex-row flex-col lg:gap-1 gap-2 text-sm lg:mt-2 mt-3'>
|
||||
<div className='flex gap-1'>
|
||||
<div>{t('auth.verfify_code_text_1')}</div>
|
||||
<div>{phone}</div>
|
||||
<div>
|
||||
{t('auth.verfify_code_text_2')}
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => setStepLogin(1)} className='text-black cursor-pointer'>
|
||||
{t('auth.change_number')}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<div className='text-sm'>
|
||||
{t('auth.verify_code')}
|
||||
</div>
|
||||
<div className='mt-2 w-full flex justify-center dltr otp'>
|
||||
<OTPInput
|
||||
shouldAutoFocus
|
||||
value={formik.values.code}
|
||||
numInputs={5}
|
||||
inputType='tel'
|
||||
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
||||
renderInput={(props) =>
|
||||
<input
|
||||
{...props}
|
||||
type='tel'
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
formik.touched.code && formik.errors.code &&
|
||||
<div className='text-xs mt-2 text-red-500'>{formik.errors.code}</div>
|
||||
}
|
||||
|
||||
<div className='flex mt-4 justify-end'>
|
||||
{
|
||||
value !== '00:00' ?
|
||||
<div className='flex gap-1 text-description text-xs'>
|
||||
<div>{value}</div>
|
||||
<div>{t('auth.counter_otp')}</div>
|
||||
</div>
|
||||
:
|
||||
<div onClick={reSend} className='flex cursor-pointer gap-1 pl-2 text-black text-sm'>
|
||||
<div className=''>
|
||||
{t('auth.try_again')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.login')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={otpVerify.isPending}
|
||||
/>
|
||||
|
||||
{/* <div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.login_with_password')}
|
||||
</p>
|
||||
<img src={ArrowLeftIcon} className='w-5' />
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginStep2
|
||||
@@ -0,0 +1,108 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginWithPasswordType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||
import { useLoginWithPassword } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { setRefreshToken } from '../../../config/func'
|
||||
import { setToken } from '../../../config/func'
|
||||
import { getRedirectUrl } from '../../../config/func'
|
||||
import { toast } from '../../../components/Toast'
|
||||
|
||||
const LoginStep3: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setStepLogin, email, phone } = useAuthStore()
|
||||
const loginWithPassword = useLoginWithPassword()
|
||||
|
||||
const formik = useFormik<LoginWithPasswordType>({
|
||||
initialValues: {
|
||||
password: '',
|
||||
email: email ? email : undefined,
|
||||
phone: phone ? phone : undefined,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
password: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
loginWithPassword.mutate(values, {
|
||||
onSuccess(data) {
|
||||
setToken(data?.data?.accessToken?.token)
|
||||
setRefreshToken(data?.data?.refreshToken?.token)
|
||||
const redirectUrl = getRedirectUrl();
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl;
|
||||
return; // Prevent the default navigation
|
||||
}
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.enter_password_step3')}
|
||||
</h2>
|
||||
<p className='text-description text-sm mt-2'>
|
||||
{t('auth.enter_your_password')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<Input
|
||||
label={t('auth.password')}
|
||||
type='password'
|
||||
className='text-right'
|
||||
name='password'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.password}
|
||||
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.touched.password && formik.errors.password &&
|
||||
<Error
|
||||
errorText={formik.errors.password}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
<Link to={Pages.auth.forgotPassword}>
|
||||
<div className='mt-4 flex justify-end text-sm'>
|
||||
{t('auth.forgot_password')}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
label={t('auth.login')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
/>
|
||||
|
||||
<div onClick={() => setStepLogin(2)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.login_with_otp')}
|
||||
</p>
|
||||
<img src={ArrowLeftIcon} className='w-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginStep3
|
||||
@@ -0,0 +1,78 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CheckHasAccountPhoneType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { useCheckHasAccountRegister, useLoginWithOtp } from '../hooks/useAuthData'
|
||||
import { toast } from '../../../components/Toast'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
const RegisterStep1: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setPhone, setStepLogin } = useAuthStore()
|
||||
const loginWithOtp = useLoginWithOtp()
|
||||
const checkHasAccount = useCheckHasAccountRegister()
|
||||
|
||||
const formik = useFormik<CheckHasAccountPhoneType>({
|
||||
initialValues: {
|
||||
phone: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
checkHasAccount.mutate(values, {
|
||||
onSuccess() {
|
||||
setPhone(values.phone)
|
||||
setStepLogin(2)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<div className='flex-1'>
|
||||
|
||||
|
||||
<div className='mt-16'>
|
||||
<Input
|
||||
label={t('auth.mobile_number')}
|
||||
placeholder={t('auth.enter_mobile_number')}
|
||||
type='tel'
|
||||
className='text-right'
|
||||
name='phone'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.phone}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.touched.phone && formik.errors.phone &&
|
||||
<Error
|
||||
errorText={formik.errors.phone}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterStep1
|
||||
@@ -0,0 +1,143 @@
|
||||
import { FC, Fragment } from 'react'
|
||||
import { useFormik } from 'formik'
|
||||
import { RegisterType } from '../types/AuthTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../../components/Input'
|
||||
import DatePickerComponent from '../../../components/DatePicker'
|
||||
import Button from '../../../components/Button'
|
||||
import { useRegister } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import { setToken } from '../../../config/func'
|
||||
import { setRefreshToken } from '../../../config/func'
|
||||
import { getRedirectUrl } from '../../../config/func'
|
||||
const RegisterStep2: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { phone } = useAuthStore()
|
||||
const register = useRegister()
|
||||
|
||||
const formik = useFormik<RegisterType>({
|
||||
initialValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
birthDate: '',
|
||||
code: 0,
|
||||
nationalCode: '',
|
||||
password: '',
|
||||
phone: phone,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
firstName: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
lastName: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
birthDate: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
code: Yup.number()
|
||||
.required(t('errors.required')),
|
||||
nationalCode: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
password: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
phone: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
}),
|
||||
onSubmit(values) {
|
||||
register.mutate(values, {
|
||||
onSuccess(data) {
|
||||
setToken(data?.data?.accessToken?.token)
|
||||
setRefreshToken(data?.data?.refreshToken?.token)
|
||||
const redirectUrl = getRedirectUrl();
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl;
|
||||
return; // Prevent the default navigation
|
||||
}
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className='mt-5 rowTwoInput'>
|
||||
<Input
|
||||
label={t('auth.name')}
|
||||
placeholder={t('auth.enter_name')}
|
||||
name='firstName'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('auth.family')}
|
||||
placeholder={t('auth.enter_family')}
|
||||
name='lastName'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className='mt-4 min-w-full rowTwoInput'>
|
||||
<DatePickerComponent
|
||||
label={t('auth.dateofbirth')}
|
||||
onChange={(date: string) => formik.setFieldValue('birthDate', date)}
|
||||
placeholder={t('auth.select_date')}
|
||||
error_text={formik.touched.birthDate && formik.errors.birthDate ? formik.errors.birthDate : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('auth.national_code')}
|
||||
placeholder={t('auth.national_code_enter')}
|
||||
name='nationalCode'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.nationalCode && formik.errors.nationalCode ? formik.errors.nationalCode : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 rowTwoInput'>
|
||||
<Input
|
||||
label={t('auth.password')}
|
||||
placeholder={t('auth.enter_password')}
|
||||
type='password'
|
||||
name='password'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth.phonen_number')}
|
||||
placeholder={t('auth.enter_phone_number')}
|
||||
readOnly
|
||||
value={phone}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 rowTwoInput'>
|
||||
<Input
|
||||
label={t('auth.verify_code')}
|
||||
placeholder={t('auth.enter_verify_code')}
|
||||
type='tel'
|
||||
name='code'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.code && formik.errors.code ? formik.errors.code : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={register.isPending}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterStep2
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import * as api from '../service/AuthService'
|
||||
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
|
||||
|
||||
export const useLoginWithPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: LoginWithPasswordType) => api.loginWithPassword(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useLoginWithOtp = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: LoginWithOtpType) => api.loginWithOtp(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useOtpVerify = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: OtpVerifyType) => api.otpVerify(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCheckHasAccount = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CheckHasAccountType) => api.checkHasAccount(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCheckHasAccountRegister = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CheckHasAccountPhoneType) => api.checkHasAccountRegister(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useRegister = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: RegisterType) => api.register(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useLogout = () => {
|
||||
return useMutation({
|
||||
mutationFn: (_) => api.logout(),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import axios from "../../../config/axios";
|
||||
import {
|
||||
CheckHasAccountPhoneType,
|
||||
CheckHasAccountType,
|
||||
LoginWithOtpType,
|
||||
LoginWithPasswordType,
|
||||
OtpVerifyType,
|
||||
RefreshTokenType,
|
||||
RegisterType,
|
||||
} from "../types/AuthTypes";
|
||||
|
||||
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
||||
const { data } = await axios.post(`/auth/login/password`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const loginWithOtp = async (params: LoginWithOtpType) => {
|
||||
const { data } = await axios.post(`/auth/otp/send`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const otpVerify = async (params: OtpVerifyType) => {
|
||||
const { data } = await axios.post(`/auth/otp/verify`, params);
|
||||
return data;
|
||||
};
|
||||
export const checkHasAccount = async (params: CheckHasAccountType) => {
|
||||
const { data } = await axios.post(`/auth/check`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const checkHasAccountRegister = async (
|
||||
params: CheckHasAccountPhoneType
|
||||
) => {
|
||||
const { data } = await axios.post(`/auth/register/initiate`, params);
|
||||
return data;
|
||||
};
|
||||
export const register = async (params: RegisterType) => {
|
||||
const { data } = await axios.post(`/auth/register/complete`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const refreshToken = async (params: RefreshTokenType) => {
|
||||
const { data } = await axios.post(`/auth/refresh`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const logout = async () => {
|
||||
const { data } = await axios.post(`/auth/logout`);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { create } from "zustand";
|
||||
import { AuthStoreType } from "../../auth/types/AuthTypes";
|
||||
|
||||
export const useAuthStore = create<AuthStoreType>((set) => ({
|
||||
phone: "",
|
||||
setPhone(value) {
|
||||
set({ phone: value });
|
||||
},
|
||||
email: "",
|
||||
setEmail(value) {
|
||||
set({ email: value });
|
||||
},
|
||||
stepLogin: 1,
|
||||
setStepLogin(value) {
|
||||
set({ stepLogin: value });
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,52 @@
|
||||
export type LoginType = {
|
||||
phone_email: string;
|
||||
};
|
||||
export type LoginPasswordType = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type AuthStoreType = {
|
||||
phone: string;
|
||||
setPhone: (value: string) => void;
|
||||
email: string;
|
||||
setEmail: (value: string) => void;
|
||||
stepLogin: number;
|
||||
setStepLogin: (value: number) => void;
|
||||
};
|
||||
|
||||
export type LoginWithPasswordType = {
|
||||
email?: string;
|
||||
password: string;
|
||||
phone?: string;
|
||||
};
|
||||
|
||||
export type LoginWithOtpType = {
|
||||
phone: string;
|
||||
};
|
||||
|
||||
export type OtpVerifyType = {
|
||||
phone: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type CheckHasAccountType = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type CheckHasAccountPhoneType = {
|
||||
phone: string;
|
||||
};
|
||||
|
||||
export type RegisterType = {
|
||||
phone: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthDate: string;
|
||||
nationalCode: string;
|
||||
password: string;
|
||||
code: number;
|
||||
};
|
||||
|
||||
export type RefreshTokenType = {
|
||||
refreshToken: string;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { FC } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
import Login from '../pages/auth/Login'
|
||||
import Register from '../pages/auth/Register'
|
||||
|
||||
const AuthRouter: FC = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={Pages.auth.login} element={<Login />} />
|
||||
<Route path={Pages.auth.register} element={<Register />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthRouter
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DefaultError } from "@tanstack/react-query";
|
||||
import { IBaseResponse } from "./response.types";
|
||||
|
||||
interface IError {
|
||||
message: string[];
|
||||
}
|
||||
|
||||
export interface IErrorResponse extends IBaseResponse {
|
||||
error: IError;
|
||||
}
|
||||
|
||||
export interface IApiErrorRepsonse extends DefaultError {
|
||||
response?: IErrorResponse;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IBaseResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
}
|
||||
export interface IResponse<T> extends IBaseResponse {
|
||||
data: T;
|
||||
}
|
||||
@@ -21,6 +21,20 @@ module.exports = withMT({
|
||||
},
|
||||
borderRadius: {
|
||||
'2.5': '10px'
|
||||
},
|
||||
keyframes: {
|
||||
'slide-in-right': {
|
||||
'0%': { transform: 'translateX(100%)', opacity: '0' },
|
||||
'100%': { transform: 'translateX(0)', opacity: '1' }
|
||||
},
|
||||
'slide-out-right': {
|
||||
'0%': { transform: 'translateX(0)', opacity: '1' },
|
||||
'100%': { transform: 'translateX(100%)', opacity: '0' }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'slide-in-right': 'slide-in-right 0.3s ease-out',
|
||||
'slide-out-right': 'slide-out-right 0.3s ease-out'
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user