edit profile

This commit is contained in:
hamid zarghami
2026-07-25 10:59:52 +03:30
parent 52bb36ac3c
commit 42528f4d5c
9 changed files with 356 additions and 197 deletions
+4 -1
View File
@@ -786,7 +786,10 @@
"confrim_email": "تایید ایمیل", "confrim_email": "تایید ایمیل",
"confrim": "تایید صورتحساب ", "confrim": "تایید صورتحساب ",
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.", "link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
"email_not_verified": "ایمیل تایید نشده" "email_not_verified": "ایمیل تایید نشده",
"resend_email": "ارسال مجدد تایید",
"first_name": "نام",
"last_name": "نام خانوادگی"
}, },
"email": "ایمیل", "email": "ایمیل",
"customer": { "customer": {
+14 -125
View File
@@ -1,70 +1,31 @@
import { FC, useCallback, useEffect, useState } from 'react' import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AvatarImage from '../../assets/images/avatar_image.png' import AvatarImage from '../../assets/images/avatar_image.png'
import Button from '../../components/Button' import Button from '../../components/Button'
import { DocumentUpload } from 'iconsax-react' import { DocumentUpload } from 'iconsax-react'
import Input from '../../components/Input'
import DatePickerComponent from '../../components/DatePicker'
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData' import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
import Username from './components/Username' import Username from './components/Username'
import { useDropzone } from 'react-dropzone' import { useDropzone } from 'react-dropzone'
import { toast } from '../../components/Toast'; import { toast } from '../../components/Toast'
import { ErrorType } from '../../helpers/types' import { ErrorType } from '../../helpers/types'
import { UpdateProfileType } from './types/ProfileTypes' import { UpdateProfileType } from './types/ProfileTypes'
import PageLoading from '../../components/PageLoading' import PageLoading from '../../components/PageLoading'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import Email from './components/Email' import Email from './components/Email'
import Phone from './components/Phone' import Phone from './components/Phone'
import { useGetProvines } from '../customer/hooks/useCustomerData' import FirstName from './components/FirstName'
import LastName from './components/LastName'
import BirthDate from './components/BirthDate'
import NationalCode from './components/NationalCode'
import { useSingleUpload } from '../service/hooks/useServiceData' import { useSingleUpload } from '../service/hooks/useServiceData'
const Profile: FC = () => { const Profile: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [file, setFile] = useState<File>() const [file, setFile] = useState<File>()
const getProvines = useGetProvines()
const getProfile = useGetProfile() const getProfile = useGetProfile()
const singleUpload = useSingleUpload() const singleUpload = useSingleUpload()
const updateProfile = useUpdateProfile() const updateProfile = useUpdateProfile()
const formik = useFormik<UpdateProfileType>({
initialValues: {
cityId: '',
userAddress: '',
postalCode: '',
},
validationSchema: Yup.object({
cityId: Yup.string().required(t('errors.required')),
userAddress: Yup.string().required(t('errors.required')),
postalCode: Yup.string().required(t('errors.required')),
}),
onSubmit: (values) => {
updateProfile.mutate(values, {
onSuccess: () => {
toast(t('success'), 'success')
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
})
useEffect(() => {
if (getProfile.data) {
formik.setValues({
cityId: getProfile.data.data.user.city?.id || '',
userAddress: getProfile.data.data.user.userAddress || '',
postalCode: getProfile.data.data.user.postalCode || '',
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getProfile.data])
const onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) { if (acceptedFiles.length > 0) {
@@ -83,12 +44,12 @@ const Profile: FC = () => {
toast(t('profile.image_uploaded_successfully'), 'success') toast(t('profile.image_uploaded_successfully'), 'success')
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error') toast(error.response?.data?.error?.message?.[0], 'error')
} }
}) })
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error') toast(error.response?.data?.error?.message?.[0], 'error')
} }
}) })
} else { } else {
@@ -107,7 +68,7 @@ const Profile: FC = () => {
</div> </div>
{ {
getProfile.isPending || getProvines.isPending ? getProfile.isPending ?
<PageLoading /> <PageLoading />
: :
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'> <div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
@@ -175,87 +136,15 @@ const Profile: FC = () => {
</div> </div>
</div> </div>
<div className='flex-1'> <div className='flex-1'>
<DatePickerComponent <FirstName firstName={getProfile.data?.data?.user?.firstName} />
label={t('profile.date_of_birth')} <LastName lastName={getProfile.data?.data?.user?.lastName} />
onChange={() => { }}
placeholder=''
defaulValue={getProfile.data?.data?.user?.birthDate}
/>
<div className='xl:mt-7 mt-4'> <div className='xl:mt-7 mt-4'>
<Input <BirthDate birthDate={getProfile.data?.data?.user?.birthDate} />
label={t('profile.national_code')}
value={getProfile.data?.data?.user?.nationalCode}
/>
</div> </div>
<NationalCode nationalCode={getProfile.data?.data?.user?.nationalCode} />
</div> </div>
</div> </div>
{/* <div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'>
<div className='flex-1'>
<div>
{t('profile.address')}
</div>
<div className='text-description text-xs mt-2'>
{t('profile.address_live')}
</div>
</div>
<div className='flex-1'>
<div className='rowTwoInput'>
<Select
label={t('profile.province')}
items={getProvines.data?.data?.provinces.map((item: ProvinesItemType) => ({ label: item.name, value: item.id }))}
onChange={(e) => setProvinesId(e.target.value)}
className='bg-white border'
placeholder={t('select')}
value={provinesId}
/>
<Select
label={t('profile.city')}
items={getCities.data?.data?.cities ? getCities.data?.data?.cities.map((item: ProvinesItemType) => ({ label: item.name, value: item.id })) : []}
placeholder={t('select')}
{...formik.getFieldProps('cityId')}
error_text={formik.touched.cityId && formik.errors.cityId ? formik.errors.cityId : ''}
/>
</div>
<div className='xl:mt-7 mt-4'>
<Input
label={t('profile.postal_code')}
{...formik.getFieldProps('postalCode')}
error_text={formik.touched.postalCode && formik.errors.postalCode ? formik.errors.postalCode : ''}
value={formik.values.postalCode}
/>
</div>
<div className='xl:mt-7 mt-4'>
<Textarea
label={t('profile.address')}
defaultValue={formik.values.userAddress}
{...formik.getFieldProps('userAddress')}
error_text={formik.touched.userAddress && formik.errors.userAddress ? formik.errors.userAddress : ''}
/>
</div>
<div className='flex justify-end mt-5'>
<Button
className='xl:w-fit px-10'
onClick={() => formik.handleSubmit()}
isLoading={updateProfile.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('save')}
</div>
</div>
</Button>
</div>
</div>
</div> */}
<div className='h-20 xl:hidden'></div> <div className='h-20 xl:hidden'></div>
</div> </div>
} }
@@ -264,4 +153,4 @@ const Profile: FC = () => {
) )
} }
export default Profile export default Profile
@@ -0,0 +1,80 @@
import { FC, useEffect, useState } from 'react'
import Input from '../../../components/Input'
import Button from '../../../components/Button'
import DatePickerComponent from '../../../components/DatePicker'
import { Edit } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import { useUpdateProfile } from '../hooks/useProfileData'
import { UpdateProfileType } from '../types/ProfileTypes'
import { toast } from '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
type Props = {
birthDate: string | null | undefined
}
const BirthDate: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [value, setValue] = useState<string>(props.birthDate || '')
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
const updateProfile = useUpdateProfile()
useEffect(() => {
setValue(props.birthDate || '')
}, [props.birthDate])
const handleSave = () => {
const params: UpdateProfileType = { birthDate: value }
updateProfile.mutate(params, {
onSuccess: () => {
toast(t('success'), 'success')
setIsReadOnly(true)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message?.[0], 'error')
}
})
}
const isDisabled = !value || value === (props.birthDate || '')
return (
<div className='flex items-end xl:gap-6 gap-3'>
{
isReadOnly ? (
<Input
label={t('profile.date_of_birth')}
value={value}
readOnly
/>
) : (
<DatePickerComponent
label={t('profile.date_of_birth')}
onChange={setValue}
placeholder=''
defaulValue={value}
/>
)
}
{
isReadOnly ? (
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>{t('edit')}</div>
</div>
) : (
<Button
label={t('save')}
className='px-4 w-fit'
disabled={isDisabled}
onClick={handleSave}
isLoading={updateProfile.isPending}
/>
)
}
</div>
)
}
export default BirthDate
+31 -69
View File
@@ -1,14 +1,12 @@
import { FC, Fragment, useState } from 'react' import { FC, Fragment, useEffect, useState } from 'react'
import Input from '../../../components/Input' import Input from '../../../components/Input'
import { Edit, TickCircle } from 'iconsax-react' import { Edit } from 'iconsax-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '../../../components/Button' import Button from '../../../components/Button'
import { isEmail } from '../../../config/func' import { isEmail } from '../../../config/func'
import DefaulModal from '../../../components/DefaulModal'
import OTPInput from 'react-otp-input'
import { useUpdateEmail } from '../hooks/useProfileData' import { useUpdateEmail } from '../hooks/useProfileData'
import { ErrorType } from '../../../helpers/types' import { ErrorType } from '../../../helpers/types'
import { toast } from '../../../components/Toast'; import { toast } from '../../../components/Toast'
type Props = { type Props = {
email: string | null, email: string | null,
@@ -20,17 +18,20 @@ const Email: FC<Props> = (props: Props) => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const [email, setEmail] = useState<string>(props.email ? props.email : '') const [email, setEmail] = useState<string>(props.email ? props.email : '')
const [isReadOnly, setIsReadOnly] = useState<boolean>(true) const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
const [showModal, setShowModal] = useState<boolean>(false)
const [code, setCode] = useState<string>('')
const updateEmail = useUpdateEmail() const updateEmail = useUpdateEmail()
const handleShowModal = () => { useEffect(() => {
updateEmail.mutate({ email: email }, { setEmail(props.email ? props.email : '')
}, [props.email])
const handleSendVerification = () => {
updateEmail.mutate({ email }, {
onSuccess: () => { onSuccess: () => {
toast(t('profile.link_email'), 'success') toast(t('profile.link_email'), 'success')
setIsReadOnly(true)
}, },
onError: (error: ErrorType) => { onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error') toast(error.response?.data?.error?.message?.[0], 'error')
} }
}) })
} }
@@ -45,17 +46,30 @@ const Email: FC<Props> = (props: Props) => {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
/> />
{ {
!props.isVerified && !props.isVerified && isReadOnly &&
<div className='text-[10px] absolute left-24 text-red-400 top-[34px]'> <div className='text-[10px] absolute left-24 text-red-400 top-[34px]'>
{t('profile.email_not_verified')} {t('profile.email_not_verified')}
</div> </div>
} }
{ {
isReadOnly ? isReadOnly ?
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'> <div className='flex items-center gap-3 relative -top-3'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' /> {
<div> !props.isVerified && !!email &&
{t('edit')} <button
type='button'
onClick={handleSendVerification}
disabled={updateEmail.isPending}
className='text-[#0047FF] text-xs whitespace-nowrap disabled:opacity-50'
>
{t('profile.resend_email')}
</button>
}
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>
{t('edit')}
</div>
</div> </div>
</div> </div>
: :
@@ -63,65 +77,13 @@ const Email: FC<Props> = (props: Props) => {
label={t('save')} label={t('save')}
className='px-4 w-fit' className='px-4 w-fit'
disabled={!isEmail(email)} disabled={!isEmail(email)}
onClick={handleShowModal} onClick={handleSendVerification}
isLoading={updateEmail.isPending} isLoading={updateEmail.isPending}
/> />
} }
</div> </div>
<DefaulModal
open={showModal}
close={() => setShowModal(false)}
isHeader
title_header={t('profile.confrim_email')}
>
<div className='mt-7'>
<div className='text-xs text-center'>
کد تایید به ایمیل {email} ارسال شده است.برای تایید کد مربوطه را وارد کنید.
</div>
<div className='mt-10'>کد تایید</div>
<div className='mt-2 w-full flex justify-center dltr otp'>
<OTPInput
value={code}
onChange={setCode}
shouldAutoFocus
renderInput={(props) =>
<input
{...props}
type='tel'
autoComplete="one-time-code"
inputMode="numeric"
className='w-full h-[50px] flex-1 mx-2 bg-white bg-opacity-30 border rounded-2.5' />
}
/>
</div>
<div className='mt-14 flex justify-end border-t border-border pt-8'>
<div className='flex gap-5'>
<Button
className='bg-white bg-opacity-40 text-description w-[150px] text-xs'
label='لغو'
onClick={() => setShowModal(false)}
/>
<Button
className='w-[150px] text-xs'
>
<div className='flex gap-2 items-center'>
<TickCircle
size={20}
color='white'
/>
<div>{t('wallet.submit_slip')}</div>
</div>
</Button>
</div>
</div>
</div>
</DefaulModal>
</Fragment> </Fragment>
) )
} }
export default Email export default Email
@@ -0,0 +1,69 @@
import { FC, useEffect, useState } from 'react'
import Input from '../../../components/Input'
import Button from '../../../components/Button'
import { Edit } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import { useUpdateProfile } from '../hooks/useProfileData'
import { UpdateProfileType } from '../types/ProfileTypes'
import { toast } from '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
type Props = {
firstName: string | null | undefined
}
const FirstName: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [value, setValue] = useState<string>(props.firstName || '')
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
const updateProfile = useUpdateProfile()
useEffect(() => {
setValue(props.firstName || '')
}, [props.firstName])
const handleSave = () => {
const params: UpdateProfileType = { firstName: value.trim() }
updateProfile.mutate(params, {
onSuccess: () => {
toast(t('success'), 'success')
setIsReadOnly(true)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message?.[0], 'error')
}
})
}
const isDisabled = value.trim().length < 2 || value.trim() === (props.firstName || '')
return (
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input
label={t('profile.first_name')}
value={value}
readOnly={isReadOnly}
onChange={(e) => setValue(e.target.value)}
/>
{
isReadOnly ? (
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>{t('edit')}</div>
</div>
) : (
<Button
label={t('save')}
className='px-4 w-fit'
disabled={isDisabled}
onClick={handleSave}
isLoading={updateProfile.isPending}
/>
)
}
</div>
)
}
export default FirstName
+69
View File
@@ -0,0 +1,69 @@
import { FC, useEffect, useState } from 'react'
import Input from '../../../components/Input'
import Button from '../../../components/Button'
import { Edit } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import { useUpdateProfile } from '../hooks/useProfileData'
import { UpdateProfileType } from '../types/ProfileTypes'
import { toast } from '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
type Props = {
lastName: string | null | undefined
}
const LastName: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [value, setValue] = useState<string>(props.lastName || '')
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
const updateProfile = useUpdateProfile()
useEffect(() => {
setValue(props.lastName || '')
}, [props.lastName])
const handleSave = () => {
const params: UpdateProfileType = { lastName: value.trim() }
updateProfile.mutate(params, {
onSuccess: () => {
toast(t('success'), 'success')
setIsReadOnly(true)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message?.[0], 'error')
}
})
}
const isDisabled = value.trim().length < 2 || value.trim() === (props.lastName || '')
return (
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input
label={t('profile.last_name')}
value={value}
readOnly={isReadOnly}
onChange={(e) => setValue(e.target.value)}
/>
{
isReadOnly ? (
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>{t('edit')}</div>
</div>
) : (
<Button
label={t('save')}
className='px-4 w-fit'
disabled={isDisabled}
onClick={handleSave}
isLoading={updateProfile.isPending}
/>
)
}
</div>
)
}
export default LastName
@@ -0,0 +1,74 @@
import { FC, useEffect, useState } from 'react'
import Input from '../../../components/Input'
import Button from '../../../components/Button'
import { Edit } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import { useUpdateProfile } from '../hooks/useProfileData'
import { UpdateProfileType } from '../types/ProfileTypes'
import { toast } from '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
type Props = {
nationalCode: string | null | undefined
}
const NationalCode: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [value, setValue] = useState<string>(props.nationalCode || '')
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
const updateProfile = useUpdateProfile()
useEffect(() => {
setValue(props.nationalCode || '')
}, [props.nationalCode])
const handleSave = () => {
const params: UpdateProfileType = { nationalCode: value }
updateProfile.mutate(params, {
onSuccess: () => {
toast(t('success'), 'success')
setIsReadOnly(true)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message?.[0], 'error')
}
})
}
const isDisabled = value.length !== 10 || value === (props.nationalCode || '')
return (
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input
label={t('profile.national_code')}
value={value}
readOnly={isReadOnly}
onChange={(e) => {
const next = e.target.value.replace(/\D/g, '').slice(0, 10)
setValue(next)
}}
inputMode='numeric'
maxLength={10}
/>
{
isReadOnly ? (
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>{t('edit')}</div>
</div>
) : (
<Button
label={t('save')}
className='px-4 w-fit'
disabled={isDisabled}
onClick={handleSave}
isLoading={updateProfile.isPending}
/>
)
}
</div>
)
}
export default NationalCode
+14 -2
View File
@@ -1,4 +1,4 @@
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import * as api from '../service/ProfileService' import * as api from '../service/ProfileService'
import { CheckUserNameType, UpdateEmailType, UpdatePhoneType, UpdateProfileType, VerifyPhoneType } from '../types/ProfileTypes'; import { CheckUserNameType, UpdateEmailType, UpdatePhoneType, UpdateProfileType, VerifyPhoneType } from '../types/ProfileTypes';
@@ -16,14 +16,22 @@ export const useCheckUserName = () => {
}; };
export const useUpdateProfile = () => { export const useUpdateProfile = () => {
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (variables: UpdateProfileType) => api.updateProfile(variables), mutationFn: (variables: UpdateProfileType) => api.updateProfile(variables),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["profile"] });
},
}); });
}; };
export const useUpdateEmail = () => { export const useUpdateEmail = () => {
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (variables: UpdateEmailType) => api.updateEmail(variables), mutationFn: (variables: UpdateEmailType) => api.updateEmail(variables),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["profile"] });
},
}); });
}; };
@@ -34,7 +42,11 @@ export const useUpdatePhone = () => {
}; };
export const useVerifyPhone = () => { export const useVerifyPhone = () => {
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (variables: VerifyPhoneType) => api.verifyPhone(variables), mutationFn: (variables: VerifyPhoneType) => api.verifyPhone(variables),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["profile"] });
},
}); });
}; };
+1
View File
@@ -7,6 +7,7 @@ export type UpdateProfileType = {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
birthDate?: string; birthDate?: string;
nationalCode?: string;
userName?: string; userName?: string;
profilePic?: string; profilePic?: string;
cityId?: string; cityId?: string;