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": {
+147 -61
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,6 +106,10 @@ const Profile: FC = () => {
{t('profile.account_user')} {t('profile.account_user')}
</div> </div>
{
getProfile.isPending || getProvines.isPending ?
<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'>
<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='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 className='flex-1'>
@@ -29,18 +121,22 @@ const Profile: FC = () => {
</div> </div>
</div> </div>
<div className='flex-1 flex xl:gap-6 gap-4'> <div className='flex-1 flex xl:gap-6 gap-4'>
<img src={AvatarImage} className='xl:size-20 size-14 rounded-full object-cover' /> <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> <div>
<Button <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' 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'> <input {...getInputProps()} />
<div {...getRootProps()} className='flex items-center gap-3'>
<DocumentUpload color='white' size={18} /> <DocumentUpload color='white' size={18} />
<div>{t('profile.upload_image')}</div> <div>{t('profile.upload_image')}</div>
</div> </div>
</Button> </Button>
<p className='mt-3 text-description'> <p className='mt-3 items-center flex gap-0.5 text-description'>
{t('profile.format_image')} {t('profile.formats')}
<div className='-mt-0.5'>{t('profile.format_image')}</div>
<span>{t('profile.max_size')}</span>
</p> </p>
</div> </div>
</div> </div>
@@ -56,45 +152,16 @@ const Profile: FC = () => {
</div> </div>
</div> </div>
<div className='flex-1 '> <div className='flex-1 '>
<div className='flex items-end xl:gap-6 gap-3'> <Username
<Input username={getProfile.data?.data?.user?.userName}
label={t('profile.username')}
value={'mehrdad'}
readOnly
/> />
<div className='flex relative -top-3 gap-1 text-[#0047FF] text-xs items-center'> <Email
<Edit className='xl:size-[18px] size-4' color='#0047FF' /> email={getProfile.data?.data?.user?.email}
<div> isVerified={getProfile.data?.data?.user?.emailVerified}
{t('edit')}
</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'> <Phone
<Edit className='xl:size-[18px] size-4' color='#0047FF' /> phone={getProfile.data?.data?.user?.phone}
<div>
{t('edit')}
</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>
{t('edit')}
</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -112,19 +179,19 @@ const Profile: FC = () => {
label={t('profile.date_of_birth')} label={t('profile.date_of_birth')}
onChange={() => { }} onChange={() => { }}
placeholder='' placeholder=''
defaulValue='1400-01-01' defaulValue={getProfile.data?.data?.user?.birthDate}
/> />
<div className='xl:mt-7 mt-4'> <div className='xl:mt-7 mt-4'>
<Input <Input
label={t('profile.national_code')} label={t('profile.national_code')}
value={'۰۰۱۲۳۴۵۳۴۴'} value={getProfile.data?.data?.user?.nationalCode}
/> />
</div> </div>
</div> </div>
</div> </div>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'> {/* <div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'>
<div className='flex-1'> <div className='flex-1'>
<div> <div>
{t('profile.address')} {t('profile.address')}
@@ -137,42 +204,61 @@ const Profile: FC = () => {
<div className='rowTwoInput'> <div className='rowTwoInput'>
<Select <Select
label={t('profile.province')} label={t('profile.province')}
items={[ items={getProvines.data?.data?.provinces.map((item: ProvinesItemType) => ({ label: item.name, value: item.id }))}
{ onChange={(e) => setProvinesId(e.target.value)}
label: 'تهران',
value: 'tehran'
}
]}
className='bg-white border' className='bg-white border'
placeholder={t('select')}
value={provinesId}
/> />
<Select <Select
label={t('profile.city')} label={t('profile.city')}
items={[ items={getCities.data?.data?.cities ? getCities.data?.data?.cities.map((item: ProvinesItemType) => ({ label: item.name, value: item.id })) : []}
{ placeholder={t('select')}
label: 'تهران', {...formik.getFieldProps('cityId')}
value: 'tehran' error_text={formik.touched.cityId && formik.errors.cityId ? formik.errors.cityId : ''}
}
]}
/> />
</div> </div>
<div className='xl:mt-7 mt-4'> <div className='xl:mt-7 mt-4'>
<Input <Input
label={t('profile.postal_code')} label={t('profile.postal_code')}
value={'۱۲۳۴۵۶۷۸'} {...formik.getFieldProps('postalCode')}
error_text={formik.touched.postalCode && formik.errors.postalCode ? formik.errors.postalCode : ''}
value={formik.values.postalCode}
/> />
</div> </div>
<div className='xl:mt-7 mt-4'> <div className='xl:mt-7 mt-4'>
<Textarea <Textarea
label={t('profile.address')} label={t('profile.address')}
value={'للورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک استورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است'} defaultValue={formik.values.userAddress}
{...formik.getFieldProps('userAddress')}
error_text={formik.touched.userAddress && formik.errors.userAddress ? formik.errors.userAddress : ''}
/> />
</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>
</div> </div>
</Button>
</div>
</div>
</div> */}
<div className='h-20 xl:hidden'></div> <div className='h-20 xl:hidden'></div>
</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;
};
+31 -10
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,19 +50,30 @@ 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'>
{NumberFormat(getWalletBalance.data?.data?.balance)}
<div>تومان</div>
</div>
</div>
<div className='bg-white py-10 px-5 h-fit rounded-3xl mt-8'>
<div className='text-sm'> <div className='text-sm'>
{t('ticket.title_hint')} لطفا قبل از شارژ کیف پول نکات زیر را مد نظر قرار دهید.
</div> </div>
<div className='mt-6'> <div className='mt-6'>
@@ -62,7 +82,7 @@ const Wallet: FC = () => {
<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'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید. شما می توانید به صورت آنلاین یا کارت به کارت و یا واریز از طریق شماره شبا کیف پول خود را شارژ نمایید.
</div> </div>
</div> </div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'> <div className='flex items-start gap-2 mt-6 border-b pb-5'>
@@ -70,7 +90,7 @@ const Wallet: FC = () => {
<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'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است. تراکنش های کیف پول شما از طریق بخش تراکنش قابل دسترسی است.
</div> </div>
</div> </div>
<div className='flex items-start gap-2 mt-6'> <div className='flex items-start gap-2 mt-6'>
@@ -78,7 +98,8 @@ const Wallet: FC = () => {
<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;
};
+61 -16
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'>
<Link to={Pages.services.other}>
<Element3 color='black' className='xl:size-[18px] size-[17px]' /> <Element3 color='black' className='xl:size-[18px] size-[17px]' />
<Link to={Pages.wallet}> </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'> {
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'> <div className='size-6 rounded-full bg-description overflow-hidden'>
<Link to={Pages.profile}> <img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
<img src={AvatarImage} className='size-full object-cover' />
</Link>
</div> </div>
<div className='xl:flex hidden gap-1 items-center'> <div className='xl:flex hidden gap-1 items-center'>
<div className='text-xs'>مهرداد مظفری</div> <div className='text-xs'>
{data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName}
</div>
<ArrowDown2 size={14} color='#8C90A3' /> <ArrowDown2 size={14} color='#8C90A3' />
</div> </div>
</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>
) )
+11 -1
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>
{
profile?.data?.user?.roles.some((role: { name: string }) => role.name === 'super_admin') &&
<div className={clx( <div className={clx(
'mt-10 px-12 text-header text-sm font-normal', 'mt-10 px-12 text-header text-sm font-normal',
hasSubMenu && 'px-2 text-center' hasSubMenu && 'px-2 text-center'
)}> )}>
{t('sidebar.other')} {t('sidebar.other')}
</div> </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( <div className={clx(
'mt-10 px-12 text-header text-sm font-normal', 'mt-10 px-12 text-header text-sm font-normal',
hasSubMenu && 'px-2 text-center text-xs' hasSubMenu && 'px-2 text-center text-xs'
)}> )}>
{t('sidebar.mnage_content')} {t('sidebar.mnage_content')}
</div> </div>
}
<div className='text-xs text-[#8C90A3]'> <div className='text-xs text-[#8C90A3]'>
<SideBarItem <SideBarItem