admin login

This commit is contained in:
hamid zarghami
2025-08-30 16:24:53 +03:30
parent d19bf516e5
commit 54c3c59a12
24 changed files with 906 additions and 650 deletions
+3
View File
@@ -0,0 +1,3 @@
VITE_BASE_URL = 'https://api.shinan.ir'
VITE_TOKEN_NAME = 'sh_admin_token'
VITE_REFRESH_TOKEN_NAME = 'sh_admin_token'
+1 -18
View File
@@ -1,15 +1,10 @@
import { type 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'>
@@ -18,21 +13,9 @@ const Login: FC = () => {
<img src={LogoSmallImage} className='w-8' />
<div className='flex flex-1 flex-col h-full justify-center'>
{
stepLogin === 1 ?
<LoginStep1 />
:
stepLogin === 2 && !!phone ?
<LoginStep2 />
: stepLogin === 2 && !!email ?
<LoginStep3 />
: stepLogin === 3 ?
<LoginStep3 />
: null
}
<LoginStep1 />
</div>
</div>
</div>
+3 -12
View File
@@ -5,16 +5,12 @@ 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'
// import RegisterStep2 from './components/RegisterStep2' // Not used in current implementation
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'>
@@ -27,15 +23,10 @@ const Register: FC = () => {
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_information')}
</p>
</p>
</div>
{
stepLogin === 1 ?
<RegisterStep1 />
:
<RegisterStep2 />
}
<RegisterStep1 />
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
<p className='text-description'>
+56 -44
View File
@@ -1,55 +1,50 @@
import { FC } from 'react'
import { type FC } from 'react'
import Input from '../../../components/Input'
import { useTranslation } from 'react-i18next'
import { LoginType } from '../types/AuthTypes'
import { type 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 { isEmail } from '../../../config/func'
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
import { useLoginWithPassword } from '../hooks/useAuthData'
import { toast } from 'react-toastify'
import { ErrorType } from '../../../helpers/types'
import { type ErrorType } from '../../../helpers/types'
import { setRefreshToken, setToken } from '../../../config/func'
import { Pages } from '../../../config/Pages'
const LoginStep1: FC = () => {
const { t } = useTranslation('global')
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore()
const loginWithOtp = useLoginWithOtp()
const checkHasAccount = useCheckHasAccount()
const { setEmail } = useAuthStore()
const loginWithPassword = useLoginWithPassword()
const formik = useFormik<LoginType>({
const formik = useFormik<LoginWithPasswordType>({
initialValues: {
phone_email: phone,
email: '',
password: '',
},
validationSchema: Yup.object({
phone_email: Yup.string()
email: Yup.string()
.email(t('errors.invalid_email'))
.required(t('errors.required')),
password: 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(error?.response?.data?.error?.message[0])
},
})
} else {
setPhone(values.phone_email)
loginWithOtp.mutate({ phone: values.phone_email }, {
onSuccess() {
setStepLogin(2)
toast.success(t('auth.otp_sent'))
},
onError(error: ErrorType) {
toast.error(error?.response?.data?.error?.message[0])
},
})
}
setEmail(values.email)
loginWithPassword.mutate(values, {
onSuccess(data) {
if (data?.results?.accessToken?.token && data?.results?.refreshToken?.token) {
setToken(data.results.accessToken.token)
setRefreshToken(data.results.refreshToken.token)
window.location.href = Pages.dashboard
}
},
onError(error: ErrorType) {
toast.error(error?.response?.data?.error?.message[0])
},
})
},
})
@@ -64,32 +59,49 @@ const LoginStep1: FC = () => {
</p>
</div>
<div className='mt-16'>
<div className='mt-16 space-y-6'>
<Input
label={t('auth.mobile_or_email')}
placeholder={t('auth.enter_mobile_or_email')}
type='text'
label={t('auth.email')}
placeholder={t('auth.enter_email')}
type='email'
className='text-right'
name='phone_email'
name='email'
onChange={formik.handleChange}
value={formik.values.phone_email}
value={formik.values.email}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : ''}
/>
<Input
label={t('auth.password')}
placeholder={t('auth.enter_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.phone_email && formik.errors.phone_email &&
formik.touched.email && formik.errors.email &&
<Error
errorText={formik.errors.phone_email}
errorText={formik.errors.email}
/>
}
{
formik.touched.password && formik.errors.password &&
<Error
errorText={formik.errors.password}
/>
}
</div>
<Button
label={t('auth.next')}
label={t('auth.login')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
isLoading={loginWithPassword.isPending}
/>
</div>
-118
View File
@@ -1,118 +0,0 @@
import { FC } 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 { useOtpVerify } from '../hooks/useAuthData'
import { ErrorType } from '../../../helpers/types'
import { toast } from 'react-toastify'
import { setToken, setRefreshToken } from '../../../config/func'
const LoginStep2: FC = () => {
const { t } = useTranslation('global')
const { phone, setStepLogin } = useAuthStore()
const { value } = useCountDown(2, true)
const otpVerify = useOtpVerify()
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)
window.location.href = Pages.dashboard
},
onError(error: ErrorType) {
toast.error(error?.response?.data?.error?.message[0])
},
})
},
})
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} 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'>
<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()}
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
-101
View File
@@ -1,101 +0,0 @@
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 { toast } from 'react-toastify'
import { ErrorType } from '../../../helpers/types'
import { setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
const LoginStep3: FC = () => {
const { t } = useTranslation('global')
const { setStepLogin, email } = useAuthStore()
const loginWithPassword = useLoginWithPassword()
const formik = useFormik<LoginWithPasswordType>({
initialValues: {
password: '',
email: email
},
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)
window.location.href = Pages.dashboard
},
onError(error: ErrorType) {
toast.error(error?.response?.data?.error?.message[0])
},
})
},
})
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
+20 -22
View File
@@ -1,36 +1,36 @@
import { FC } from 'react'
import { type FC } from 'react'
import Input from '../../../components/Input'
import { useTranslation } from 'react-i18next'
import { CheckHasAccountPhoneType } from '../types/AuthTypes'
import { type CheckHasAccountType } 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 { useCheckHasAccount } from '../hooks/useAuthData'
import { toast } from 'react-toastify'
import { ErrorType } from '../../../helpers/types'
import { type ErrorType } from '../../../helpers/types'
const RegisterStep1: FC = () => {
const { t } = useTranslation('global')
const { setPhone, setStepLogin } = useAuthStore()
const loginWithOtp = useLoginWithOtp()
const checkHasAccount = useCheckHasAccountRegister()
const { setEmail } = useAuthStore()
const checkHasAccount = useCheckHasAccount()
const formik = useFormik<CheckHasAccountPhoneType>({
const formik = useFormik<CheckHasAccountType>({
initialValues: {
phone: '',
email: '',
},
validationSchema: Yup.object({
phone: Yup.string()
email: Yup.string()
.email(t('errors.invalid_email'))
.required(t('errors.required'))
}),
onSubmit(values) {
checkHasAccount.mutate(values, {
onSuccess() {
setPhone(values.phone)
setStepLogin(2)
setEmail(values.email)
// Redirect to register step 2 or handle as needed
},
onError(error: ErrorType) {
toast.error(error?.response?.data?.error?.message[0])
@@ -42,33 +42,31 @@ const RegisterStep1: FC = () => {
return (
<div className='flex-1'>
<div className='mt-16'>
<Input
label={t('auth.mobile_number')}
placeholder={t('auth.enter_mobile_number')}
type='tel'
label={t('auth.email')}
placeholder={t('auth.enter_email')}
type='email'
className='text-right'
name='phone'
name='email'
onChange={formik.handleChange}
value={formik.values.phone}
value={formik.values.email}
/>
{
formik.touched.phone && formik.errors.phone &&
formik.touched.email && formik.errors.email &&
<Error
errorText={formik.errors.phone}
errorText={formik.errors.email}
/>
}
</div>
<Button
label={t('auth.next')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
isLoading={checkHasAccount.isPending}
/>
</div>
)
+11 -9
View File
@@ -1,21 +1,21 @@
import { FC, Fragment } from 'react'
import { type FC, Fragment } from 'react'
import { useFormik } from 'formik'
import { RegisterType } from '../types/AuthTypes'
import { type RegisterType } from '../types/AuthTypes'
import * as Yup from 'yup'
import { useAuthStore } from '../store/AuthStore'
// import { useAuthStore } from '../store/AuthStore' // Not used in current implementation
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 { type ErrorType } from '../../../helpers/types'
import { toast } from 'react-toastify'
import { Pages } from '../../../config/Pages'
const RegisterStep2: FC = () => {
const { t } = useTranslation('global')
const { phone } = useAuthStore()
// const { email } = useAuthStore() // Not used in current implementation
const register = useRegister()
const formik = useFormik<RegisterType>({
@@ -26,7 +26,7 @@ const RegisterStep2: FC = () => {
code: 0,
nationalCode: '',
password: '',
phone: phone,
phone: '', // We'll need to add phone field to the form
},
validationSchema: Yup.object({
firstName: Yup.string()
@@ -105,10 +105,12 @@ const RegisterStep2: FC = () => {
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
/>
<Input
label={t('auth.phonen_number')}
label={t('auth.phone_number')}
placeholder={t('auth.enter_phone_number')}
readOnly
value={phone}
type='tel'
name='phone'
onChange={formik.handleChange}
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : ''}
/>
</div>
+1 -19
View File
@@ -1,6 +1,6 @@
import { useMutation } from '@tanstack/react-query';
import * as api from '../service/AuthService'
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
import { type CheckHasAccountType, type LoginWithPasswordType, type RegisterType } from '../types/AuthTypes';
export const useLoginWithPassword = () => {
return useMutation({
@@ -8,30 +8,12 @@ export const useLoginWithPassword = () => {
});
};
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),
+1 -19
View File
@@ -1,39 +1,21 @@
import axios from "../../../config/axios";
import {
type CheckHasAccountPhoneType,
type CheckHasAccountType,
type LoginWithOtpType,
type LoginWithPasswordType,
type OtpVerifyType,
type RefreshTokenType,
type RegisterType,
} from "../types/AuthTypes";
export const loginWithPassword = async (params: LoginWithPasswordType) => {
const { data } = await axios.post(`/auth/login/password/admin`, params);
const { data } = await axios.post(`/admin/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/admin`, 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;
+5 -9
View File
@@ -1,17 +1,13 @@
import { create } from "zustand";
import { AuthStoreType } from "../../auth/types/AuthTypes";
export type AuthStoreType = {
email: string;
setEmail: (value: string) => void;
};
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 });
},
}));
+2 -34
View File
@@ -1,45 +1,12 @@
import { type XOR } from "../../../helpers/types";
export type LoginType = {
phone_email: string;
};
export type LoginPasswordType = {
password: string;
};
export type AuthStoreType = {
phone: string;
setPhone: (value: string) => void;
export type LoginWithPasswordType = {
email: string;
setEmail: (value: string) => void;
stepLogin: number;
setStepLogin: (value: number) => void;
};
export type LoginWithPasswordType = XOR<
{ email: string },
{ phone: number }
> & {
password: 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;
@@ -49,6 +16,7 @@ export type RegisterType = {
password: string;
code: number;
};
export type RefreshTokenType = {
refreshToken: string;
};
+6 -2
View File
@@ -1,8 +1,12 @@
import React from 'react'
import Login from '../pages/auth/Login'
import { Route, Routes } from 'react-router-dom'
import { Pages } from '../config/Pages'
const Auth = () => {
return (
<div>Auth</div>
<Routes>
<Route path={Pages.auth.login} element={<Login />} />
</Routes>
)
}
+786 -232
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,5 +1,5 @@
import { DocumentText } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { Pages } from '../../config/Pages'
@@ -14,7 +14,7 @@ const BlogSubMenu: FC = () => {
return (
<div className='py-12'>
<div className='flex gap-3 items-center border-b pb-10 px-6'>
<div className='flex gap-3 items-center border-b border-border pb-10 px-6'>
<DocumentText
size={24}
color='#000'
+1 -1
View File
@@ -1,5 +1,5 @@
import { People } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { Pages } from '../../config/Pages'
+1 -1
View File
@@ -1,5 +1,5 @@
import { Teacher } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { useLocation } from 'react-router-dom'
+1 -1
View File
@@ -1,5 +1,5 @@
import { Receipt21 } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { useLocation } from 'react-router-dom'
+1 -1
View File
@@ -1,5 +1,5 @@
import { Element3 } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { useLocation } from 'react-router-dom'
+1 -1
View File
@@ -1,4 +1,4 @@
import { FC } from 'react'
import { type FC } from 'react'
import { Link } from 'react-router-dom'
import { clx } from '../../helpers/utils'
+1 -1
View File
@@ -1,5 +1,5 @@
import { Headphone } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { Pages } from '../../config/Pages'
+1 -1
View File
@@ -1,5 +1,5 @@
import { Messages3 } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { useLocation } from 'react-router-dom'
@@ -1,5 +1,5 @@
import { Card } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
+1 -1
View File
@@ -1,5 +1,5 @@
import { Profile } from 'iconsax-react'
import { FC } from 'react'
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import SubMenuItem from './SubMenuItem'
import { Pages } from '../../config/Pages'