profile
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-21 16:05:15 +03:30
parent 830e229cf3
commit 04687b855d
5 changed files with 190 additions and 50 deletions
+168
View File
@@ -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
-21
View File
@@ -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;
},
});
};
+1
View File
@@ -19,5 +19,6 @@ export type GetMeResponseType = BaseResponse<UserMeType>;
export type UpdateProfileType = {
firstName: string;
lastName: string;
address?: string;
avatarUrl?: string;
};
+2
View File
@@ -19,6 +19,7 @@ import LearningList from '@/pages/learning/List'
import LearningDetail from '@/pages/learning/Detail'
import InvoiceDetail from '@/pages/invoice/Detail'
import PayInvoice from '@/pages/payment/PayInvoice'
import Profile from '@/pages/profile/Profile'
const MainRouter: FC = () => {
return (
@@ -26,6 +27,7 @@ const MainRouter: FC = () => {
<Routes>
<Route path="/" element={<Home />} />
<Route path={Paths.home} element={<Home />} />
<Route path={Paths.profile} element={<Profile />} />
<Route path={Paths.myRequests} element={<MyRequests />} />
<Route path={Paths.order.myOrders} element={<MyOrders />} />
<Route path={Paths.proformaInvoice} element={<ProformaInvoice />} />
+19 -29
View File
@@ -1,12 +1,14 @@
import { type FC } from 'react'
import Input from '@/components/Input'
import PresignedImage from '@/components/PresignedImage'
import UserAvatar from '@/components/UserAvatar'
import { HambergerMenu, Wallet } from 'iconsax-react'
import { Link } from 'react-router-dom'
import Notifications from '@/pages/notification/Notification'
import { useSharedStore } from './store/useSharedStore'
import { t } from '@/locale'
import { useGetMe } from '@/pages/user/hooks/useUserData'
import { NumberFormat } from '@/config/func'
import { Paths } from '@/config/Paths'
const Header: FC = () => {
@@ -14,7 +16,6 @@ const Header: FC = () => {
const { setOpenSidebar, openSidebar } = useSharedStore()
const displayName = [data?.firstName, data?.lastName].filter(Boolean).join(' ').trim()
const initials = `${String(data?.firstName ?? '').charAt(0)}${String(data?.lastName ?? '').charAt(0)}`
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))]'>
@@ -29,15 +30,7 @@ const Header: FC = () => {
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
<HambergerMenu size={24} color='black' />
</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'>
{/* <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='px-3 text-xs'>
{NumberFormat(data?.maxCredit) + ' ' + t('rial')}
@@ -46,29 +39,26 @@ const Header: FC = () => {
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
</div>
</div>
{/* </Link> */}
<Notifications />
{displayName && (
<div 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'>
{data?.avatarUrl ? (
<PresignedImage
src={data.avatarUrl}
className='size-full object-cover'
alt=''
/>
) : (
initials
)}
</div>
<div className='xl:block hidden text-xs'>
{displayName}
</div>
</div>
{data && (
<Link to={Paths.profile} className='flex gap-2 items-center'>
<UserAvatar
src={data.avatarUrl}
firstName={data.firstName}
lastName={data.lastName}
className='size-6'
textClassName='text-xs'
/>
{displayName && (
<div className='xl:block hidden text-xs'>
{displayName}
</div>
)}
</Link>
)}
</div>
</div>
)
}
export default Header
export default Header