complete profile
This commit is contained in:
+31
-9
@@ -4,15 +4,17 @@ import 'swiper/swiper-bundle.css';
|
|||||||
import 'react-loading-skeleton/dist/skeleton.css'
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
// import { getToken } from './config/func'
|
|
||||||
import MainRouter from './router/MainRouter'
|
import MainRouter from './router/MainRouter'
|
||||||
// import AuthRouter from './router/AuthRouter'
|
|
||||||
import ToastContainer from './components/Toast'
|
import ToastContainer from './components/Toast'
|
||||||
import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
|
import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
|
||||||
import AuthRouter from './router/AuthRouter';
|
import AuthRouter from './router/AuthRouter';
|
||||||
|
import CompleteProfileRouter from './router/CompleteProfileRouter';
|
||||||
import type { IApiErrorRepsonse } from './shared/types/Types';
|
import type { IApiErrorRepsonse } from './shared/types/Types';
|
||||||
import { Paths } from './config/Paths';
|
import { Paths } from './config/Paths';
|
||||||
import { refreshToken } from './pages/auth/service/AuthService';
|
import { refreshToken } from './pages/auth/service/AuthService';
|
||||||
|
import { useGetMe } from './pages/user/hooks/useUserData';
|
||||||
|
import { needsProfileCompletion } from './pages/user/utils/profileUtils';
|
||||||
|
import MoonLoader from 'react-spinners/MoonLoader';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -123,17 +125,37 @@ const queryClient = new QueryClient({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const App: FC = () => {
|
const AppRoutes: FC = () => {
|
||||||
|
const isLoggedIn = !!getToken()
|
||||||
const isLoggedIn = getToken()
|
const { data: user, isLoading } = useGetMe({ enabled: isLoggedIn })
|
||||||
|
const profileIncomplete = isLoggedIn && needsProfileCompletion(user)
|
||||||
|
|
||||||
|
if (isLoggedIn && isLoading) {
|
||||||
|
return (
|
||||||
|
<div className='w-full h-screen flex items-center justify-center'>
|
||||||
|
<MoonLoader size={32} color='#000' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
{!isLoggedIn ? (
|
||||||
|
<AuthRouter />
|
||||||
|
) : profileIncomplete ? (
|
||||||
|
<CompleteProfileRouter />
|
||||||
|
) : (
|
||||||
|
<MainRouter />
|
||||||
|
)}
|
||||||
|
<ToastContainer />
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: FC = () => {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<AppRoutes />
|
||||||
{isLoggedIn ? <MainRouter /> : <AuthRouter />}
|
|
||||||
<ToastContainer />
|
|
||||||
</BrowserRouter>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const Paths = {
|
|||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
login: '/login',
|
login: '/login',
|
||||||
|
completeProfile: '/complete-profile',
|
||||||
},
|
},
|
||||||
payment: {
|
payment: {
|
||||||
payInvoice: '/payments/invoice/',
|
payInvoice: '/payments/invoice/',
|
||||||
|
|||||||
@@ -22,10 +22,8 @@ export const statusRegistry: Record<string, StatusConfig> = {
|
|||||||
[ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED]: { label: 'تایید جزئی', variant: 'info' },
|
[ProformaInvoiceStatusEnum.PARTIALLY_CONFIRMED]: { label: 'تایید جزئی', variant: 'info' },
|
||||||
[ProformaInvoiceStatusEnum.CONFIRMED]: { label: 'تایید شده', variant: 'success' },
|
[ProformaInvoiceStatusEnum.CONFIRMED]: { label: 'تایید شده', variant: 'success' },
|
||||||
|
|
||||||
// --- Invoice confirm status (from API) ---
|
// --- Invoice confirm status (from API; partially_confirmed/confirmed covered by enum above) ---
|
||||||
pending: { label: 'تایید نشده', variant: 'warning' },
|
pending: { label: 'تایید نشده', variant: 'warning' },
|
||||||
partially_confirmed: { label: 'تایید جزئی', variant: 'info' },
|
|
||||||
confirmed: { label: 'تایید شده', variant: 'success' },
|
|
||||||
|
|
||||||
// --- OrderStatusEnum ---
|
// --- OrderStatusEnum ---
|
||||||
[OrderStatusEnum.CREATED]: { label: 'ایجاد شده', variant: 'info' },
|
[OrderStatusEnum.CREATED]: { label: 'ایجاد شده', variant: 'info' },
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import LogoImage from '../../assets/images/logo.svg'
|
||||||
|
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||||
|
import CompleteProfileForm from './components/CompleteProfileForm'
|
||||||
|
|
||||||
|
const CompleteProfile: FC = () => {
|
||||||
|
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' />
|
||||||
|
<CompleteProfileForm />
|
||||||
|
</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 CompleteProfile
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import Error from '@/components/Error'
|
||||||
|
import { useGetMe, useUpdateProfile } from '@/pages/user/hooks/useUserData'
|
||||||
|
import { toast } from '@/shared/toast'
|
||||||
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
import type { UpdateProfileType } from '@/pages/user/types/Types'
|
||||||
|
|
||||||
|
const CompleteProfileForm: FC = () => {
|
||||||
|
const { data: user } = useGetMe()
|
||||||
|
const updateProfile = useUpdateProfile()
|
||||||
|
|
||||||
|
const formik = useFormik<UpdateProfileType>({
|
||||||
|
enableReinitialize: true,
|
||||||
|
initialValues: {
|
||||||
|
firstName: user?.firstName ?? '',
|
||||||
|
lastName: user?.lastName ?? '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
firstName: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.required('نام اجباری است.'),
|
||||||
|
lastName: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.required('نام خانوادگی اجباری است.'),
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
updateProfile.mutate(
|
||||||
|
{
|
||||||
|
firstName: values.firstName.trim(),
|
||||||
|
lastName: values.lastName.trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
toast('اطلاعات شما با موفقیت ثبت شد', 'success')
|
||||||
|
window.location.href = Paths.home
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='mt-5'>
|
||||||
|
<h2 className='lg:text-2xl font-bold'>
|
||||||
|
تکمیل اطلاعات
|
||||||
|
</h2>
|
||||||
|
<p className='text-description text-sm lg:mt-2 mt-3'>
|
||||||
|
لطفاً نام و نام خانوادگی خود را وارد کنید .
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-16 flex flex-col gap-6'>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='نام'
|
||||||
|
placeholder='نام خود را وارد کنید'
|
||||||
|
name='firstName'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
value={formik.values.firstName}
|
||||||
|
/>
|
||||||
|
{formik.touched.firstName && formik.errors.firstName && (
|
||||||
|
<Error errorText={formik.errors.firstName} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='نام خانوادگی'
|
||||||
|
placeholder='نام خانوادگی خود را وارد کنید'
|
||||||
|
name='lastName'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
value={formik.values.lastName}
|
||||||
|
/>
|
||||||
|
{formik.touched.lastName && formik.errors.lastName && (
|
||||||
|
<Error errorText={formik.errors.lastName} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label='ثبت و ادامه'
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={updateProfile.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CompleteProfileForm
|
||||||
@@ -12,6 +12,7 @@ import { extractErrorMessage, setRefreshToken } from '../../../config/func'
|
|||||||
import { setToken } from '../../../config/func'
|
import { setToken } from '../../../config/func'
|
||||||
import { toast } from '@/shared/toast'
|
import { toast } from '@/shared/toast'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
|
import { needsProfileCompletion } from '@/pages/user/utils/profileUtils'
|
||||||
|
|
||||||
const LoginStep2: FC = () => {
|
const LoginStep2: FC = () => {
|
||||||
|
|
||||||
@@ -38,8 +39,10 @@ const LoginStep2: FC = () => {
|
|||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setToken(data?.data?.tokens?.accessToken?.token)
|
setToken(data?.data?.tokens?.accessToken?.token)
|
||||||
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
|
setRefreshToken(data?.data?.tokens?.refreshToken?.token)
|
||||||
// Check if there's a redirect parameter in the URL
|
const user = data?.data?.user
|
||||||
window.location.href = Paths.profile
|
window.location.href = needsProfileCompletion(user)
|
||||||
|
? Paths.auth.completeProfile
|
||||||
|
: Paths.home
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast(extractErrorMessage(error), 'error')
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/UserService";
|
import * as api from "../service/UserService";
|
||||||
|
import type { UpdateProfileType } from "../types/Types";
|
||||||
|
|
||||||
export const useGetMe = () => {
|
export const useGetMe = (options?: { enabled?: boolean }) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["me"],
|
queryKey: ["me"],
|
||||||
queryFn: api.getMe,
|
queryFn: api.getMe,
|
||||||
|
enabled: options?.enabled ?? true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateProfile = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: UpdateProfileType) => api.updateProfile(variables),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(["me"], data);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import axios from "@/config/axios";
|
import axios from "@/config/axios";
|
||||||
import type { GetMeResponseType } from "@/pages/user/types/Types";
|
import type { GetMeResponseType, UpdateProfileType } from "@/pages/user/types/Types";
|
||||||
|
|
||||||
export const getMe = async (): Promise<GetMeResponseType["data"]> => {
|
export const getMe = async (): Promise<GetMeResponseType["data"]> => {
|
||||||
const { data } = await axios.get<GetMeResponseType>("/public/users/me");
|
const { data } = await axios.get<GetMeResponseType>("/public/users/me");
|
||||||
return data.data;
|
return data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateProfile = async (
|
||||||
|
params: UpdateProfileType,
|
||||||
|
): Promise<GetMeResponseType["data"]> => {
|
||||||
|
const { data } = await axios.patch<GetMeResponseType>(
|
||||||
|
"/public/users/update",
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,3 +14,8 @@ export interface UserMeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type GetMeResponseType = BaseResponse<UserMeType>;
|
export type GetMeResponseType = BaseResponse<UserMeType>;
|
||||||
|
|
||||||
|
export type UpdateProfileType = {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
type ProfileNameFields = {
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const needsProfileCompletion = (user?: ProfileNameFields | null): boolean => {
|
||||||
|
if (!user) return false;
|
||||||
|
return !user.firstName?.trim() || !user.lastName?.trim();
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import CompleteProfile from '@/pages/auth/CompleteProfile'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import { Route, Routes } from 'react-router-dom'
|
||||||
|
|
||||||
|
const CompleteProfileRouter: FC = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="*" element={<CompleteProfile />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CompleteProfileRouter
|
||||||
Reference in New Issue
Block a user