profile + summerize

This commit is contained in:
hamid zarghami
2025-07-24 17:17:55 +03:30
parent d8726db44e
commit cc744ebac9
24 changed files with 1063 additions and 20 deletions
+57
View File
@@ -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
+21
View File
@@ -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
+94 -7
View File
@@ -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}
/>
);
};
*/ */
+8
View File
@@ -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;
} }
+14
View File
@@ -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;
+31
View File
@@ -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
View File
@@ -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>
) )
+255
View File
@@ -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
+64
View File
@@ -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
+143
View File
@@ -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
+131
View File
@@ -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;
};
+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;
};
+2 -2
View File
@@ -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
+15 -7
View File
@@ -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,
+7 -1
View File
@@ -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;
};
+7
View File
@@ -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";
}
+2
View File
@@ -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>
) )
} }
+3 -3
View File
@@ -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">