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_desc": "تصویر خود را در حساب کاربری قرار دهید.",
"upload_image": "آپلود تصویر",
"format_image": "فرمت های jpg, png,jpeg.حداکثر ۱مگابایت",
"formats": "فرمت های",
"max_size": "حداکثر ۱مگابایت",
"format_image": " jpg, png,jpeg.",
"info_account": "اطلاعات حساب",
"auth_after_change_info": "در صورت تغییر اطلاعات حساب باید مجددا احراز هویت انجام شود.",
"username": "نام کاربری",
@@ -623,7 +625,15 @@
"address_live": "آدرس محل سکونت خود را با دقت وارد کنید",
"province": "استان",
"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": "ایمیل",
"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 AvatarImage from '../../assets/images/avatar_image.png'
import Button from '../../components/Button'
import { DocumentUpload, Edit } from 'iconsax-react'
import { DocumentUpload } from 'iconsax-react'
import Input from '../../components/Input'
import DatePickerComponent from '../../components/DatePicker'
import Select from '../../components/Select'
import Textarea from '../../components/Textarea'
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
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 { 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 (
<div className='mt-4 '>
@@ -18,161 +106,159 @@ const Profile: FC = () => {
{t('profile.account_user')}
</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'>
<div className='flex-1'>
<div>
{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={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' />
{
getProfile.isPending || getProvines.isPending ?
<PageLoading />
:
<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'>
<div className='flex-1'>
<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 className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input
label={t('email')}
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 className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7 border-b pb-7'>
<div className='flex-1'>
<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 className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
<Input
label={t('profile.phone_call')}
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 className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'>
<div className='flex-1'>
<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 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='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='1400-01-01'
/>
<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='xl:mt-7 mt-4'>
<Input
label={t('profile.national_code')}
value={'۰۰۱۲۳۴۵۳۴۴'}
/>
</div>
</div>
</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 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={[
{
label: 'تهران',
value: 'tehran'
}
]}
className='bg-white border'
/>
<Select
label={t('profile.city')}
items={[
{
label: 'تهران',
value: 'tehran'
}
]}
/>
</div>
</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>
)
+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 { useTranslation } from 'react-i18next'
import Tabs from '../../components/Tabs'
import Online from './components/Online'
import CardtoCard from './components/CardtoCard'
import Sheba from './components/Sheba'
import { useGetWalletBalance } from './hooks/useWalletData'
import { NumberFormat } from '../../config/func'
const Wallet: FC = () => {
const { t } = useTranslation('global')
const getWalletBalance = useGetWalletBalance()
const [activeTab, setActiveTab] = useState<string>('online')
return (
@@ -17,6 +17,15 @@ const Wallet: FC = () => {
{t('wallet.increese_wallet')}
</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='flex-1'>
<Tabs
@@ -41,44 +50,56 @@ const Wallet: FC = () => {
onChange={setActiveTab}
/>
{
{/* {
activeTab === 'online' ?
<Online />
: activeTab === 'card' ?
<CardtoCard />
: <Sheba />
}
} */}
</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='text-sm'>
{t('ticket.title_hint')}
<div className='h-8 text-black rounded-xl text-xs gap-1 w-fit px-4 bg-[#EEF0F7] flex items-center'>
{NumberFormat(getWalletBalance.data?.data?.balance)}
<div>تومان</div>
</div>
</div>
<div className='mt-6'>
<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 className='bg-white py-10 px-5 h-fit rounded-3xl mt-8'>
<div className='text-sm'>
لطفا قبل از شارژ کیف پول نکات زیر را مد نظر قرار دهید.
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
<div className='mt-6'>
<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 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 className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
شرایط و مقررات کیف پول داناک در بخش قوانین مقررات درج گردیده، شما با شارژ کیف پول با این قوانین و مقررات موافقت می نمایید.
</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 { ArrowDown2, Element3, HambergerMenu, Wallet } from 'iconsax-react'
import AvatarImage from '../assets/images/Avatar.png'
import { ArrowDown2, CloseCircle, Element3, HambergerMenu, Logout, Wallet } from 'iconsax-react'
import AvatarImage from '../assets/images/avatar_image.png'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { Link, useLocation } from 'react-router-dom'
import { Pages } from '../config/Pages'
import Notifications from '../pages/notification/Notification'
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 location = useLocation();
const [popoverKey, setPopoverKey] = useState(0);
const { t } = useTranslation('global')
const { setOpenSidebar, openSidebar, hasSubMenu } = useSharedStore()
const { setOpenSidebar, openSidebar } = useSharedStore()
const { data } = useGetProfile()
useEffect(() => {
setPopoverKey((prevKey) => prevKey + 1);
}, [location.pathname]);
return (
<div className={clx(
'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='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)]'>
<div className='min-w-[270px] hidden xl:block'>
<Input
@@ -32,22 +37,62 @@ const Header: FC = () => {
</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'>
<Element3 color='black' className='xl:size-[18px] size-[17px]' />
<Link to={Pages.wallet}>
<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>
<Notifications />
<div className='flex gap-2 items-center'>
<div className='size-6 rounded-full bg-description overflow-hidden'>
<Link to={Pages.profile}>
<img src={AvatarImage} className='size-full object-cover' />
</Link>
</div>
<div className='xl:flex hidden gap-1 items-center'>
<div className='text-xs'>مهرداد مظفری</div>
<ArrowDown2 size={14} color='#8C90A3' />
</div>
</div>
{
data && (
<Popover className="relative" key={popoverKey}>
<PopoverButton >
<div className='flex gap-2 items-center mt-2.5'>
<div className='size-6 rounded-full bg-description overflow-hidden'>
<img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
</div>
<div className='xl:flex hidden gap-1 items-center'>
<div className='text-xs'>
{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>
)
+22 -12
View File
@@ -16,11 +16,13 @@ import TicketSubMenu from './components/TicketSubMenu'
import UsersSubMenu from './components/UsersSubMenu'
import LearningSubMenu from './components/LearningSubMenu'
import BlogSubMenu from './components/BlogSubMenu'
import { useGetProfile } from '../pages/profile/hooks/useProfileData'
const SideBar: FC = () => {
const { t } = useTranslation('global')
const { openSidebar, setOpenSidebar, hasSubMenu, setSubMenuName, setSubtMenu, subMenuName } = useSharedStore()
const location = useLocation()
const { data: profile } = useGetProfile()
const isActive = (name: string) => {
return location.pathname.includes(name)
}
@@ -158,12 +160,17 @@ const SideBar: FC = () => {
/>
</div>
<div className={clx(
'mt-10 px-12 text-header text-sm font-normal',
hasSubMenu && 'px-2 text-center'
)}>
{t('sidebar.other')}
</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'
)}>
{t('sidebar.other')}
</div>
}
<div className='text-xs text-[#8C90A3]'>
@@ -186,13 +193,16 @@ const SideBar: FC = () => {
/>
</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]'>
<SideBarItem