sidebar + permission header

This commit is contained in:
hamid zarghami
2025-04-20 15:45:34 +03:30
parent a946d7f6e9
commit 36d56dfe20
17 changed files with 975 additions and 401 deletions
+12 -2
View File
@@ -610,7 +610,9 @@
"image_profile": "تصویر حساب کاربری", "image_profile": "تصویر حساب کاربری",
"image_profile_desc": "تصویر خود را در حساب کاربری قرار دهید.", "image_profile_desc": "تصویر خود را در حساب کاربری قرار دهید.",
"upload_image": "آپلود تصویر", "upload_image": "آپلود تصویر",
"format_image": "فرمت های jpg, png,jpeg.حداکثر ۱مگابایت", "formats": "فرمت های",
"max_size": "حداکثر ۱مگابایت",
"format_image": " jpg, png,jpeg.",
"info_account": "اطلاعات حساب", "info_account": "اطلاعات حساب",
"auth_after_change_info": "در صورت تغییر اطلاعات حساب باید مجددا احراز هویت انجام شود.", "auth_after_change_info": "در صورت تغییر اطلاعات حساب باید مجددا احراز هویت انجام شود.",
"username": "نام کاربری", "username": "نام کاربری",
@@ -623,7 +625,15 @@
"address_live": "آدرس محل سکونت خود را با دقت وارد کنید", "address_live": "آدرس محل سکونت خود را با دقت وارد کنید",
"province": "استان", "province": "استان",
"city": "شهر", "city": "شهر",
"postal_code": "کد پستی" "postal_code": "کد پستی",
"username_available": "نام کاربری موجود",
"username_not_available": "نام کاربری موجود نیست",
"username_save_success": "نام کاربری با موفقیت ذخیره شد",
"image_uploaded_successfully": "تصویر با موفقیت آپلود شد",
"confrim_email": "تایید ایمیل",
"confrim": "تایید صورتحساب ",
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
"email_not_verified": "ایمیل تایید نشده"
}, },
"email": "ایمیل", "email": "ایمیل",
"customer": { "customer": {
+229 -143
View File
@@ -1,16 +1,104 @@
import { FC } from 'react' import { FC, useCallback, useEffect, 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, Edit } from 'iconsax-react' import { DocumentUpload } from 'iconsax-react'
import Input from '../../components/Input' import Input from '../../components/Input'
import DatePickerComponent from '../../components/DatePicker' import DatePickerComponent from '../../components/DatePicker'
import Select from '../../components/Select' import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
import Textarea from '../../components/Textarea' import Username from './components/Username'
import { useDropzone } from 'react-dropzone'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { UpdateProfileType } from './types/ProfileTypes'
import PageLoading from '../../components/PageLoading'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import Email from './components/Email'
import Phone from './components/Phone'
import { useGetProvines } from '../customer/hooks/useCustomerData'
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 getProvines = useGetProvines()
const getProfile = useGetProfile()
const singleUpload = useSingleUpload()
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.success(t('success'))
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
}
})
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[]) => {
if (acceptedFiles.length > 0) {
const file = acceptedFiles[0];
if (file.type.startsWith('image/')) {
setFile(file);
const formData = new FormData()
formData.append('file', file)
singleUpload.mutate(formData, {
onSuccess: (data) => {
const params: UpdateProfileType = {
profilePic: data.data?.url
}
updateProfile.mutate(params, {
onSuccess: () => {
toast.success(t('profile.image_uploaded_successfully'))
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
}
})
} else {
toast.error(t('errors.is_not_image'))
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const { getRootProps, getInputProps } = useDropzone({ onDrop })
return ( return (
<div className='mt-4 '> <div className='mt-4 '>
@@ -18,161 +106,159 @@ const Profile: FC = () => {
{t('profile.account_user')} {t('profile.account_user')}
</div> </div>
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'> {
<div className='xl:mt-10 mt-4 flex xl:flex-row flex-col xl:gap-0 gap-7 xl:items-center border-b pb-7'> getProfile.isPending || getProvines.isPending ?
<div className='flex-1'> <PageLoading />
<div> :
{t('profile.image_profile')} <div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
</div> <div className='xl:mt-10 mt-4 flex xl:flex-row flex-col xl:gap-0 gap-7 xl:items-center border-b pb-7'>
<div className='text-description text-xs mt-2'> <div className='flex-1'>
{t('profile.image_profile_desc')}
</div>
</div>
<div className='flex-1 flex xl:gap-6 gap-4'>
<img src={AvatarImage} className='xl:size-20 size-14 rounded-full object-cover' />
<div>
<Button
className='xl:w-[160px] w-fit px-4 xl:;px-0 h-8 xl:h-10 text-xs xl:text-sm'
>
<div className='flex items-center gap-3'>
<DocumentUpload color='white' size={18} />
<div>{t('profile.upload_image')}</div>
</div>
</Button>
<p className='mt-3 text-description'>
{t('profile.format_image')}
</p>
</div>
</div>
</div>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7 border-b pb-7'>
<div className='flex-1'>
<div>
{t('profile.info_account')}
</div>
<div className='text-description text-xs mt-2'>
{t('profile.auth_after_change_info')}
</div>
</div>
<div className='flex-1 '>
<div className='flex items-end xl:gap-6 gap-3'>
<Input
label={t('profile.username')}
value={'mehrdad'}
readOnly
/>
<div className='flex relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div> <div>
{t('edit')} {t('profile.image_profile')}
</div>
<div className='text-description text-xs mt-2'>
{t('profile.image_profile_desc')}
</div>
</div>
<div className='flex-1 flex xl:gap-6 gap-4'>
<img src={file ? URL.createObjectURL(file) : getProfile.data?.data?.user?.profilePic ? getProfile.data?.data?.user?.profilePic : AvatarImage} className='xl:size-20 size-14 rounded-full object-cover' />
<div>
<Button
isLoading={singleUpload.isPending || updateProfile.isPending}
className='xl:w-[160px] w-fit px-4 xl:;px-0 h-8 xl:h-10 text-xs xl:text-sm'
>
<input {...getInputProps()} />
<div {...getRootProps()} className='flex items-center gap-3'>
<DocumentUpload color='white' size={18} />
<div>{t('profile.upload_image')}</div>
</div>
</Button>
<p className='mt-3 items-center flex gap-0.5 text-description'>
{t('profile.formats')}
<div className='-mt-0.5'>{t('profile.format_image')}</div>
<span>{t('profile.max_size')}</span>
</p>
</div> </div>
</div> </div>
</div> </div>
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input <div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7 border-b pb-7'>
label={t('email')} <div className='flex-1'>
value={'info@example.com'}
readOnly
/>
<div className='flex relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div> <div>
{t('edit')} {t('profile.info_account')}
</div>
<div className='text-description text-xs mt-2'>
{t('profile.auth_after_change_info')}
</div>
</div>
<div className='flex-1 '>
<Username
username={getProfile.data?.data?.user?.userName}
/>
<Email
email={getProfile.data?.data?.user?.email}
isVerified={getProfile.data?.data?.user?.emailVerified}
/>
<Phone
phone={getProfile.data?.data?.user?.phone}
/>
</div>
</div>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7 border-b pb-7'>
<div className='flex-1'>
<div>
{t('profile.personal_information')}
</div>
<div className='text-description text-xs mt-2'>
{t('profile.enter_carefully_your_information')}
</div>
</div>
<div className='flex-1'>
<DatePickerComponent
label={t('profile.date_of_birth')}
onChange={() => { }}
placeholder=''
defaulValue={getProfile.data?.data?.user?.birthDate}
/>
<div className='xl:mt-7 mt-4'>
<Input
label={t('profile.national_code')}
value={getProfile.data?.data?.user?.nationalCode}
/>
</div> </div>
</div> </div>
</div> </div>
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input {/* <div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'>
label={t('profile.phone_call')} <div className='flex-1'>
value={'۰۹۱۲۹۲۸۳۳۹۵'}
readOnly
/>
<div className='flex relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div> <div>
{t('edit')} {t('profile.address')}
</div>
<div className='text-description text-xs mt-2'>
{t('profile.address_live')}
</div> </div>
</div> </div>
</div> <div className='flex-1'>
</div> <div className='rowTwoInput'>
</div> <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='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7 border-b pb-7'> <div className='xl:mt-7 mt-4'>
<div className='flex-1'> <Input
<div> label={t('profile.postal_code')}
{t('profile.personal_information')} {...formik.getFieldProps('postalCode')}
</div> error_text={formik.touched.postalCode && formik.errors.postalCode ? formik.errors.postalCode : ''}
<div className='text-description text-xs mt-2'> value={formik.values.postalCode}
{t('profile.enter_carefully_your_information')} />
</div> </div>
</div> <div className='xl:mt-7 mt-4'>
<div className='flex-1'> <Textarea
<DatePickerComponent label={t('profile.address')}
label={t('profile.date_of_birth')} defaultValue={formik.values.userAddress}
onChange={() => { }} {...formik.getFieldProps('userAddress')}
placeholder='' error_text={formik.touched.userAddress && formik.errors.userAddress ? formik.errors.userAddress : ''}
defaulValue='1400-01-01' />
/> </div>
<div className='xl:mt-7 mt-4'> <div className='flex justify-end mt-5'>
<Input <Button
label={t('profile.national_code')} className='xl:w-fit px-10'
value={'۰۰۱۲۳۴۵۳۴۴'} onClick={() => formik.handleSubmit()}
/> isLoading={updateProfile.isPending}
</div> >
</div> <div className='flex gap-2 items-center'>
</div> <TickCircle size={20} color='white' />
<div>
{t('save')}
</div>
</div>
</Button>
</div>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'> </div>
<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={[
{
label: 'تهران',
value: 'tehran'
}
]}
className='bg-white border'
/>
<Select
label={t('profile.city')}
items={[
{
label: 'تهران',
value: 'tehran'
}
]}
/>
</div>
<div className='xl:mt-7 mt-4'>
<Input
label={t('profile.postal_code')}
value={'۱۲۳۴۵۶۷۸'}
/>
</div>
<div className='xl:mt-7 mt-4'>
<Textarea
label={t('profile.address')}
value={'للورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک استورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است'}
/>
</div>
</div>
</div>
<div className='h-20 xl:hidden'></div> </div> */}
</div>
<div className='h-20 xl:hidden'></div>
</div>
}
</div> </div>
) )
+127
View File
@@ -0,0 +1,127 @@
import { FC, Fragment, useState } from 'react'
import Input from '../../../components/Input'
import { Edit, TickCircle } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import Button from '../../../components/Button'
import { isEmail } from '../../../config/func'
import DefaulModal from '../../../components/DefaulModal'
import OTPInput from 'react-otp-input'
import { useUpdateEmail } from '../hooks/useProfileData'
import { ErrorType } from '../../../helpers/types'
import { toast } from 'react-toastify'
type Props = {
email: string | null,
isVerified: boolean
}
const Email: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [email, setEmail] = useState<string>(props.email ? props.email : '')
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
const [showModal, setShowModal] = useState<boolean>(false)
const [code, setCode] = useState<string>('')
const updateEmail = useUpdateEmail()
const handleShowModal = () => {
updateEmail.mutate({ email: email }, {
onSuccess: () => {
toast.success(t('profile.link_email'))
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
return (
<Fragment>
<div className='flex relative items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input
label={t('email')}
value={email}
readOnly={isReadOnly}
onChange={(e) => setEmail(e.target.value)}
/>
{
!props.isVerified &&
<div className='text-[10px] absolute left-24 text-red-400 top-[34px]'>
{t('profile.email_not_verified')}
</div>
}
{
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={!isEmail(email)}
onClick={handleShowModal}
isLoading={updateEmail.isPending}
/>
}
</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>
)
}
export default Email
+143
View File
@@ -0,0 +1,143 @@
import { FC, Fragment, useState } from 'react'
import Input from '../../../components/Input'
import { Edit, TickCircle } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import Button from '../../../components/Button'
import DefaulModal from '../../../components/DefaulModal'
import OTPInput from 'react-otp-input'
import { useUpdatePhone, useVerifyPhone } from '../hooks/useProfileData'
import { ErrorType } from '../../../helpers/types'
import { toast } from 'react-toastify'
import { VerifyPhoneType } from '../types/ProfileTypes'
type Props = {
phone: string,
}
const Phone: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [phone, setPhone] = useState<string>(props.phone)
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
const [showModal, setShowModal] = useState<boolean>(false)
const [code, setCode] = useState<string>('')
const updatePhone = useUpdatePhone()
const verifyPhone = useVerifyPhone()
const handleShowModal = () => {
updatePhone.mutate({ phone: phone }, {
onSuccess: () => {
setShowModal(true)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
const handleSubmit = () => {
const params: VerifyPhoneType = {
code: code,
phone: phone
}
verifyPhone.mutate(params, {
onSuccess: () => {
setShowModal(false)
setIsReadOnly(true)
toast.success(t('success'))
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
return (
<Fragment>
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input
label={t('profile.phone_call')}
value={phone}
readOnly={isReadOnly}
onChange={(e) => setPhone(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={phone.length !== 11}
onClick={handleShowModal}
isLoading={updatePhone.isPending}
/>
}
</div>
<DefaulModal
open={showModal}
close={() => setShowModal(false)}
isHeader
title_header={t('profile.confrim_email')}
>
<div className='mt-7'>
<div className='text-xs text-center'>
کد تایید به شماره {phone} ارسال شده است.برای تایید کد مربوطه را وارد کنید.
</div>
<div className='mt-10'>کد تایید</div>
<div className='mt-2 w-full flex justify-center dltr otp'>
<OTPInput
value={code}
onChange={setCode}
shouldAutoFocus
numInputs={5}
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'
onClick={handleSubmit}
disabled={code.length !== 5}
>
<div className='flex gap-2 items-center'>
<TickCircle
size={20}
color='white'
/>
<div>{t('save')}</div>
</div>
</Button>
</div>
</div>
</div>
</DefaulModal>
</Fragment>
)
}
export default Phone
+132
View File
@@ -0,0 +1,132 @@
import { FC, useEffect, useState } from 'react'
import Input from '../../../components/Input'
import { Edit } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import Button from '../../../components/Button'
import { useCheckUserName, useUpdateProfile } from '../hooks/useProfileData'
import { CheckUserNameType, UpdateProfileType } from '../types/ProfileTypes'
import { toast } from 'react-toastify'
import { ErrorType } from '../../../helpers/types'
type Props = {
username: string | null,
}
const Username: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [username, setUsername] = useState<string>(props.username ? props.username : '')
const [canSave, setCanSave] = useState<boolean>(false)
const [canEdit, setCanEdit] = useState<boolean>(false)
const [status, setStatus] = useState<'success' | 'error' | ''>('')
const checkUsername = useCheckUserName()
const updateProfile = useUpdateProfile()
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setCanSave(false)
if (value.indexOf(' ') === -1) {
if (value.length <= 3) {
setUsername(value)
return
}
setStatus('')
const params: CheckUserNameType = {
value: e.target.value,
type: 'userName'
}
setUsername(e.target.value)
checkUsername.mutate(params, {
onSuccess() {
setStatus('success')
setCanSave(true)
},
onError() {
setStatus('error')
}
})
}
}
const handleSave = () => {
const params: UpdateProfileType = {
userName: username
}
updateProfile.mutate(params, {
onSuccess: () => {
toast.success(t('profile.username_save_success'))
setCanEdit(false)
setCanSave(false)
setStatus('')
},
onError(error: ErrorType) {
toast.error(error.response?.data?.error?.message?.[0])
},
})
}
useEffect(() => {
if (username !== props.username) {
setUsername(props.username ? props.username : '')
}
}, [props.username])
return (
<>
<div className='flex items-end xl:gap-6 gap-3'>
<div className='relative w-full'>
<Input
label={t('profile.username')}
value={username}
readOnly={!canEdit}
onChange={handleChange}
minLength={3}
maxLength={20}
/>
{
checkUsername.isPending &&
<div className='absolute left-4 top-8'>
<div className='loader'></div>
</div>
}
</div>
<div className='flex relative gap-1 text-[#0047FF] text-xs items-center'>
{
!canEdit ?
<div onClick={() => setCanEdit(true)} className='flex gap-1 cursor-pointer relative -top-3'>
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>
{t('edit')}
</div>
</div>
:
<Button
label={t('save')}
onClick={handleSave}
className='px-4'
disabled={!canSave}
isLoading={updateProfile.isPending}
/>
}
</div>
</div>
{
status === 'success' ?
<div className='text-xs text-green-500 mt-2'>
{t('profile.username_available')}
</div>
:
status === 'error' ?
<div className='text-xs text-red-500 mt-2'>
{t('profile.username_not_available')}
</div>
: null
}
</>
)
}
export default Username
@@ -0,0 +1,40 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import * as api from '../service/ProfileService'
import { CheckUserNameType, UpdateEmailType, UpdatePhoneType, UpdateProfileType, VerifyPhoneType } from '../types/ProfileTypes';
export const useGetProfile = () => {
return useQuery({
queryKey: ["profile"],
queryFn: () => api.getProfile(),
});
};
export const useCheckUserName = () => {
return useMutation({
mutationFn: (variables: CheckUserNameType) => api.checkUserName(variables),
});
};
export const useUpdateProfile = () => {
return useMutation({
mutationFn: (variables: UpdateProfileType) => api.updateProfile(variables),
});
};
export const useUpdateEmail = () => {
return useMutation({
mutationFn: (variables: UpdateEmailType) => api.updateEmail(variables),
});
};
export const useUpdatePhone = () => {
return useMutation({
mutationFn: (variables: UpdatePhoneType) => api.updatePhone(variables),
});
};
export const useVerifyPhone = () => {
return useMutation({
mutationFn: (variables: VerifyPhoneType) => api.verifyPhone(variables),
});
};
@@ -0,0 +1,38 @@
import axios from "../../../config/axios";
import {
CheckUserNameType,
UpdateEmailType,
UpdatePhoneType,
UpdateProfileType,
VerifyPhoneType,
} from "../types/ProfileTypes";
export const getProfile = async () => {
const { data } = await axios.get(`/users/me`);
return data;
};
export const checkUserName = async (params: CheckUserNameType) => {
const { data } = await axios.post(`/users/check-validity`, params);
return data;
};
export const updateProfile = async (params: UpdateProfileType) => {
const { data } = await axios.patch(`/users/update-profile`, params);
return data;
};
export const updateEmail = async (params: UpdateEmailType) => {
const { data } = await axios.patch(`/users/change-email`, params);
return data;
};
export const updatePhone = async (params: UpdatePhoneType) => {
const { data } = await axios.patch(`/users/change-phone`, params);
return data;
};
export const verifyPhone = async (params: VerifyPhoneType) => {
const { data } = await axios.patch(`/users/verify-phone`, params);
return data;
};
+28
View File
@@ -0,0 +1,28 @@
export type CheckUserNameType = {
type: "userName";
value: string;
};
export type UpdateProfileType = {
firstName?: string;
lastName?: string;
birthDate?: string;
userName?: string;
profilePic?: string;
cityId?: string;
postalCode?: string;
userAddress?: string;
};
export type UpdateEmailType = {
email: string;
};
export type UpdatePhoneType = {
phone: string;
};
export type VerifyPhoneType = {
phone: string;
code: string;
};
+49 -28
View File
@@ -2,13 +2,13 @@ import { Cards, Money3, MoneyRecive, TickSquare } from 'iconsax-react'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Tabs from '../../components/Tabs' import Tabs from '../../components/Tabs'
import Online from './components/Online' import { useGetWalletBalance } from './hooks/useWalletData'
import CardtoCard from './components/CardtoCard' import { NumberFormat } from '../../config/func'
import Sheba from './components/Sheba'
const Wallet: FC = () => { const Wallet: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const getWalletBalance = useGetWalletBalance()
const [activeTab, setActiveTab] = useState<string>('online') const [activeTab, setActiveTab] = useState<string>('online')
return ( return (
@@ -17,6 +17,15 @@ const Wallet: FC = () => {
{t('wallet.increese_wallet')} {t('wallet.increese_wallet')}
</div> </div>
<div className='mt-7 xl:hidden flex xl:px-10 px-6 w-full items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-10 h-[70px] justify-between rounded-[32px] bg-white bg-opacity-45'>
<div className='xl:text-base text-sm'>{t('wallet.balance')}</div>
<div className='h-8 text-black rounded-xl text-sm gap-1 w-fit xl:px-14 px-4 bg-[#EEF0F7] flex items-center'>
{NumberFormat(getWalletBalance.data?.data?.balance)}
<div>تومان</div>
</div>
</div>
<div className='mt-8 flex gap-6'> <div className='mt-8 flex gap-6'>
<div className='flex-1'> <div className='flex-1'>
<Tabs <Tabs
@@ -41,44 +50,56 @@ const Wallet: FC = () => {
onChange={setActiveTab} onChange={setActiveTab}
/> />
{ {/* {
activeTab === 'online' ? activeTab === 'online' ?
<Online /> <Online />
: activeTab === 'card' ? : activeTab === 'card' ?
<CardtoCard /> <CardtoCard />
: <Sheba /> : <Sheba />
} } */}
</div> </div>
<div className='w-sidebar xl:block hidden'>
<div className=' xl:flex hidden px-6 w-full items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-3 h-20 justify-between rounded-[32px] bg-white bg-opacity-45'>
<div className='text-xs'>
{t('wallet.balance')}
</div>
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'> <div className='h-8 text-black rounded-xl text-xs gap-1 w-fit px-4 bg-[#EEF0F7] flex items-center'>
<div className='text-sm'> {NumberFormat(getWalletBalance.data?.data?.balance)}
{t('ticket.title_hint')} <div>تومان</div>
</div>
</div> </div>
<div className='mt-6'> <div className='bg-white py-10 px-5 h-fit rounded-3xl mt-8'>
<div className='flex items-start gap-2 border-b pb-5'> <div className='text-sm'>
<div> لطفا قبل از شارژ کیف پول نکات زیر را مد نظر قرار دهید.
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
</div>
</div> </div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div> <div className='mt-6'>
<TickSquare size={20} color='black' variant='Bold' /> <div className='flex items-start gap-2 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
شما می توانید به صورت آنلاین یا کارت به کارت و یا واریز از طریق شماره شبا کیف پول خود را شارژ نمایید.
</div>
</div> </div>
<div className='text-description text-xs leading-5'> <div className='flex items-start gap-2 mt-6 border-b pb-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است. <div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
تراکنش های کیف پول شما از طریق بخش تراکنش قابل دسترسی است.
</div>
</div> </div>
</div> <div className='flex items-start gap-2 mt-6'>
<div className='flex items-start gap-2 mt-6'> <div>
<div> <TickSquare size={20} color='black' variant='Bold' />
<TickSquare size={20} color='black' variant='Bold' /> </div>
</div> <div className='text-description text-xs leading-5'>
<div className='text-description text-xs leading-5'> شرایط و مقررات کیف پول داناک در بخش قوانین مقررات درج گردیده، شما با شارژ کیف پول با این قوانین و مقررات موافقت می نمایید.
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است. </div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,41 +0,0 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../../components/Input'
import UploadBox from '../../../components/UploadBox'
import Button from '../../../components/Button'
const CardtoCard: FC = () => {
const { t } = useTranslation('global')
return (
<div className='mt-8 bg-white p-8 rounded-3xl'>
<div>
{t('wallet.card_to_card')}
</div>
<div className='mt-8'>
<Input
label={t('wallet.amount')}
placeholder={t('wallet.enter_amount_card')}
/>
</div>
<div className='mt-8'>
<UploadBox
label={t('wallet.upload_deposit_slip')}
/>
</div>
<div className='mt-10 flex justify-end'>
<Button
label={t('wallet.submit_slip')}
className='w-[180px]'
/>
</div>
</div>
)
}
export default CardtoCard
-111
View File
@@ -1,111 +0,0 @@
import { FC, useState } from 'react'
import Button from '../../../components/Button'
import { useTranslation } from 'react-i18next'
import { clx } from '../../../helpers/utils'
import SamanImage from '../../../assets/images/saman.png'
import Input from '../../../components/Input'
const Online: FC = () => {
const { t } = useTranslation('global')
const [priceSelect, setPriceSelect] = useState<number>(1)
const [bankSelected, setBankSelected] = useState<number>(1)
return (
<div className='mt-8 bg-white p-8 rounded-3xl'>
<div>
{t('wallet.online_pay')}
</div>
<div className='mt-8 text-sm'>
{t('wallet.select_price')}
</div>
<div className='mt-4 flex gap-4 flex-wrap'>
<div onClick={() => setPriceSelect(1)} className={clx(
'bg-[#EBEDF5] border border-[#EBEDF5] cursor-pointer flex items-center text-xs h-10 px-6 rounded-lg',
priceSelect === 1 && 'border-black'
)}>
۱۰,۰۰۰,۰۰۰ تومان
</div>
<div onClick={() => setPriceSelect(2)} className={clx(
'bg-[#EBEDF5] border border-[#EBEDF5] cursor-pointer flex items-center text-xs h-10 px-6 rounded-lg',
priceSelect === 2 && 'border-black'
)}>
۱۰,۰۰۰,۰۰۰ تومان
</div>
<div onClick={() => setPriceSelect(3)} className={clx(
'bg-[#EBEDF5] border border-[#EBEDF5] cursor-pointer flex items-center text-xs h-10 px-6 rounded-lg',
priceSelect === 3 && 'border-black'
)}>
۱۰,۰۰۰,۰۰۰ تومان
</div>
<div onClick={() => setPriceSelect(4)} className={clx(
'bg-[#EBEDF5] border border-[#EBEDF5] cursor-pointer flex items-center text-xs h-10 px-6 rounded-lg',
priceSelect === 4 && 'border-black'
)}>
۱۰,۰۰۰,۰۰۰ تومان
</div>
<div onClick={() => setPriceSelect(5)} className={clx(
'bg-[#EBEDF5] border border-[#EBEDF5] cursor-pointer flex items-center text-xs h-10 px-6 rounded-lg',
priceSelect === 5 && 'border-black'
)}>
۱۰,۰۰۰,۰۰۰ تومان
</div>
</div>
<div className='mt-9'>
<Input
label={t('wallet.optional_price')}
placeholder={t('wallet.enter_your_price')}
/>
</div>
<div className='mt-8 text-sm'>
<div>
{t('wallet.select_payment')}
</div>
<div className='mt-6 flex gap-4'>
<div onClick={() => setBankSelected(1)} className={clx(
'xl:h-16 rounded-xl cursor-pointer flex xl:flex-row flex-col gap-4 items-center text-center py-3 xl:py-0 xl:text-right px-4 border border-border xl:w-[240px]',
bankSelected === 1 && 'border-black'
)}>
<img src={SamanImage} className='w-10' />
<div>
<div>
بانک پارسیان
</div>
<div className='text-xs mt-1 text-description'>
تمام کارت های عضو شتاب
</div>
</div>
</div>
<div onClick={() => setBankSelected(2)} className={clx(
'xl:h-16 rounded-xl cursor-pointer flex xl:flex-row flex-col gap-4 items-center text-center py-3 xl:py-0 xl:text-right px-4 border border-border xl:w-[240px]',
bankSelected === 2 && 'border-black'
)}>
<img src={SamanImage} className='w-10' />
<div>
<div>
بانک پارسیان
</div>
<div className='text-xs mt-1 text-description'>
تمام کارت های عضو شتاب
</div>
</div>
</div>
</div>
<div className='mt-10 flex justify-end'>
<Button
label={t('wallet.pay')}
className='w-[180px]'
/>
</div>
</div>
</div>
)
}
export default Online
-41
View File
@@ -1,41 +0,0 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../../components/Input'
import UploadBox from '../../../components/UploadBox'
import Button from '../../../components/Button'
const Sheba: FC = () => {
const { t } = useTranslation('global')
return (
<div className='mt-8 bg-white p-8 rounded-3xl'>
<div>
{t('wallet.pay_sheba')}
</div>
<div className='mt-8'>
<Input
label={t('wallet.amount')}
placeholder={t('wallet.enter_sheba_deposit')}
/>
</div>
<div className='mt-8'>
<UploadBox
label={t('wallet.upload_deposit_slip')}
/>
</div>
<div className='mt-10 flex justify-end'>
<Button
label={t('wallet.submit_slip')}
className='w-[180px]'
/>
</div>
</div>
)
}
export default Sheba
+37
View File
@@ -0,0 +1,37 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/WalletService";
import { DepositTransferType, PaymentType } from "../types/WalletTypes";
export const useGetGetWays = () => {
return useQuery({
queryKey: ["getGetWays"],
queryFn: api.getGetWays,
});
};
export const useGetWalletBalance = () => {
return useQuery({
queryKey: ["wallet-balance"],
queryFn: api.getWalletBalance,
});
};
export const usePayment = () => {
return useMutation({
mutationFn: (variables: PaymentType) => api.payment(variables),
});
};
export const useGetBankAccount = () => {
return useQuery({
queryKey: ["bankAccount"],
queryFn: api.getBankAccount,
});
};
export const useDepositTransfer = () => {
return useMutation({
mutationFn: (variables: DepositTransferType) =>
api.depositTransfer(variables),
});
};
+27
View File
@@ -0,0 +1,27 @@
import axios from "../../../config/axios";
import { DepositTransferType, PaymentType } from "../types/WalletTypes";
export const getGetWays = async () => {
const { data } = await axios.get(`/payments/gateways`);
return data;
};
export const getWalletBalance = async () => {
const { data } = await axios.get(`/wallets/balance`);
return data;
};
export const payment = async (params: PaymentType) => {
const { data } = await axios.post(`/payments/deposit/gateway`, params);
return data;
};
export const getBankAccount = async () => {
const { data } = await axios.get(`/payments/bank-account`);
return data;
};
export const depositTransfer = async (params: DepositTransferType) => {
const { data } = await axios.post(`/payments/deposit/transfer`, params);
return data;
};
+23
View File
@@ -0,0 +1,23 @@
export type PaymentType = {
amount: number;
gatewayId: string;
};
export type GatewayItemType = {
id: string;
name: string;
description: string;
logoUrl: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
nameFa: string;
};
export type MethosTransferType = "CARD_TO_CARD" | "SHEBA";
export type DepositTransferType = {
amount: number;
transferReceiptUrl: string;
bankAccountId: string;
method: MethosTransferType;
};
+68 -23
View File
@@ -1,25 +1,30 @@
import { FC } from 'react' import { FC, useEffect, useState } from 'react'
import Input from '../components/Input' import Input from '../components/Input'
import { ArrowDown2, Element3, HambergerMenu, Wallet } from 'iconsax-react' import { ArrowDown2, CloseCircle, Element3, HambergerMenu, Logout, Wallet } from 'iconsax-react'
import AvatarImage from '../assets/images/Avatar.png' import AvatarImage from '../assets/images/avatar_image.png'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { Pages } from '../config/Pages' import { Pages } from '../config/Pages'
import Notifications from '../pages/notification/Notification' import Notifications from '../pages/notification/Notification'
import { useSharedStore } from './store/sharedStore' import { useSharedStore } from './store/sharedStore'
import { clx } from '../helpers/utils' import { useGetProfile } from '../pages/profile/hooks/useProfileData'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import SideBarItem from './SideBarItem'
const Header: FC = () => { const Header: FC = () => {
const location = useLocation();
const [popoverKey, setPopoverKey] = useState(0);
const { t } = useTranslation('global') const { t } = useTranslation('global')
const { setOpenSidebar, openSidebar, hasSubMenu } = useSharedStore() const { setOpenSidebar, openSidebar } = useSharedStore()
const { data } = useGetProfile()
useEffect(() => {
setPopoverKey((prevKey) => prevKey + 1);
}, [location.pathname]);
return ( return (
<div className={clx( <div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
'fixed z-10 right-4 left-4 xl:right-[291px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-308px)]',
hasSubMenu && 'xl:right-[318px] xl:w-[calc(100%-344px)]',
)}>
<div className='min-w-[270px] hidden xl:block'> <div className='min-w-[270px] hidden xl:block'>
<Input <Input
@@ -32,22 +37,62 @@ const Header: FC = () => {
</div> </div>
{/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */} {/* <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'>
<Element3 color='black' className='xl:size-[18px] size-[17px]' /> <Link to={Pages.services.other}>
<Link to={Pages.wallet}> <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' /> <Wallet className='xl:size-[18px] size-[17px]' color='black' />
</Link> </Link>
<Notifications /> <Notifications />
<div className='flex gap-2 items-center'> {
<div className='size-6 rounded-full bg-description overflow-hidden'> data && (
<Link to={Pages.profile}> <Popover className="relative" key={popoverKey}>
<img src={AvatarImage} className='size-full object-cover' /> <PopoverButton >
</Link> <div className='flex gap-2 items-center mt-2.5'>
</div> <div className='size-6 rounded-full bg-description overflow-hidden'>
<div className='xl:flex hidden gap-1 items-center'> <img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
<div className='text-xs'>مهرداد مظفری</div> </div>
<ArrowDown2 size={14} color='#8C90A3' /> <div className='xl:flex hidden gap-1 items-center'>
</div> <div className='text-xs'>
</div> {data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName}
</div>
<ArrowDown2 size={14} color='#8C90A3' />
</div>
</div>
</PopoverButton>
<PopoverPanel style={{ minHeight: window.innerWidth < 1140 ? window.innerHeight : undefined }} anchor="bottom" className="flex xl:ml-6 overflow-auto flex-col gap-3 bg-white boxShadow xl:mt-7 z-30 py-4 text-xs rounded-2.5 xl:w-[300px] -mt-[50px] w-full fixed xl:h-fit h-full shadow-md">
<div className='absolute xl:hidden top-6 left-6'>
<CloseCircle onClick={() => setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' />
</div>
<Link to={Pages.profile} className='flex flex-col gap-2 items-center justify-center border-b border-[#EAEDF5] pb-4'>
<div className='size-14 rounded-full overflow-hidden'>
<img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
</div>
<div>
{data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName}
</div>
<div className='text-description'>
{data?.data?.user?.email}
</div>
</Link>
<div className='px-6 -mt-[2px]'>
<SideBarItem
icon={<Logout color={'black'} size={20} />}
title={t('sidebar.logout')}
isActive
link={Pages.setting}
isLogout
/>
</div>
</PopoverPanel>
</Popover>
)
}
</div> </div>
</div> </div>
) )
+22 -12
View File
@@ -16,11 +16,13 @@ import TicketSubMenu from './components/TicketSubMenu'
import UsersSubMenu from './components/UsersSubMenu' import UsersSubMenu from './components/UsersSubMenu'
import LearningSubMenu from './components/LearningSubMenu' import LearningSubMenu from './components/LearningSubMenu'
import BlogSubMenu from './components/BlogSubMenu' import BlogSubMenu from './components/BlogSubMenu'
import { useGetProfile } from '../pages/profile/hooks/useProfileData'
const SideBar: FC = () => { const SideBar: FC = () => {
const { t } = useTranslation('global') const { t } = useTranslation('global')
const { openSidebar, setOpenSidebar, hasSubMenu, setSubMenuName, setSubtMenu, subMenuName } = useSharedStore() const { openSidebar, setOpenSidebar, hasSubMenu, setSubMenuName, setSubtMenu, subMenuName } = useSharedStore()
const location = useLocation() const location = useLocation()
const { data: profile } = useGetProfile()
const isActive = (name: string) => { const isActive = (name: string) => {
return location.pathname.includes(name) return location.pathname.includes(name)
} }
@@ -158,12 +160,17 @@ const SideBar: FC = () => {
/> />
</div> </div>
<div className={clx( {
'mt-10 px-12 text-header text-sm font-normal', profile?.data?.user?.roles.some((role: { name: string }) => role.name === 'super_admin') &&
hasSubMenu && 'px-2 text-center' <div className={clx(
)}> 'mt-10 px-12 text-header text-sm font-normal',
{t('sidebar.other')} hasSubMenu && 'px-2 text-center'
</div> )}>
{t('sidebar.other')}
</div>
}
<div className='text-xs text-[#8C90A3]'> <div className='text-xs text-[#8C90A3]'>
@@ -186,13 +193,16 @@ const SideBar: FC = () => {
/> />
</div> </div>
{
profile?.data?.user?.roles?.some((role: { name: string }) => role.name === 'super_admin') &&
<div className={clx(
'mt-10 px-12 text-header text-sm font-normal',
hasSubMenu && 'px-2 text-center text-xs'
)}>
{t('sidebar.mnage_content')}
</div>
}
<div className={clx(
'mt-10 px-12 text-header text-sm font-normal',
hasSubMenu && 'px-2 text-center text-xs'
)}>
{t('sidebar.mnage_content')}
</div>
<div className='text-xs text-[#8C90A3]'> <div className='text-xs text-[#8C90A3]'>
<SideBarItem <SideBarItem