diff --git a/.env b/.env index 86a5253..d366931 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -VITE_API_BASE_URL = 'http://192.168.99.235:4000' -VITE_TOKEN_NAME = 'negareh_t' -VITE_REFRESH_TOKEN_NAME = 'negareh_rt' \ No newline at end of file +VITE_API_BASE_URL = 'http://10.22.13.88:4000' +VITE_TOKEN_NAME = 'negareh_at' +VITE_REFRESH_TOKEN_NAME = 'negareh_art' \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 70d6589..c6814b4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,9 +6,9 @@ import 'react-toastify/dist/ReactToastify.css' import { BrowserRouter } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ToastContainer } from 'react-toastify' -// import { getToken } from './config/func' +import { getToken } from './config/func' import MainRouter from './router/MainRouter' -// import AuthRouter from './router/AuthRouter' +import AuthRouter from './router/AuthRouter' // ایجاد QueryClient برای React Query const queryClient = new QueryClient({ @@ -23,13 +23,12 @@ const queryClient = new QueryClient({ const App: FC = () => { - // const isLoggedIn = getToken() + const isLoggedIn = getToken() return ( - {/* {isLoggedIn ? : } */} - + {isLoggedIn ? : } + + + + diff --git a/src/hooks/useCountDown.ts b/src/hooks/useCountDown.ts new file mode 100755 index 0000000..c4baa29 --- /dev/null +++ b/src/hooks/useCountDown.ts @@ -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({ m: minutes, s: 0 }); + const [stop, setStop] = useState(!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, + }; +} diff --git a/src/pages/auth/Login.tsx b/src/pages/auth/Login.tsx new file mode 100644 index 0000000..88445cc --- /dev/null +++ b/src/pages/auth/Login.tsx @@ -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 ( +
+
+
+
+ +
+ + + { + stepLogin === 1 ? + + : + stepLogin === 2 && !!phone ? + + : null + } +
+ + +
+
+ +
+ +
+
+
+ ) +} + +export default Login \ No newline at end of file diff --git a/src/pages/auth/components/LoginStep1.tsx b/src/pages/auth/components/LoginStep1.tsx new file mode 100644 index 0000000..f4db8b7 --- /dev/null +++ b/src/pages/auth/components/LoginStep1.tsx @@ -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({ + 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 ( +
+
+

+ خوش آمدید +

+

+ لطفا اطلاعات خود را وارد کنید. +

+
+ +
+ + + { + formik.touched.phone_email && formik.errors.phone_email && + + } + +
+ + +
+ ) +} + +export default LoginStep1 \ No newline at end of file diff --git a/src/pages/auth/components/LoginStep2.tsx b/src/pages/auth/components/LoginStep2.tsx new file mode 100644 index 0000000..4d2b26f --- /dev/null +++ b/src/pages/auth/components/LoginStep2.tsx @@ -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({ + 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 ( +
+
+

+ کد تایید را وارد کنید +

+

+

+
کد تایید برای
+
{phone}
+
+ ارسال شد. +
+
+
setStepLogin(1)} className='text-black cursor-pointer'> + تغییر شماره +
+

+
+ +
+
+ کد تایید +
+
+ formik.setFieldValue('otp', otp)} + renderInput={(props) => + + } + /> +
+ + { + formik.touched.otp && formik.errors.otp && +
{formik.errors.otp}
+ } + +
+ { + value !== '00:00' ? +
+
{value}
+
مانده تا دریافت مجدد کد
+
+ : +
+
+ ارسال مجدد +
+
+ + } + +
+ +
+ + +
+ ) +} + +export default LoginStep2 \ No newline at end of file diff --git a/src/pages/auth/hooks/useAuthData.tsx b/src/pages/auth/hooks/useAuthData.tsx new file mode 100644 index 0000000..a602b62 --- /dev/null +++ b/src/pages/auth/hooks/useAuthData.tsx @@ -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, + }); +}; \ No newline at end of file diff --git a/src/pages/auth/service/AuthService.ts b/src/pages/auth/service/AuthService.ts new file mode 100644 index 0000000..a3c7d7f --- /dev/null +++ b/src/pages/auth/service/AuthService.ts @@ -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; +}; diff --git a/src/pages/auth/store/AuthStore.ts b/src/pages/auth/store/AuthStore.ts new file mode 100644 index 0000000..c6751b4 --- /dev/null +++ b/src/pages/auth/store/AuthStore.ts @@ -0,0 +1,17 @@ +import { create } from "zustand"; +import { type AuthStoreType } from "../../auth/types/AuthTypes"; + +export const useAuthStore = create((set) => ({ + phone: "", + setPhone(value) { + set({ phone: value }); + }, + email: "", + setEmail(value) { + set({ email: value }); + }, + stepLogin: 1, + setStepLogin(value) { + set({ stepLogin: value }); + }, +})); diff --git a/src/pages/auth/types/AuthTypes.ts b/src/pages/auth/types/AuthTypes.ts new file mode 100644 index 0000000..dc3b6fe --- /dev/null +++ b/src/pages/auth/types/AuthTypes.ts @@ -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; +}; diff --git a/src/pages/learning/hooks/useLearningData.ts b/src/pages/learning/hooks/useLearningData.ts index cc7b136..bc5748b 100644 --- a/src/pages/learning/hooks/useLearningData.ts +++ b/src/pages/learning/hooks/useLearningData.ts @@ -186,7 +186,8 @@ export const useCreateLearning = () => { // Return mock mutation return { - mutate: (variables: CreateLearningType, options?: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mutate: (options?: any) => { // Simulate success setTimeout(() => { if (options?.onSuccess) { diff --git a/src/pages/learning/service/LearningService.ts b/src/pages/learning/service/LearningService.ts index 74a7266..9665c72 100644 --- a/src/pages/learning/service/LearningService.ts +++ b/src/pages/learning/service/LearningService.ts @@ -1,5 +1,5 @@ import axios from "../../../config/axios"; -import { +import type { CreateCategoryLearningType, CreateLearningType, } from "../types/LearningTypes"; diff --git a/src/pages/product/category/Create.tsx b/src/pages/product/category/Create.tsx index c704082..d8298e5 100644 --- a/src/pages/product/category/Create.tsx +++ b/src/pages/product/category/Create.tsx @@ -99,7 +99,7 @@ const ProductCategory: FC = () => { items={data?.data?.map((item) => { return { label: item.title, - value: item.id + value: item.id + '' } }) || []} onChange={formik.handleChange} diff --git a/src/pages/product/category/List.tsx b/src/pages/product/category/List.tsx index 6421e4e..4307a55 100644 --- a/src/pages/product/category/List.tsx +++ b/src/pages/product/category/List.tsx @@ -68,7 +68,7 @@ const CategoryList: FC = () => { /> handleDelete(item.id)} + onDelete={() => handleDelete(item.id + '')} isloading={isPending} /> diff --git a/src/pages/product/category/Update.tsx b/src/pages/product/category/Update.tsx index 738ac54..5260d8c 100644 --- a/src/pages/product/category/Update.tsx +++ b/src/pages/product/category/Update.tsx @@ -118,7 +118,7 @@ const UpdateCategory: FC = () => { items={data?.data?.map((item) => { return { label: item.title, - value: item.id + value: item.id + '' } }) || []} onChange={formik.handleChange} diff --git a/src/pages/product/components/CategoriesSelect.tsx b/src/pages/product/components/CategoriesSelect.tsx index 5306f24..cbc7bd8 100644 --- a/src/pages/product/components/CategoriesSelect.tsx +++ b/src/pages/product/components/CategoriesSelect.tsx @@ -18,7 +18,7 @@ const CategoriesSelect: FC = (props) => { items={categories?.data?.map((item) => { return { label: item.title, - value: item.id + value: item.id + '' } }) || []} {...props} diff --git a/src/pages/ticket/Detail.tsx b/src/pages/ticket/Detail.tsx index dc27c01..dd20ade 100644 --- a/src/pages/ticket/Detail.tsx +++ b/src/pages/ticket/Detail.tsx @@ -21,7 +21,7 @@ const TicketDetail: FC = () => { const navigate = useNavigate() const { id } = useParams() const getMessages = useGetMessages(id ? id : '') - const [files, setFiles] = useState([]) + const [setFiles] = useState([]) const addMessage = useAddMessageTicket(id ? id : '') const [isResetFiles, setIsResetFiles] = useState(false) const closeTicket = useCloseTicket() diff --git a/src/router/AuthRouter.tsx b/src/router/AuthRouter.tsx index 3a10b61..84434f0 100644 --- a/src/router/AuthRouter.tsx +++ b/src/router/AuthRouter.tsx @@ -1,10 +1,11 @@ +import Login from '@/pages/auth/Login' import { type FC } from 'react' -import { Routes } from 'react-router-dom' +import { Route, Routes } from 'react-router-dom' const AuthRouter: FC = () => { return ( - {/* } /> */} + } /> ) }