@@ -0,0 +1,168 @@
|
|||||||
|
import { useState, type FC } from 'react'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
import UploadBox from '@/components/UploadBox'
|
||||||
|
import PresignedImage from '@/components/PresignedImage'
|
||||||
|
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 { useSingleUpload } from '@/pages/uploader/hooks/useUploader'
|
||||||
|
import { toast } from '@/shared/toast'
|
||||||
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import type { UpdateProfileType } from '@/pages/user/types/Types'
|
||||||
|
import { Edit } from 'iconsax-react'
|
||||||
|
|
||||||
|
const Profile: FC = () => {
|
||||||
|
const { data: user, isLoading } = useGetMe()
|
||||||
|
const updateProfile = useUpdateProfile()
|
||||||
|
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
||||||
|
const [file, setFile] = useState<File>()
|
||||||
|
|
||||||
|
const handleSave = (values: UpdateProfileType) => {
|
||||||
|
updateProfile.mutate(
|
||||||
|
{
|
||||||
|
firstName: values.firstName.trim(),
|
||||||
|
lastName: values.lastName.trim(),
|
||||||
|
address: values.address?.trim() || undefined,
|
||||||
|
...(values.avatarUrl ? { avatarUrl: values.avatarUrl } : {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
toast('پروفایل با موفقیت ویرایش شد', 'success')
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formik = useFormik<UpdateProfileType>({
|
||||||
|
enableReinitialize: true,
|
||||||
|
initialValues: {
|
||||||
|
firstName: user?.firstName ?? '',
|
||||||
|
lastName: user?.lastName ?? '',
|
||||||
|
address: user?.addresse ?? '',
|
||||||
|
avatarUrl: user?.avatarUrl ?? undefined,
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
firstName: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.required('نام اجباری است.'),
|
||||||
|
lastName: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.required('نام خانوادگی اجباری است.'),
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
if (file) {
|
||||||
|
upload(file, {
|
||||||
|
onSuccess(data) {
|
||||||
|
handleSave({
|
||||||
|
...values,
|
||||||
|
avatarUrl: data?.data?.key,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handleSave(values)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className='mt-5 flex h-64 items-center justify-center'>
|
||||||
|
<div className='text-lg'>در حال بارگذاری...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-5'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<h1 className='text-lg font-light'>پروفایل</h1>
|
||||||
|
<Button
|
||||||
|
className='w-fit px-6'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={updateProfile.isPending || isUploading}
|
||||||
|
>
|
||||||
|
<div className='flex gap-1.5'>
|
||||||
|
<Edit size={18} color='black' />
|
||||||
|
<div className='text-[13px] font-light'>ذخیره تغییرات</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 flex-1 rounded-3xl bg-white p-6'>
|
||||||
|
{formik.values.avatarUrl && !file && (
|
||||||
|
<PresignedImage
|
||||||
|
src={formik.values.avatarUrl}
|
||||||
|
className='mb-4 h-20 w-20 rounded-full object-cover'
|
||||||
|
alt='آواتار'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<UploadBox
|
||||||
|
label='آواتار (اختیاری)'
|
||||||
|
onChange={(files) => setFile(files[0])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='rowTwoInput mt-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>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label='شماره موبایل'
|
||||||
|
value={user?.phone ? `0${user.phone}` : ''}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Textarea
|
||||||
|
label='آدرس'
|
||||||
|
placeholder='آدرس خود را وارد کنید'
|
||||||
|
name='address'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
value={formik.values.address ?? ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import axios from "@/config/axios";
|
|
||||||
|
|
||||||
export interface ProfileData {
|
|
||||||
user: {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
email: string;
|
|
||||||
profilePic?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGetProfile = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["profile"],
|
|
||||||
queryFn: async (): Promise<{ data: ProfileData }> => {
|
|
||||||
const response = await axios.get("/profile");
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -19,5 +19,6 @@ export type GetMeResponseType = BaseResponse<UserMeType>;
|
|||||||
export type UpdateProfileType = {
|
export type UpdateProfileType = {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
address?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import LearningList from '@/pages/learning/List'
|
|||||||
import LearningDetail from '@/pages/learning/Detail'
|
import LearningDetail from '@/pages/learning/Detail'
|
||||||
import InvoiceDetail from '@/pages/invoice/Detail'
|
import InvoiceDetail from '@/pages/invoice/Detail'
|
||||||
import PayInvoice from '@/pages/payment/PayInvoice'
|
import PayInvoice from '@/pages/payment/PayInvoice'
|
||||||
|
import Profile from '@/pages/profile/Profile'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -26,6 +27,7 @@ const MainRouter: FC = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path={Paths.home} element={<Home />} />
|
<Route path={Paths.home} element={<Home />} />
|
||||||
|
<Route path={Paths.profile} element={<Profile />} />
|
||||||
<Route path={Paths.myRequests} element={<MyRequests />} />
|
<Route path={Paths.myRequests} element={<MyRequests />} />
|
||||||
<Route path={Paths.order.myOrders} element={<MyOrders />} />
|
<Route path={Paths.order.myOrders} element={<MyOrders />} />
|
||||||
<Route path={Paths.proformaInvoice} element={<ProformaInvoice />} />
|
<Route path={Paths.proformaInvoice} element={<ProformaInvoice />} />
|
||||||
|
|||||||
+13
-23
@@ -1,12 +1,14 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import PresignedImage from '@/components/PresignedImage'
|
import UserAvatar from '@/components/UserAvatar'
|
||||||
import { HambergerMenu, Wallet } from 'iconsax-react'
|
import { HambergerMenu, Wallet } from 'iconsax-react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import Notifications from '@/pages/notification/Notification'
|
import Notifications from '@/pages/notification/Notification'
|
||||||
import { useSharedStore } from './store/useSharedStore'
|
import { useSharedStore } from './store/useSharedStore'
|
||||||
import { t } from '@/locale'
|
import { t } from '@/locale'
|
||||||
import { useGetMe } from '@/pages/user/hooks/useUserData'
|
import { useGetMe } from '@/pages/user/hooks/useUserData'
|
||||||
import { NumberFormat } from '@/config/func'
|
import { NumberFormat } from '@/config/func'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
|
||||||
@@ -14,7 +16,6 @@ const Header: FC = () => {
|
|||||||
const { setOpenSidebar, openSidebar } = useSharedStore()
|
const { setOpenSidebar, openSidebar } = useSharedStore()
|
||||||
|
|
||||||
const displayName = [data?.firstName, data?.lastName].filter(Boolean).join(' ').trim()
|
const displayName = [data?.firstName, data?.lastName].filter(Boolean).join(' ').trim()
|
||||||
const initials = `${String(data?.firstName ?? '').charAt(0)}${String(data?.lastName ?? '').charAt(0)}`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed z-10 left-[var(--layout-frame)] right-[var(--layout-frame)] top-[var(--layout-frame)] flex h-[var(--layout-header-height)] items-center justify-between rounded-[32px] bg-white px-6 xl:left-auto xl:right-[calc(var(--layout-frame)+var(--layout-main-offset))] xl:h-[var(--layout-header-height-xl)] xl:w-[calc(100%-(var(--layout-frame)*2)-var(--layout-main-offset))]'>
|
<div className='fixed z-10 left-[var(--layout-frame)] right-[var(--layout-frame)] top-[var(--layout-frame)] flex h-[var(--layout-header-height)] items-center justify-between rounded-[32px] bg-white px-6 xl:left-auto xl:right-[calc(var(--layout-frame)+var(--layout-main-offset))] xl:h-[var(--layout-header-height-xl)] xl:w-[calc(100%-(var(--layout-frame)*2)-var(--layout-main-offset))]'>
|
||||||
@@ -29,15 +30,7 @@ const Header: FC = () => {
|
|||||||
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
|
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
|
||||||
<HambergerMenu size={24} color='black' />
|
<HambergerMenu size={24} color='black' />
|
||||||
</div>
|
</div>
|
||||||
{/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */}
|
|
||||||
<div className='flex xl:gap-6 gap-4 items-center'>
|
<div className='flex xl:gap-6 gap-4 items-center'>
|
||||||
{/* <Link to={Pages.services.other}>
|
|
||||||
<Element3 color='black' className='xl:size-[18px] size-[17px]' />
|
|
||||||
</Link>
|
|
||||||
<Link className='xl:hidden' to={Pages.wallet}>
|
|
||||||
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
|
|
||||||
</Link>
|
|
||||||
<Link className='hidden xl:block' to={Pages.wallet}> */}
|
|
||||||
<div className='flex items-center h-8 pl-2 rounded-full bg-[#EEF0F7]'>
|
<div className='flex items-center h-8 pl-2 rounded-full bg-[#EEF0F7]'>
|
||||||
<div className='px-3 text-xs'>
|
<div className='px-3 text-xs'>
|
||||||
{NumberFormat(data?.maxCredit) + ' ' + t('rial')}
|
{NumberFormat(data?.maxCredit) + ' ' + t('rial')}
|
||||||
@@ -46,25 +39,22 @@ const Header: FC = () => {
|
|||||||
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
|
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* </Link> */}
|
|
||||||
<Notifications />
|
<Notifications />
|
||||||
{displayName && (
|
{data && (
|
||||||
<div className='flex gap-2 items-center'>
|
<Link to={Paths.profile} className='flex gap-2 items-center'>
|
||||||
<div className='size-6 rounded-full bg-description overflow-hidden flex items-center justify-center text-white text-xs font-medium'>
|
<UserAvatar
|
||||||
{data?.avatarUrl ? (
|
|
||||||
<PresignedImage
|
|
||||||
src={data.avatarUrl}
|
src={data.avatarUrl}
|
||||||
className='size-full object-cover'
|
firstName={data.firstName}
|
||||||
alt=''
|
lastName={data.lastName}
|
||||||
|
className='size-6'
|
||||||
|
textClassName='text-xs'
|
||||||
/>
|
/>
|
||||||
) : (
|
{displayName && (
|
||||||
initials
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='xl:block hidden text-xs'>
|
<div className='xl:block hidden text-xs'>
|
||||||
{displayName}
|
{displayName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user