This commit is contained in:
hamid zarghami
2026-01-29 09:48:53 +03:30
parent 7e5184529e
commit 420e011b50
19 changed files with 568 additions and 17 deletions
+3 -3
View File
@@ -1,3 +1,3 @@
VITE_API_BASE_URL = 'http://192.168.99.235:4000' VITE_API_BASE_URL = 'http://10.22.13.88:4000'
VITE_TOKEN_NAME = 'negareh_t' VITE_TOKEN_NAME = 'negareh_at'
VITE_REFRESH_TOKEN_NAME = 'negareh_rt' VITE_REFRESH_TOKEN_NAME = 'negareh_art'
+4 -5
View File
@@ -6,9 +6,9 @@ import 'react-toastify/dist/ReactToastify.css'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ToastContainer } from 'react-toastify' import { ToastContainer } from 'react-toastify'
// import { getToken } from './config/func' import { getToken } from './config/func'
import MainRouter from './router/MainRouter' import MainRouter from './router/MainRouter'
// import AuthRouter from './router/AuthRouter' import AuthRouter from './router/AuthRouter'
// ایجاد QueryClient برای React Query // ایجاد QueryClient برای React Query
const queryClient = new QueryClient({ const queryClient = new QueryClient({
@@ -23,13 +23,12 @@ const queryClient = new QueryClient({
const App: FC = () => { const App: FC = () => {
// const isLoggedIn = getToken() const isLoggedIn = getToken()
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
{/* {isLoggedIn ? <MainRouter /> : <AuthRouter />} */} {isLoggedIn ? <MainRouter /> : <AuthRouter />}
<MainRouter />
</BrowserRouter> </BrowserRouter>
<ToastContainer <ToastContainer
position="top-left" position="top-left"
+5
View File
@@ -0,0 +1,5 @@
<svg width="36" height="65" viewBox="0 0 36 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.7925 64.8275L20.2339 60.6914L17.7098 64.7757L15.1835 60.6957L12.6292 64.8275H22.7925Z" fill="#F6AF31"/>
<path d="M17.6947 0L0 28.6356L2.32581 32.4009C2.74183 31.7263 3.14707 31.0862 3.51351 30.4784L2.37539 28.6356L17.6947 3.84294L33.014 28.6356L31.891 30.4547C32.2682 31.1228 32.7015 31.7091 33.0787 32.3772L35.3894 28.6356L17.6947 0Z" fill="#F6AF31"/>
<path d="M17.7098 7.50488L0.0150757 36.1405L17.7098 64.7761L35.4045 36.1405L17.7098 7.50488ZM18.4319 29.3124V12.516L23.7797 21.1718L18.4319 29.3124ZM24.6592 22.5943L28.6491 29.0516L18.4319 43.5289V32.0734L24.6592 22.5943ZM16.9166 31.86V43.4362L6.94509 28.7693L11.1333 21.9908L16.9166 31.86ZM12.0364 20.5316L16.9187 12.6324V28.8641L12.0364 20.5316ZM18.4319 46.1563L29.5156 30.4526L33.0291 36.1405L18.4319 59.765V46.1563ZM6.07426 30.181L16.9187 46.1304V59.6507L2.39262 36.1405L6.07426 30.181Z" fill="#4F5260"/>
</svg>

After

Width:  |  Height:  |  Size: 978 B

+76
View File
@@ -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,
};
}
+44
View File
@@ -0,0 +1,44 @@
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'
const Login: FC = () => {
const { stepLogin, phone } = 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 />
: 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
+83
View File
@@ -0,0 +1,83 @@
import { type FC } from 'react'
import Input from '../../../components/Input'
import { type 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 { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
import { toast } from '@/shared/toast'
import { extractErrorMessage } from '@/config/func'
const LoginStep1: FC = () => {
const { phone, setPhone, setStepLogin } = useAuthStore()
const loginWithOtp = useLoginWithOtp()
const checkHasAccount = useCheckHasAccount()
const formik = useFormik<LoginType>({
initialValues: {
phone_email: phone,
},
validationSchema: Yup.object({
phone_email: Yup.string()
.required('این فیلد اجباری است.')
}),
onSubmit(values) {
setPhone(values.phone_email)
loginWithOtp.mutate({ phone: values.phone_email }, {
onSuccess() {
setStepLogin(2)
toast('کد تایید ارسال شد', 'success')
},
onError(error) {
toast(extractErrorMessage(error), 'error')
},
})
},
})
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
خوش آمدید
</h2>
<p className='text-description text-sm mt-2'>
لطفا اطلاعات خود را وارد کنید.
</p>
</div>
<div className='mt-16'>
<Input
label={'شماره موبایل'}
placeholder={'شماره موبایل خود را وارد کنید'}
type='tel'
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={'ادامه'}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
/>
</div>
)
}
export default LoginStep1
+180
View File
@@ -0,0 +1,180 @@
import { type FC, useEffect } from 'react'
import { type 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 { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
import { extractErrorMessage, setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
import { toast } from '@/shared/toast'
import { Paths } from '@/config/Paths'
const LoginStep2: FC = () => {
const { phone, setStepLogin } = useAuthStore()
const { value, reset } = useCountDown(2, true)
const otpVerify = useOtpVerify()
const loginWithOtp = useLoginWithOtp()
const formik = useFormik<OtpVerifyType>({
initialValues: {
phone: phone,
otp: ''
},
validationSchema: Yup.object({
otp: Yup.string()
.required('این فیلد اجباری است.')
}),
onSubmit(values) {
const params: OtpVerifyType = {
phone: phone,
otp: values.otp
}
otpVerify.mutate(params, {
onSuccess(data) {
setToken(data?.data?.tokens?.accessToken?.token)
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
// Check if there's a redirect parameter in the URL
window.location.href = Paths.home
},
onError(error) {
toast(extractErrorMessage(error), '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 && "otp" in otpCredential) {
formik.setFieldValue("otp", (otpCredential).otp);
}
})
.catch((err) => console.error("SMS AutoFill Error:", err));
return () => ac.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const reSend = () => {
loginWithOtp.mutate({ phone: phone }, {
onSuccess: () => {
reset()
toast('کد مجدد ارسال شد', 'success')
},
onError: (error) => {
toast(extractErrorMessage(error), 'error')
}
})
}
useEffect(() => {
if (formik.values.otp.length === 5) {
formik.handleSubmit()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formik.values.otp])
return (
<div>
<div className='mt-5'>
<h2 className='lg:text-2xl font-bold'>
کد تایید را وارد کنید
</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>کد تایید برای</div>
<div>{phone}</div>
<div>
ارسال شد.
</div>
</div>
<div onClick={() => setStepLogin(1)} className='text-black cursor-pointer'>
تغییر شماره
</div>
</p>
</div>
<div className='mt-16'>
<div className='text-sm'>
کد تایید
</div>
<div className='mt-2 w-full flex justify-center dltr otp'>
<OTPInput
shouldAutoFocus
value={formik.values.otp}
numInputs={5}
inputType='tel'
onChange={(otp: string) => formik.setFieldValue('otp', otp)}
renderInput={(props) =>
<input
{...props}
type='tel'
autoComplete="one-time-otp"
inputMode="numeric"
className='w-full h-[50px] min-w-[50px] flex-1 mx-2 bg-white border border-border rounded-[10px]' />
}
/>
</div>
{
formik.touched.otp && formik.errors.otp &&
<div className='text-xs mt-2 text-red-500'>{formik.errors.otp}</div>
}
<div className='flex mt-4 justify-end'>
{
value !== '00:00' ?
<div className='flex gap-1 text-description text-xs'>
<div>{value}</div>
<div>مانده تا دریافت مجدد کد</div>
</div>
:
<div onClick={reSend} className='flex cursor-pointer gap-1 pl-2 text-black text-sm'>
<div className=''>
ارسال مجدد
</div>
</div>
}
</div>
</div>
<Button
label={'ورود'}
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
+45
View File
@@ -0,0 +1,45 @@
import { useMutation } from '@tanstack/react-query';
import * as api from '../service/AuthService'
import type { 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,
});
};
+50
View File
@@ -0,0 +1,50 @@
import axios from "../../../config/axios";
import type {
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(`/admin/auth/otp/request`, params);
return data;
};
export const otpVerify = async (params: OtpVerifyType) => {
const { data } = await axios.post(`/admin/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(`/public/auth/refresh`, params);
return data;
};
export const logout = async () => {
const { data } = await axios.post(`/auth/logout`);
return data;
};
+17
View File
@@ -0,0 +1,17 @@
import { create } from "zustand";
import { type 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 });
},
}));
+50
View File
@@ -0,0 +1,50 @@
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;
otp: string;
};
export type CheckHasAccountType = {
email: string;
};
export type CheckHasAccountPhoneType = {
phone: string;
};
export type RegisterType = {
phone: string;
firstName: string;
lastName: string;
password: string;
code: number;
};
export type RefreshTokenType = {
refreshToken: string;
};
+2 -1
View File
@@ -186,7 +186,8 @@ export const useCreateLearning = () => {
// Return mock mutation // Return mock mutation
return { return {
mutate: (variables: CreateLearningType, options?: any) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
mutate: (options?: any) => {
// Simulate success // Simulate success
setTimeout(() => { setTimeout(() => {
if (options?.onSuccess) { if (options?.onSuccess) {
@@ -1,5 +1,5 @@
import axios from "../../../config/axios"; import axios from "../../../config/axios";
import { import type {
CreateCategoryLearningType, CreateCategoryLearningType,
CreateLearningType, CreateLearningType,
} from "../types/LearningTypes"; } from "../types/LearningTypes";
+1 -1
View File
@@ -99,7 +99,7 @@ const ProductCategory: FC = () => {
items={data?.data?.map((item) => { items={data?.data?.map((item) => {
return { return {
label: item.title, label: item.title,
value: item.id value: item.id + ''
} }
}) || []} }) || []}
onChange={formik.handleChange} onChange={formik.handleChange}
+1 -1
View File
@@ -68,7 +68,7 @@ const CategoryList: FC = () => {
/> />
</Link> </Link>
<TrashWithConfrim <TrashWithConfrim
onDelete={() => handleDelete(item.id)} onDelete={() => handleDelete(item.id + '')}
isloading={isPending} isloading={isPending}
/> />
+1 -1
View File
@@ -118,7 +118,7 @@ const UpdateCategory: FC = () => {
items={data?.data?.map((item) => { items={data?.data?.map((item) => {
return { return {
label: item.title, label: item.title,
value: item.id value: item.id + ''
} }
}) || []} }) || []}
onChange={formik.handleChange} onChange={formik.handleChange}
@@ -18,7 +18,7 @@ const CategoriesSelect: FC<Props> = (props) => {
items={categories?.data?.map((item) => { items={categories?.data?.map((item) => {
return { return {
label: item.title, label: item.title,
value: item.id value: item.id + ''
} }
}) || []} }) || []}
{...props} {...props}
+1 -1
View File
@@ -21,7 +21,7 @@ const TicketDetail: FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { id } = useParams() const { id } = useParams()
const getMessages = useGetMessages(id ? id : '') const getMessages = useGetMessages(id ? id : '')
const [files, setFiles] = useState<File[]>([]) const [setFiles] = useState<File[]>([])
const addMessage = useAddMessageTicket(id ? id : '') const addMessage = useAddMessageTicket(id ? id : '')
const [isResetFiles, setIsResetFiles] = useState<boolean>(false) const [isResetFiles, setIsResetFiles] = useState<boolean>(false)
const closeTicket = useCloseTicket() const closeTicket = useCloseTicket()
+3 -2
View File
@@ -1,10 +1,11 @@
import Login from '@/pages/auth/Login'
import { type FC } from 'react' import { type FC } from 'react'
import { Routes } from 'react-router-dom' import { Route, Routes } from 'react-router-dom'
const AuthRouter: FC = () => { const AuthRouter: FC = () => {
return ( return (
<Routes> <Routes>
{/* <Route path="/login" element={<Login />} /> */} <Route path="/login" element={<Login />} />
</Routes> </Routes>
) )
} }