profile + summerize
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
import { FC, Fragment, ReactNode, useEffect } from 'react'
|
||||||
|
import HeaderModal from './HeaderModal'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean,
|
||||||
|
close: () => void,
|
||||||
|
children: ReactNode,
|
||||||
|
isHeader?: boolean,
|
||||||
|
title_header?: string,
|
||||||
|
width?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaulModal: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.open) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'auto'
|
||||||
|
}
|
||||||
|
}, [props.open])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{
|
||||||
|
props.open && (
|
||||||
|
<Fragment>
|
||||||
|
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-y-auto fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
||||||
|
<div className='relative xl:h-full max-h-[80%] bottom-0 left-0 flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
||||||
|
<div className='border-0 h-auto p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
props.isHeader && props.title_header &&
|
||||||
|
<div onClick={props.close} className='pb-6 border-b border-white border-opacity-20'>
|
||||||
|
<div className='h-[5px] w-[200px] mx-auto bg-[#D1D3D7] rounded-full mb-4 xl:hidden'></div>
|
||||||
|
<HeaderModal close={props.close} label={props.title_header} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{props.children}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={props.close} className='fixed size-full bg-black/3 top-0 bottom-0 right-0 inset-0 z-50 '></div>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DefaulModal
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string,
|
||||||
|
close: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeaderModal: FC<Props> = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div className='text-sm'>{props.label}</div>
|
||||||
|
<div className='size-7 rounded-full bg-white bg-opacity-35 flex justify-center items-center'>
|
||||||
|
<CloseCircle size={16} color='white' onClick={props.close} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderModal
|
||||||
@@ -25,6 +25,7 @@ const Table = <T extends RowDataType>({
|
|||||||
selectedRows: externalSelectedRows,
|
selectedRows: externalSelectedRows,
|
||||||
clearSelection = false,
|
clearSelection = false,
|
||||||
rowActions,
|
rowActions,
|
||||||
|
inlineActions,
|
||||||
pagination,
|
pagination,
|
||||||
}: TableProps<T>): React.ReactElement => {
|
}: TableProps<T>): React.ReactElement => {
|
||||||
|
|
||||||
@@ -145,9 +146,30 @@ const Table = <T extends RowDataType>({
|
|||||||
checked={isRowSelected(item)}
|
checked={isRowSelected(item)}
|
||||||
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* {inlineActions && (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 flex items-center gap-2 relative z-[5]"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{inlineActions(item).map((action, actionIndex) => (
|
||||||
|
<div
|
||||||
|
key={actionIndex}
|
||||||
|
className={`cursor-pointer ${action.className || ''}`}
|
||||||
|
onClick={action.onClick}
|
||||||
|
title={action.tooltip}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Inline Actions - بعد از checkbox */}
|
||||||
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{/* محتوای اصلی - ستون اول */}
|
{/* محتوای اصلی - ستون اول */}
|
||||||
<div className={`flex items-center mb-0.5 text-sm ${fontWeight}`}>
|
<div className={`flex items-center mb-0.5 text-sm ${fontWeight}`}>
|
||||||
@@ -185,7 +207,7 @@ const Table = <T extends RowDataType>({
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (inlineActions ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -193,7 +215,7 @@ const Table = <T extends RowDataType>({
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 pb-6 md:pb-8 text-center text-gray-500">
|
<td colSpan={columns.length + (selectable ? 1 : 0) + (inlineActions ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 pb-6 md:pb-8 text-center text-gray-500">
|
||||||
{noDataMessage}
|
{noDataMessage}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -217,12 +239,32 @@ const Table = <T extends RowDataType>({
|
|||||||
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
|
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<div className='flex items-center gap-3'>
|
||||||
checked={isRowSelected(item)}
|
<Checkbox
|
||||||
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
checked={isRowSelected(item)}
|
||||||
/>
|
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
inlineActions && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{inlineActions(item).map((action, actionIndex) => (
|
||||||
|
<div
|
||||||
|
key={actionIndex}
|
||||||
|
className={`cursor-pointer ${action.className || ''}`}
|
||||||
|
onClick={action.onClick}
|
||||||
|
title={action.tooltip}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<td
|
<td
|
||||||
key={`${item.id}-${col.key}`}
|
key={`${item.id}-${col.key}`}
|
||||||
@@ -262,6 +304,9 @@ const Table = <T extends RowDataType>({
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
|
{inlineActions && (
|
||||||
|
<th className="px-3 md:px-6 py-3 md:py-4 text-center text-sm font-medium">عملیات</th>
|
||||||
|
)}
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<Td key={col.key} text={col.title} />
|
<Td key={col.key} text={col.title} />
|
||||||
))}
|
))}
|
||||||
@@ -343,7 +388,7 @@ const Table = <T extends RowDataType>({
|
|||||||
{actionsPosition === 'header-replace' && (
|
{actionsPosition === 'header-replace' && (
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={columns.length} className="p-0">
|
<th colSpan={columns.length + (selectable ? 1 : 0) + (inlineActions ? 1 : 0) + (rowActions ? 1 : 0)} className="p-0">
|
||||||
{renderActions()}
|
{renderActions()}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -405,4 +450,46 @@ const MyComponent = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
مثال استفاده با inline actions:
|
||||||
|
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { Star1, Archive } from 'iconsax-react';
|
||||||
|
|
||||||
|
const MyEmailComponent = () => {
|
||||||
|
const columns = [
|
||||||
|
{ title: 'موضوع', key: 'subject' },
|
||||||
|
{ title: 'فرستنده', key: 'from' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ id: 1, subject: 'سلام', from: 'ali@example.com', flagged: false },
|
||||||
|
{ id: 2, subject: 'خبرنامه', from: 'newsletter@example.com', flagged: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getInlineActions = (item) => [
|
||||||
|
{
|
||||||
|
icon: <Star1
|
||||||
|
variant={item.flagged ? 'Bold' : 'Outline'}
|
||||||
|
size={18}
|
||||||
|
color={item.flagged ? '#FFC107' : '#8C90A3'}
|
||||||
|
/>,
|
||||||
|
onClick: () => handleFavoriteToggle(item.id),
|
||||||
|
tooltip: item.flagged ? 'حذف از علاقهمندیها' : 'اضافه به علاقهمندیها'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Archive size={18} color="#8C90A3" />,
|
||||||
|
onClick: () => handleArchive(item.id),
|
||||||
|
tooltip: 'بایگانی'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
inlineActions={getInlineActions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +19,13 @@ export interface PaginationConfig {
|
|||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InlineActionItem {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
onClick: () => void;
|
||||||
|
tooltip?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TableProps<T extends RowDataType = RowDataType> {
|
export interface TableProps<T extends RowDataType = RowDataType> {
|
||||||
columns: ColumnType<T>[];
|
columns: ColumnType<T>[];
|
||||||
data: T[];
|
data: T[];
|
||||||
@@ -39,5 +46,6 @@ export interface TableProps<T extends RowDataType = RowDataType> {
|
|||||||
selectedRows?: T[];
|
selectedRows?: T[];
|
||||||
clearSelection?: boolean;
|
clearSelection?: boolean;
|
||||||
rowActions?: (item: T) => RowActionItem[];
|
rowActions?: (item: T) => RowActionItem[];
|
||||||
|
inlineActions?: (item: T) => InlineActionItem[];
|
||||||
pagination?: PaginationConfig;
|
pagination?: PaginationConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,6 +342,20 @@ textarea::placeholder {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modalGlass {
|
||||||
|
background: rgba(0, 0, 0, 0.436);
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
|
||||||
|
backdrop-filter: blur(7px);
|
||||||
|
}
|
||||||
|
.modalGlass2 {
|
||||||
|
/* background: rgba(255, 255, 255, 0.502); */
|
||||||
|
background-color: white;
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
|
||||||
|
/* backdrop-filter: blur(5px); */
|
||||||
|
}
|
||||||
|
|
||||||
.modalGlass3 {
|
.modalGlass3 {
|
||||||
background: rgba(62, 61, 61, 0.2);
|
background: rgba(62, 61, 61, 0.2);
|
||||||
background-blend-mode: multiply;
|
background-blend-mode: multiply;
|
||||||
|
|||||||
@@ -27,6 +27,37 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"close": "بستن"
|
"close": "بستن"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"account_user": "حساب کاربری",
|
||||||
|
"image_profile": "تصویر حساب کاربری",
|
||||||
|
"image_profile_desc": "تصویر خود را در حساب کاربری قرار دهید.",
|
||||||
|
"upload_image": "آپلود تصویر",
|
||||||
|
"formats": "فرمت های",
|
||||||
|
"max_size": "حداکثر ۱مگابایت",
|
||||||
|
"format_image": " jpg, png,jpeg.",
|
||||||
|
"info_account": "اطلاعات حساب",
|
||||||
|
"auth_after_change_info": "در صورت تغییر اطلاعات حساب باید مجددا احراز هویت انجام شود.",
|
||||||
|
"username": "نام کاربری",
|
||||||
|
"phone_call": "شماره تماس",
|
||||||
|
"personal_information": "اطلاعات شخصی",
|
||||||
|
"enter_carefully_your_information": "اطلاعت شخصی خود را با دقت وارد کنید.",
|
||||||
|
"date_of_birth": "تاریخ تولد",
|
||||||
|
"national_code": "کد ملی",
|
||||||
|
"address": "آدرس",
|
||||||
|
"address_live": "آدرس محل سکونت خود را با دقت وارد کنید",
|
||||||
|
"province": "استان",
|
||||||
|
"city": "شهر",
|
||||||
|
"postal_code": "کد پستی",
|
||||||
|
"username_available": "نام کاربری موجود",
|
||||||
|
"username_not_available": "نام کاربری موجود نیست",
|
||||||
|
"username_save_success": "نام کاربری با موفقیت ذخیره شد",
|
||||||
|
"image_uploaded_successfully": "تصویر با موفقیت آپلود شد",
|
||||||
|
"confrim_email": "تایید ایمیل",
|
||||||
|
"confrim": "تایید صورتحساب ",
|
||||||
|
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
|
||||||
|
"email_not_verified": "ایمیل تایید نشده"
|
||||||
|
},
|
||||||
|
"email": "ایمیل",
|
||||||
"received": {
|
"received": {
|
||||||
"title": "دریافتی ها",
|
"title": "دریافتی ها",
|
||||||
"from_date": "از تاریخ",
|
"from_date": "از تاریخ",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useGetFavoriteMessages } from './hooks/useFavoriteData';
|
|||||||
import { FavoriteMessage } from './types/FavoriteTypes';
|
import { FavoriteMessage } from './types/FavoriteTypes';
|
||||||
import { formatDate } from '@/config/func';
|
import { formatDate } from '@/config/func';
|
||||||
import { useEmailActions } from '@/hooks/useEmailActions';
|
import { useEmailActions } from '@/hooks/useEmailActions';
|
||||||
|
import Favorite from '../received/Components/Favorite';
|
||||||
|
|
||||||
const List: FC = () => {
|
const List: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -99,6 +100,13 @@ const List: FC = () => {
|
|||||||
setSelectedMessages([]);
|
setSelectedMessages([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInlineActions = (message: FavoriteMessage) => [
|
||||||
|
{
|
||||||
|
icon: <Favorite flagged={message.flagged} id={message.id.toString()} mailbox={message.mailbox} />,
|
||||||
|
onClick: () => { },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const getRowActions = (message: FavoriteMessage): RowActionItem[] => [
|
const getRowActions = (message: FavoriteMessage): RowActionItem[] => [
|
||||||
{
|
{
|
||||||
label: 'حذف از علاقهمندیها',
|
label: 'حذف از علاقهمندیها',
|
||||||
@@ -161,6 +169,7 @@ const List: FC = () => {
|
|||||||
currentPage: pager.page,
|
currentPage: pager.page,
|
||||||
onPageChange: handlePageChange
|
onPageChange: handlePageChange
|
||||||
} : undefined}
|
} : undefined}
|
||||||
|
inlineActions={getInlineActions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
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 } from 'iconsax-react'
|
||||||
|
import { useGetProfile, useSingleUpload, useUpdateProfile } from './hooks/useProfileData'
|
||||||
|
import Username from './components/Username'
|
||||||
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
import { ErrorType } from '@/helpers/types'
|
||||||
|
import { UpdateProfileType } from './types/ProfileTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import Email from './components/Email'
|
||||||
|
|
||||||
|
const Profile: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [file, setFile] = useState<File>()
|
||||||
|
const getProfile = useGetProfile()
|
||||||
|
const updateProfile = useUpdateProfile()
|
||||||
|
const singleUpload = useSingleUpload()
|
||||||
|
|
||||||
|
const formik = useFormik<UpdateProfileType>({
|
||||||
|
initialValues: {
|
||||||
|
cityId: '',
|
||||||
|
userAddress: '',
|
||||||
|
postalCode: '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
cityId: Yup.string().required(t('errors.required')),
|
||||||
|
userAddress: Yup.string().required(t('errors.required')),
|
||||||
|
postalCode: Yup.string().required(t('errors.required')),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
updateProfile.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('success'), 'success')
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (getProfile.data) {
|
||||||
|
formik.setValues({
|
||||||
|
cityId: getProfile.data.data.user.city?.id || '',
|
||||||
|
userAddress: getProfile.data.data.user.userAddress || '',
|
||||||
|
postalCode: getProfile.data.data.user.postalCode || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [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(t('profile.image_uploaded_successfully'), 'success')
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast(t('errors.is_not_image'), 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4 '>
|
||||||
|
<div>
|
||||||
|
{t('profile.account_user')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
getProfile.isPending ?
|
||||||
|
<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'>
|
||||||
|
<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={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
|
||||||
|
loading={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='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7 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 '>
|
||||||
|
<Username
|
||||||
|
username={getProfile.data?.data?.user?.userName}
|
||||||
|
/>
|
||||||
|
<Email
|
||||||
|
email={getProfile.data?.data?.user?.emailAddress}
|
||||||
|
isVerified={getProfile.data?.data?.user?.emailVerified}
|
||||||
|
/>
|
||||||
|
</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=''
|
||||||
|
defaultValue={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='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div>
|
||||||
|
{t('profile.address')}
|
||||||
|
</div>
|
||||||
|
<div className='text-description text-xs mt-2'>
|
||||||
|
{t('profile.address_live')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<Select
|
||||||
|
label={t('profile.province')}
|
||||||
|
items={getProvines.data?.data?.provinces.map((item: ProvinesItemType) => ({ label: item.name, value: item.id }))}
|
||||||
|
onChange={(e) => setProvinesId(e.target.value)}
|
||||||
|
className='bg-white border'
|
||||||
|
placeholder={t('select')}
|
||||||
|
value={provinesId}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={t('profile.city')}
|
||||||
|
items={getCities.data?.data?.cities ? getCities.data?.data?.cities.map((item: ProvinesItemType) => ({ label: item.name, value: item.id })) : []}
|
||||||
|
placeholder={t('select')}
|
||||||
|
{...formik.getFieldProps('cityId')}
|
||||||
|
error_text={formik.touched.cityId && formik.errors.cityId ? formik.errors.cityId : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='xl:mt-7 mt-4'>
|
||||||
|
<Input
|
||||||
|
label={t('profile.postal_code')}
|
||||||
|
{...formik.getFieldProps('postalCode')}
|
||||||
|
error_text={formik.touched.postalCode && formik.errors.postalCode ? formik.errors.postalCode : ''}
|
||||||
|
value={formik.values.postalCode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='xl:mt-7 mt-4'>
|
||||||
|
<Textarea
|
||||||
|
label={t('profile.address')}
|
||||||
|
defaultValue={formik.values.userAddress}
|
||||||
|
{...formik.getFieldProps('userAddress')}
|
||||||
|
error_text={formik.touched.userAddress && formik.errors.userAddress ? formik.errors.userAddress : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end mt-5'>
|
||||||
|
<Button
|
||||||
|
className='xl:w-fit px-10'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={updateProfile.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<TickCircle size={20} color='white' />
|
||||||
|
<div>
|
||||||
|
{t('save')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className='h-20 xl:hidden'></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { FC, Fragment, useState } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { isEmail } from '../../../config/func'
|
||||||
|
import { useUpdateEmail } from '../hooks/useProfileData'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
|
||||||
|
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 updateEmail = useUpdateEmail()
|
||||||
|
|
||||||
|
const handleShowModal = () => {
|
||||||
|
updateEmail.mutate({ email: email }, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast(t('profile.link_email'), 'success')
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
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}
|
||||||
|
loading={updateEmail.isPending}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Email
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { FC, Fragment, useState } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import OTPInput from 'react-otp-input'
|
||||||
|
import { useUpdatePhone, useVerifyPhone } from '../hooks/useProfileData'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { VerifyPhoneType } from '../types/ProfileTypes'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
|
||||||
|
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.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const params: VerifyPhoneType = {
|
||||||
|
code: code,
|
||||||
|
phone: phone
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyPhone.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setShowModal(false)
|
||||||
|
setIsReadOnly(true)
|
||||||
|
toast(t('success'), 'success')
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
loading={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
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useCheckUserName, useUpdateProfile } from '../hooks/useProfileData'
|
||||||
|
import { CheckUserNameType, UpdateProfileType } from '../types/ProfileTypes'
|
||||||
|
import { toast } from '../../../components/Toast'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
|
||||||
|
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(t('profile.username_save_success'), 'success')
|
||||||
|
setCanEdit(false)
|
||||||
|
setCanSave(false)
|
||||||
|
setStatus('')
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
loading={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,46 @@
|
|||||||
|
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),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSingleUpload = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: FormData) => api.singleUpload(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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/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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const singleUpload = async (params: FormData) => {
|
||||||
|
const { data } = await axios.post(`/uploader/single-file`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -25,10 +25,10 @@ const Favorite: FC<{ flagged: boolean, id: string, mailbox: string }> = ({ flagg
|
|||||||
}, [flagged])
|
}, [flagged])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-block mt-[3px] relative -right-6" onClick={handleFavoriteClick}>
|
<div onClick={handleFavoriteClick}>
|
||||||
<Star1
|
<Star1
|
||||||
variant={isFavorite ? 'Bold' : 'Outline'}
|
variant={isFavorite ? 'Bold' : 'Outline'}
|
||||||
size={18}
|
size={19}
|
||||||
color={isFavorite ? '#FFC107' : '#8C90A3'}
|
color={isFavorite ? '#FFC107' : '#8C90A3'}
|
||||||
className='cursor-pointer select-none'
|
className='cursor-pointer select-none'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useSummarizeEmail } from '../hooks/useEmailData';
|
||||||
|
import DefaulModal from '@/components/DefaulModal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { InboxMessage } from '@/pages/draft/types/DraftTypes';
|
||||||
|
|
||||||
|
const Summerize: FC<{ id: string, mailbox: string, item: InboxMessage }> = ({ id, mailbox, item }) => {
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const { mutate: summarizeEmail } = useSummarizeEmail();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [data, setData] = useState<any>()
|
||||||
|
|
||||||
|
const handleSummarizeEmail = () => {
|
||||||
|
summarizeEmail({ messageId: Number(id), mailbox }, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setData(data)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setOpen(true)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('data', data);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div onClick={handleSummarizeEmail}>
|
||||||
|
S
|
||||||
|
</div>
|
||||||
|
<DefaulModal
|
||||||
|
open={open}
|
||||||
|
close={() => setOpen(false)}
|
||||||
|
isHeader
|
||||||
|
title_header='پیش نمایش ایمیل'
|
||||||
|
>
|
||||||
|
<div className='mt-5'>
|
||||||
|
<div className='text-xs'>
|
||||||
|
موضوع:
|
||||||
|
</div>
|
||||||
|
<div className='mt-1.5 text-xs'>
|
||||||
|
{item.subject}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-7'>
|
||||||
|
<div className='text-xs'>
|
||||||
|
خلاصه متن:
|
||||||
|
</div>
|
||||||
|
<div className='mt-3 leading-5 text-xs text-description'>
|
||||||
|
{data?.data?.summary}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-14 flex justify-end'>
|
||||||
|
<div className='flex gap-3'>
|
||||||
|
<Button
|
||||||
|
label='بستن'
|
||||||
|
variant='secondary'
|
||||||
|
className='w-fit !px-10'
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label='مشاهده'
|
||||||
|
className='w-fit !px-10'
|
||||||
|
onClick={() => {
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Summerize
|
||||||
@@ -12,6 +12,7 @@ import { formatDate } from '@/config/func';
|
|||||||
import MarkAsRead from '@/assets/images/mark_as_read.svg'
|
import MarkAsRead from '@/assets/images/mark_as_read.svg'
|
||||||
import { useEmailActions } from '@/hooks/useEmailActions';
|
import { useEmailActions } from '@/hooks/useEmailActions';
|
||||||
import Favorite from './Components/Favorite';
|
import Favorite from './Components/Favorite';
|
||||||
|
import Summerize from './Components/Summerize';
|
||||||
|
|
||||||
const List: FC = () => {
|
const List: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -30,13 +31,6 @@ const List: FC = () => {
|
|||||||
const [selectedMessages, setSelectedMessages] = useState<InboxMessage[]>([]);
|
const [selectedMessages, setSelectedMessages] = useState<InboxMessage[]>([]);
|
||||||
|
|
||||||
const columns: ColumnType<InboxMessage>[] = [
|
const columns: ColumnType<InboxMessage>[] = [
|
||||||
{
|
|
||||||
key: 'flagged',
|
|
||||||
title: 'علامت گذاری',
|
|
||||||
render: (item) => (
|
|
||||||
<Favorite flagged={item.flagged} id={item.id.toString()} mailbox={item.mailbox} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'subject',
|
key: 'subject',
|
||||||
title: 'عنوان پیام',
|
title: 'عنوان پیام',
|
||||||
@@ -144,6 +138,19 @@ const List: FC = () => {
|
|||||||
setSelectedMessages([]);
|
setSelectedMessages([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInlineActions = (item: InboxMessage) => [
|
||||||
|
{
|
||||||
|
icon: <Favorite flagged={item.flagged} id={item.id.toString()} mailbox={item.mailbox} />,
|
||||||
|
onClick: () => { }, // عملیات در کامپوننت Favorite خودش انجام میشود
|
||||||
|
tooltip: item.flagged ? 'حذف از علاقهمندیها' : 'اضافه به علاقهمندیها'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Summerize item={item} id={item.id.toString()} mailbox={item.mailbox} />,
|
||||||
|
onClick: () => { }, // عملیات در کامپوننت Summerize خودش انجام میشود
|
||||||
|
tooltip: 'خلاصهسازی'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const getRowActions = (message: InboxMessage): RowActionItem[] => [
|
const getRowActions = (message: InboxMessage): RowActionItem[] => [
|
||||||
{
|
{
|
||||||
label: message.seen ? 'علامتگذاری به عنوان خوانده نشده' : 'علامتگذاری به عنوان خوانده شده',
|
label: message.seen ? 'علامتگذاری به عنوان خوانده نشده' : 'علامتگذاری به عنوان خوانده شده',
|
||||||
@@ -215,6 +222,7 @@ const List: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
|
inlineActions={getInlineActions}
|
||||||
rowActions={getRowActions}
|
rowActions={getRowActions}
|
||||||
pagination={pager ? {
|
pagination={pager ? {
|
||||||
totalPages: pager.totalPages,
|
totalPages: pager.totalPages,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import * as api from '../service/EmailService';
|
import * as api from '../service/EmailService';
|
||||||
import { SendEmailDto, MessageListQueryDto, SingleActionRequest } from '../types/Types';
|
import { SendEmailDto, MessageListQueryDto, SingleActionRequest, SummarizeEmailRequest } from '../types/Types';
|
||||||
|
|
||||||
export const useSendEmail = () => {
|
export const useSendEmail = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -119,3 +119,9 @@ export const useDownloadAttachment = () => {
|
|||||||
api.downloadAttachment(messageId, attachmentId, mailbox),
|
api.downloadAttachment(messageId, attachmentId, mailbox),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSummarizeEmail = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: SummarizeEmailRequest) => api.summarizeEmail(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
InboxResponse,
|
InboxResponse,
|
||||||
MessageDetailResponse,
|
MessageDetailResponse,
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
|
SummarizeEmailRequest,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const sendEmail = async (params: SendEmailDto) => {
|
export const sendEmail = async (params: SendEmailDto) => {
|
||||||
@@ -149,3 +150,8 @@ export const downloadAttachment = async (
|
|||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const summarizeEmail = async (params: SummarizeEmailRequest) => {
|
||||||
|
const { data } = await axios.post(`/ai-assistant/summarize-email`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -314,3 +314,10 @@ export interface SingleActionRequest {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
mailbox: string;
|
mailbox: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SummarizeEmailRequest {
|
||||||
|
messageId: number;
|
||||||
|
mailbox: string;
|
||||||
|
length?: "medium" | "short" | "long";
|
||||||
|
tone?: "professional" | "casual" | "formal";
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import SearchList from '@/pages/search/List'
|
|||||||
import DetailEmail from '@/pages/received/Detail'
|
import DetailEmail from '@/pages/received/Detail'
|
||||||
import Login from '@/pages/auth/Login'
|
import Login from '@/pages/auth/Login'
|
||||||
import Setting from '@/pages/setting/Setting'
|
import Setting from '@/pages/setting/Setting'
|
||||||
|
import Profile from '@/pages/profile/Profile'
|
||||||
|
|
||||||
const AppRouter: FC = () => {
|
const AppRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -28,6 +29,7 @@ const AppRouter: FC = () => {
|
|||||||
<Route path={Paths.detailEmail + ':id/:mailbox'} element={<DetailEmail />} />
|
<Route path={Paths.detailEmail + ':id/:mailbox'} element={<DetailEmail />} />
|
||||||
<Route path={Paths.login} element={<Login />} />
|
<Route path={Paths.login} element={<Login />} />
|
||||||
<Route path={Paths.setting} element={<Setting />} />
|
<Route path={Paths.setting} element={<Setting />} />
|
||||||
|
<Route path={Paths.profile} element={<Profile />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { ArrowDown2, CloseCircle, HambergerMenu, Logout, ProfileCircle, SearchNo
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Notifications from '../pages/notification/Notification'
|
import Notifications from '../pages/notification/Notification'
|
||||||
import { useSharedStore } from './store/sharedStore'
|
import { useSharedStore } from './store/sharedStore'
|
||||||
import { useGetProfile } from '../pages/profile/hooks/useProfileData'
|
|
||||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Paths } from '@/utils/Paths'
|
import { Paths } from '@/utils/Paths'
|
||||||
import SideBarItem from './SideBarItem'
|
import SideBarItem from './SideBarItem'
|
||||||
import AvatarImage from '../assets/images/avatar_image.png'
|
import AvatarImage from '@/assets/images/avatar_image.png'
|
||||||
|
import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ const Header: FC = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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='fixed z-2 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)]'>
|
||||||
{/* Desktop Search */}
|
{/* Desktop Search */}
|
||||||
<div className='min-w-[270px] hidden xl:block relative'>
|
<div className='min-w-[270px] hidden xl:block relative'>
|
||||||
<div id="search-input-container">
|
<div id="search-input-container">
|
||||||
|
|||||||
Reference in New Issue
Block a user