login
This commit is contained in:
+6
-2
@@ -39,18 +39,22 @@ const queryClient = new QueryClient({
|
||||
const App: FC = () => {
|
||||
|
||||
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (localStorage.getItem(import.meta.env.VITE_TOKEN_NAME)) {
|
||||
setIsLogin('isLogin')
|
||||
} else {
|
||||
setIsLogin('isNotLogin')
|
||||
window.location.href = Pages.auth.login
|
||||
if (window.location.href.split('auth').length === 1) {
|
||||
window.location.href = Pages.auth.login
|
||||
}
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
console.log('us', isLogin);
|
||||
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.97508 5.44165L2.91675 10.5L7.97508 15.5583" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.0834 10.5H3.05835" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.9833 9.99993C12.9833 11.6499 11.6499 12.9833 9.99993 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99993C7.0166 8.34993 8.34993 7.0166 9.99993 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99993Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.99987 16.8918C12.9415 16.8918 15.6832 15.1584 17.5915 12.1584C18.3415 10.9834 18.3415 9.00843 17.5915 7.83343C15.6832 4.83343 12.9415 3.1001 9.99987 3.1001C7.0582 3.1001 4.31654 4.83343 2.4082 7.83343C1.6582 9.00843 1.6582 10.9834 2.4082 12.1584C4.31654 15.1584 7.0582 16.8918 9.99987 16.8918Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 794 B |
@@ -0,0 +1,15 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
errorText: string
|
||||
}
|
||||
|
||||
const Error: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='mt-1.5 font-normal text-red-500 text-[11px]'>
|
||||
{props.errorText}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Error
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC, InputHTMLAttributes } from 'react'
|
||||
import { FC, InputHTMLAttributes, useState } from 'react'
|
||||
import { clx } from '../helpers/utils';
|
||||
import EyeIcon from '../assets/images/eye.svg'
|
||||
|
||||
type Variant = "floating_outlined" | "primary" | "search";
|
||||
|
||||
@@ -16,19 +17,29 @@ type Props = {
|
||||
|
||||
const Input: FC<Props> = (props: Props) => {
|
||||
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||
|
||||
const inputClass = clx(
|
||||
'w-full h-10 text-black block px-4 text-xs rounded-xl mt-1 border border-border',
|
||||
props.readOnly && 'bg-gray-100',
|
||||
props.className
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<label className='text-sm'>
|
||||
{props.label}
|
||||
</label>
|
||||
|
||||
<input {...props} className={inputClass} />
|
||||
<div className='w-full relative'>
|
||||
<input {...props} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
||||
|
||||
{
|
||||
props.type === 'password' &&
|
||||
<img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ export const Pages = {
|
||||
auth: {
|
||||
login: "/auth/login",
|
||||
register: "/auth/register",
|
||||
forgotPassword: "/auth/forgot",
|
||||
},
|
||||
};
|
||||
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
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);
|
||||
};
|
||||
}, [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
|
||||
};
|
||||
}
|
||||
@@ -47,3 +47,9 @@ textarea::placeholder {
|
||||
.rmdp-container {
|
||||
width: 100%;
|
||||
}
|
||||
.dltr {
|
||||
direction: ltr;
|
||||
}
|
||||
.otp div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
+15
-1
@@ -24,6 +24,20 @@
|
||||
"mobile_or_email": " شماره موبایل یا ایمیل",
|
||||
"enter_mobile_or_email": " شماره موبایل یا ایمیل خود را وارد کنید",
|
||||
"have_account": "آیا حساب کاربری ندارید؟",
|
||||
"register": "ثبت نام"
|
||||
"register": "ثبت نام",
|
||||
"enter_otp": "کد تایید را وارد کنید",
|
||||
"verfify_code_text_1": "کد تایید برای ",
|
||||
"verfify_code_text_2": "ارسال شد.",
|
||||
"change_number": "تغییر شماره",
|
||||
"verify_code": "کد تایید ",
|
||||
"counter_otp": "مانده تا دریافت مجدد کد",
|
||||
"login_with_password": "ورود با رمز عبور",
|
||||
"enter_password_step3": "رمز عبور را وارد کنید",
|
||||
"enter_your_password": "رمز عبور خود را وارد کنید.",
|
||||
"forgot_password": "فراموشی رمز عبور",
|
||||
"login_with_otp": "ورود با رمز عبور یکبار مصرف"
|
||||
},
|
||||
"errors": {
|
||||
"required": "این فیلد اجباری می باشد"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-39
@@ -1,15 +1,15 @@
|
||||
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 Input from '../../components/Input'
|
||||
import Button from '../../components/Button'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import LoginStep1 from './components/LoginStep1'
|
||||
import LoginStep2 from './components/LoginStep2'
|
||||
import LoginStep3 from './components/LoginStep3'
|
||||
|
||||
const Login: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { stepLogin } = useAuthStore()
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center py-[75px] items-center px-10'>
|
||||
@@ -19,39 +19,16 @@ const Login: FC = () => {
|
||||
<img src={LogoSmallImage} className='w-8' />
|
||||
|
||||
<div className='flex flex-1 flex-col h-full justify-center'>
|
||||
<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 rowTwoInput'>
|
||||
<Input
|
||||
label={t('auth.mobile_or_email')}
|
||||
placeholder={t('auth.enter_mobile_or_email')}
|
||||
type='tel'
|
||||
className='text-right'
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
/>
|
||||
|
||||
<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>
|
||||
{
|
||||
stepLogin === 1 ?
|
||||
<LoginStep1 />
|
||||
:
|
||||
stepLogin === 2 ?
|
||||
<LoginStep2 />
|
||||
: stepLogin === 3 ?
|
||||
<LoginStep3 />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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'
|
||||
|
||||
const LoginStep1: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setPhone, phone, setStepLogin } = useAuthStore()
|
||||
|
||||
const formik = useFormik<LoginType>({
|
||||
initialValues: {
|
||||
phone_email: phone,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone_email: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
setPhone(values.phone_email)
|
||||
setStepLogin(2)
|
||||
},
|
||||
})
|
||||
|
||||
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()}
|
||||
/>
|
||||
|
||||
<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,91 @@
|
||||
import { FC, useState } from 'react'
|
||||
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 Button from '../../../components/Button'
|
||||
import OTPInput from 'react-otp-input'
|
||||
import { useCountDown } from '../../../hooks/useCountDown'
|
||||
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||
|
||||
const LoginStep2: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setPhone, phone, setStepLogin } = useAuthStore()
|
||||
const [otpValue, setOtpValue] = useState<string>()
|
||||
const { value } = useCountDown(2, true)
|
||||
|
||||
const formik = useFormik<LoginType>({
|
||||
initialValues: {
|
||||
phone_email: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone_email: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
setPhone(values.phone_email)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.enter_otp')}
|
||||
</h2>
|
||||
<p className='text-description flex gap-1 text-sm mt-2'>
|
||||
<div>{t('auth.verfify_code_text_1')}</div>
|
||||
<div>{phone}</div>
|
||||
<div>
|
||||
{t('auth.verfify_code_text_2')}
|
||||
</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={otpValue}
|
||||
numInputs={5}
|
||||
inputType='tel'
|
||||
onChange={(otp: string) => setOtpValue(otp)}
|
||||
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex mt-4 justify-end'>
|
||||
<div className='flex gap-1 text-description text-xs'>
|
||||
<div>{value}</div>
|
||||
<div>{t('auth.counter_otp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.login')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
/>
|
||||
|
||||
<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,84 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginPasswordType } 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'
|
||||
|
||||
const LoginStep3: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setStepLogin } = useAuthStore()
|
||||
|
||||
const formik = useFormik<LoginPasswordType>({
|
||||
initialValues: {
|
||||
password: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone_email: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
console.log(values);
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
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}
|
||||
/>
|
||||
|
||||
{
|
||||
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,13 @@
|
||||
export type LoginType = {
|
||||
phone_email: string;
|
||||
};
|
||||
export type LoginPasswordType = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type AuthStoreType = {
|
||||
phone: string;
|
||||
setPhone: (value: string) => void;
|
||||
stepLogin: number;
|
||||
setStepLogin: (value: number) => void;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { create } from "zustand";
|
||||
import { AuthStoreType } from "../auth/types/AuthTypes";
|
||||
|
||||
export const useAuthStore = create<AuthStoreType>((set) => ({
|
||||
phone: "",
|
||||
setPhone(value) {
|
||||
set({ phone: value });
|
||||
},
|
||||
stepLogin: 1,
|
||||
setStepLogin(value) {
|
||||
set({ stepLogin: value });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user