This commit is contained in:
hamid zarghami
2025-07-13 13:19:06 +03:30
parent 0cf98ee67c
commit 257b287dfd
13 changed files with 491 additions and 114 deletions
+2
View File
@@ -4,3 +4,5 @@ VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.1.134:4000'
VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
+13 -10
View File
@@ -1,13 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dmail Admin | داناک</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+3 -1
View File
@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
<svg width="85" height="72" viewBox="0 0 85 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.97 0C39.21 0 20.6 0.0500073 20.18 0.0700073C28.87 8.77001 45.47 25.11 55.3 34.9C54.27 35.93 53.22 36.98 52.11 38.09C49.1 41.1 47.07 41.82 44.03 38.78C31.57 26.32 5.38 0.07 5.28 0H0V71.79C0 71.79 11.97 71.77 12.57 71.77V24.82C12.63 24.6 16.87 28.77 22.08 33.96L21.63 71.81C22.28 71.81 39.72 71.83 47.28 71.8C49.16 71.8 51.06 71.69 52.93 71.49C69.23 69.67 82.67 55.87 84.14 39.48C86.07 17.95 69.62 0 47.98 0H47.97ZM46.3 11.58C55.58 10.72 63.99 15.5 67.84 22.39C66.04 24.19 64.36 25.86 62.72 27.49C57.77 22.69 49.24 14.43 46.3 11.58ZM48.3 60.07C45.38 60.12 42.46 60.12 39.54 60.11H31.86V43.77C33.94 45.85 35.86 47.78 37.43 49.33C43.64 55.49 53.12 52.88 59.33 46.75C63.62 42.51 71.94 34.1 72.57 33.51C73.5 47.57 62.46 59.84 48.29 60.09L48.3 60.07Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 876 B

+1 -1
View File
@@ -81,7 +81,7 @@ const queryClient = new QueryClient({
// Clear tokens and redirect to login
await removeToken();
await removeRefreshToken();
window.location.href = '/auth/login';
window.location.href = import.meta.env.VITE_LOGIN_URL;
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import { FC, useState } from 'react'
import DefaulModal from './DefaulModal'
import { useTranslation } from 'react-i18next'
import Button from './Button'
import Textarea from './Textarea'
type Props = {
isLoading?: boolean,
close: () => void,
isOpen: boolean,
onConfrim: (text?: string) => void,
label?: string,
isHasDescription?: boolean,
title?: string
}
const ModalConfrim: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [description, setDescription] = useState<string>('')
return (
<DefaulModal
open={props.isOpen}
close={props.close}
title_header={props.title ? props.title : t('confrim.subject')}
isHeader
>
<div className='mt-6'>
<div className='text-sm text-center'>
{
props.label ?
props.label
:
t('confrim.content')
}
{
props.isHasDescription &&
<div className='mt-4'>
<Textarea
placeholder={t('description')}
onChange={(e) => setDescription(e.target.value)}
value={description}
className='bg-transparent border border-gray-500 rounded-xl w-[300px] p-2'
label=''
/>
</div>
}
</div>
<div className='flex gap-4 justify-center mt-10'>
<Button
label={t('confrim.yes')}
onClick={() => props.onConfrim(props.isHasDescription ? description : undefined)}
loading={props.isLoading}
/>
<Button
label={t('confrim.cancel')}
className='bg-transparent text-black border border-primary'
onClick={props.close}
/>
</div>
</div>
</DefaulModal>
)
}
export default ModalConfrim
+13
View File
@@ -101,6 +101,11 @@
"smtp_port": "SMTP پورت",
"username": "نام کاربری",
"password": "پسورد",
"setting_imap": "تنظیمات IMAP",
"setting_imap_description": "تنظیمات IMAP خود را با دقت وارد کنید.",
"imap_server": "IMAP سرور",
"imap_port": "IMAP پورت",
"encryption": "نوع رمزنگاری",
"record_dns": "رکوردهای DNS",
"record_dns_description": "در این قسمت میتوانید وضعیت رکوردهای DNS سرویس ایمیل خود را بررسی کنید.",
"your_domain": "دامنه شما",
@@ -166,5 +171,13 @@
"answer": "پاسخ",
"forward": "ارسال به دیگران",
"message": "متن پبام"
},
"confrim": {
"subject": "تایید عملیات",
"content": "آیا از انجام این عملیات مطمئن هستید؟",
"yes": "بله",
"cancel": "انصراف",
"delete_address_title": "حذف آدرس",
"delete_address_message": "آیا از حذف آدرس {email} مطمئن هستید؟"
}
}
+232 -58
View File
@@ -5,44 +5,149 @@ import { ColumnType } from '@/components/types/TableTypes'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
import { TickCircle, Edit2, Trash } from 'iconsax-react'
import { useForm } from 'react-hook-form'
import { AddressData, CreateAddressType } from './types/Types'
import { AddressData, CreateAddressType, UpdateAddressType } from './types/Types'
import { useGetDomains } from '../domain/hooks/useDomainData'
import { yupResolver } from "@hookform/resolvers/yup"
import * as yup from "yup"
import { useCreateAddress, useGetAddress } from './hooks/useAddressData'
import { useCreateAddress, useGetAddress, useUpdateAddress, useDeleteAddress } from './hooks/useAddressData'
import { useGetSpace } from '../domain/hooks/useDomainData'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
import ProgressBarEmail from './components/ProgressBarEmail'
import ToggleStatus from './components/ToggleStatus'
import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown'
import ModalConfrim from '@/components/ModalConfrim'
const Address: FC = () => {
const { t } = useTranslation()
const schema = yup.object().shape({
title: yup.string().required(t('errors.required')),
username: yup.string().required(t('errors.required')),
password: yup.string().required(t('errors.required')),
domainId: yup.string().required(t('errors.required')),
})
const [search, setSearch] = useState<string>('')
const [status, setStatus] = useState<string>('all')
const [currentPage, setCurrentPage] = useState<number>(1)
const [selectedAddress, setSelectedAddress] = useState<AddressData | null>(null)
const [isEditMode, setIsEditMode] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [addressToDelete, setAddressToDelete] = useState<AddressData | null>(null)
const { data: domain } = useGetDomains()
const { data: spaceData } = useGetSpace()
const { mutate: createAddress, isPending } = useCreateAddress()
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status)
const { register, handleSubmit, formState: { errors }, setValue
} = useForm<CreateAddressType>({
resolver: yupResolver(schema),
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
const { mutate: deleteAddress } = useDeleteAddress()
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10)
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors
} = useForm<FormData>({
defaultValues: {
domainId: domain?.data?.domain?.id,
}
})
const validateForm = (params: FormData): boolean => {
clearErrors()
let isValid = true
if (!params.title) {
setError('title', { message: t('errors.required') })
isValid = false
}
if (!params.username) {
setError('username', { message: t('errors.required') })
isValid = false
}
if (!isEditMode && !params.password) {
setError('password', { message: t('errors.required') })
isValid = false
}
if (!params.domainId) {
setError('domainId', { message: t('errors.required') })
isValid = false
}
// اعتبارسنجی quota
if (params.quotaMB) {
const quotaInBytes = params.quotaMB * 1024 * 1024
const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024
if (quotaInBytes > remainingSpace) {
setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقی‌مانده است' })
isValid = false
}
}
return isValid
}
const handleEdit = (address: AddressData) => {
setSelectedAddress(address)
setIsEditMode(true)
reset({
title: address.title,
username: address.displayName,
password: '',
domainId: address.domain.id,
quotaMB: address.emailQuota ? Math.round(address.emailQuota / (1024 * 1024)) : undefined,
})
}
const handleDelete = (address: AddressData) => {
setAddressToDelete(address)
setIsDeleteModalOpen(true)
}
const handleConfirmDelete = () => {
if (addressToDelete) {
deleteAddress(addressToDelete.id, {
onSuccess: (data) => {
toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success')
setIsDeleteModalOpen(false)
setAddressToDelete(null)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error')
setIsDeleteModalOpen(false)
setAddressToDelete(null)
}
})
}
}
const handleCloseDeleteModal = () => {
setIsDeleteModalOpen(false)
setAddressToDelete(null)
}
const handleCancelEdit = () => {
setSelectedAddress(null)
setIsEditMode(false)
reset({
title: '',
username: '',
password: '',
domainId: domain?.data?.domain?.id,
quotaMB: undefined,
})
}
const getRowActions = (address: AddressData): RowActionItem[] => [
{
label: 'ویرایش',
icon: <Edit2 color='black' size={16} />,
onClick: () => handleEdit(address),
},
{
label: 'حذف',
icon: <Trash color='red' size={16} />,
onClick: () => handleDelete(address),
className: 'text-red-600 hover:bg-red-50'
}
]
const columns: ColumnType<AddressData>[] = [
{
title: t('setting.address_title'),
@@ -64,7 +169,6 @@ const Address: FC = () => {
key: 'quota',
width: '300px',
render: (item: AddressData) => {
return (
<ProgressBarEmail key={item.id} item={item} />
)
@@ -80,6 +184,18 @@ const Address: FC = () => {
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
</div>
)
},
{
title: 'عملیات',
key: 'actions',
width: '120px',
align: 'center',
render: (item: AddressData) => (
<RowActionsDropdown
key={item.id}
actions={getRowActions(item)}
/>
)
}
]
@@ -89,24 +205,52 @@ const Address: FC = () => {
{ value: 'inactive', label: t('setting.inactive') }
]
const onSubmit = (params: CreateAddressType) => {
createAddress(params, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
const onSubmit = (params: FormData) => {
if (!validateForm(params)) {
return
}
if (isEditMode && selectedAddress) {
const updateParams: UpdateAddressType = {
title: params.title,
password: params.password || undefined,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
}
})
updateAddress({ id: selectedAddress.id, params: updateParams }, {
onSuccess: (data) => {
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
handleCancelEdit()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error')
}
})
} else {
const createParams: CreateAddressType = {
...params,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
}
createAddress(createParams, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
reset()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
}
useEffect(() => {
if (domain?.data?.domain?.id) {
if (domain?.data?.domain?.id && !isEditMode) {
setValue('domainId', domain?.data?.domain?.id)
}
}, [domain])
}, [domain, isEditMode])
useEffect(() => {
setCurrentPage(1)
}, [search, status])
return (
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
@@ -136,26 +280,28 @@ const Address: FC = () => {
columns={columns}
data={addresses?.data?.users}
pagination={{
currentPage: 1,
totalPages: 6,
onPageChange: (page) => console.log('Page:', page)
currentPage: currentPage,
totalPages: addresses?.data?.pager?.totalPages || 1,
onPageChange: (page) => setCurrentPage(page)
}}
/>
</div>
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
<div>
{t('setting.add_address')}
</div>
{/* <div className='mt-8 flex items-center justify-between'>
<div className='flex justify-between items-center'>
<div>
{t('setting.status')}
{isEditMode ? 'ویرایش آدرس' : t('setting.add_address')}
</div>
<div className='dltr pt-2'>
<Switch />
</div>
</div> */}
{isEditMode && (
<Button
variant='secondary'
onClick={handleCancelEdit}
className='px-4 w-fit text-xs'
>
انصراف
</Button>
)}
</div>
<div className='mt-6'>
<Input
@@ -169,7 +315,7 @@ const Address: FC = () => {
<Input
label={t('setting.title1')}
{...register('title')}
error_text={errors.title?.message}
error_text={errors.title?.message as string}
/>
</div>
@@ -178,7 +324,8 @@ const Address: FC = () => {
label={t('setting.username')}
className='text-left'
{...register('username')}
error_text={errors.username?.message}
error_text={errors.username?.message as string}
readOnly={isEditMode}
/>
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
@ {domain?.data?.domain?.name}
@@ -187,38 +334,65 @@ const Address: FC = () => {
<div className='mt-6'>
<Input
label={t('setting.password')}
label='حجم ایمیل (مگابایت)'
type='number'
{...register('quotaMB', { valueAsNumber: true })}
error_text={errors.quotaMB?.message as string}
placeholder='حجم ایمیل را به مگابایت وارد کنید'
/>
<div className='text-xs text-gray-500 mt-1'>
فضای باقیمانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت
</div>
</div>
<div className='mt-6'>
<Input
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
type='password'
{...register('password')}
error_text={errors.password?.message}
error_text={errors.password?.message as string}
placeholder={isEditMode ? 'برای تغییر رمز عبور وارد کنید' : ''}
/>
</div>
<div className='mt-2 text-xs'>
<div>رمز عبور میبایست:</div>
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
<li>حداقل ۸ کاراکتر باشد</li>
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
<li>شامل اعداد باشد</li>
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
</ul>
</div>
{!isEditMode && (
<div className='mt-2 text-xs'>
<div>رمز عبور میبایست:</div>
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
<li>حداقل ۸ کاراکتر باشد</li>
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
<li>شامل اعداد باشد</li>
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
</ul>
</div>
)}
<div className='mt-9 flex justify-end'>
<Button
className='w-fit px-20'
onClick={handleSubmit(onSubmit)}
loading={isPending}
loading={isPending || isUpdating}
>
<div className='flex gap-2'>
<TickCircle size={20} color='white' />
<div>
{t('setting.create')}
{isEditMode ? 'آپدیت' : t('setting.create')}
</div>
</div>
</Button>
</div>
</div>
<ModalConfrim
isOpen={isDeleteModalOpen}
close={handleCloseDeleteModal}
onConfrim={handleConfirmDelete}
title={t('confrim.delete_address_title')}
label={addressToDelete ? t('confrim.delete_address_message').replace('{email}', addressToDelete.emailAddress) : ''}
isLoading={false}
/>
</div>
)
}
@@ -1,11 +1,17 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { useQueryClient } from "@tanstack/react-query";
import { UpdateAddressType } from "../types/Types";
export const useGetAddress = (search: string, status: string) => {
export const useGetAddress = (
search: string,
status: string,
page: number = 1,
limit: number = 10
) => {
return useQuery({
queryKey: ["address", search, status],
queryFn: () => api.getAddress(search, status),
queryKey: ["address", search, status, page, limit],
queryFn: () => api.getAddress(search, status, page, limit),
});
};
@@ -20,7 +26,32 @@ export const useCreateAddress = () => {
};
export const useToggleStatus = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.toggleStatus,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["address"] });
},
});
};
export const useUpdateAddress = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, params }: { id: string; params: UpdateAddressType }) =>
api.updateAddress(id, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["address"] });
},
});
};
export const useDeleteAddress = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteAddress,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["address"] });
},
});
};
+19 -2
View File
@@ -1,11 +1,18 @@
import axios from "@/config/axios";
import { CreateAddressType } from "../types/Types";
import { CreateAddressType, UpdateAddressType } from "../types/Types";
export const getAddress = async (search: string, status: string) => {
export const getAddress = async (
search: string,
status: string,
page: number = 1,
limit: number = 10
) => {
const query = new URLSearchParams();
if (search) query.set("q", search);
if (status && status !== "all")
query.set("isActive", status === "active" ? "1" : "0");
query.set("page", page.toString());
query.set("limit", limit.toString());
const { data } = await axios.get(`/users?${query.toString()}`);
return data;
};
@@ -19,3 +26,13 @@ export const toggleStatus = async (id: string) => {
const { data } = await axios.patch(`/users/${id}/toggle-status`);
return data;
};
export const updateAddress = async (id: string, params: UpdateAddressType) => {
const { data } = await axios.patch(`/users/${id}`, params);
return data;
};
export const deleteAddress = async (id: string) => {
const { data } = await axios.delete(`/users/${id}`);
return data;
};
+21
View File
@@ -3,6 +3,15 @@ export type CreateAddressType = {
password: string;
domainId: string;
title: string;
quota?: number;
};
export type UpdateAddressType = {
password?: string;
displayName?: string;
quota?: number;
aliases?: string[];
title?: string;
};
export interface AddressData extends Record<string, unknown> {
@@ -56,3 +65,15 @@ export interface AddressData extends Record<string, unknown> {
business: string;
};
}
export interface AddressResponse {
data: {
users: AddressData[];
pager: {
page: number;
limit: number;
total: number;
totalPages: number;
};
};
}
+68 -38
View File
@@ -1,46 +1,39 @@
import Input from '@/components/Input'
import RadioGroup from '@/components/RadioGroup'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useMailServerData } from './hooks/useMailServerData'
const Domain: FC = () => {
const MailServer: FC = () => {
const { t } = useTranslation()
return (
<div className='bg-white rounded-4xl p-8 mt-9'>
<div>
<div className='flex xl:flex-row gap-4 flex-col justify-between xl:items-end border-b pb-10 broder-border'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.setting_fetch')}
</div>
<div className='xl:text-sm text-xs text-description mt-1.5'>
{t('setting.setting_fetch_description')}
</div>
</div>
<div className='flex-1'>
<RadioGroup
items={[
{
label: '۱ دقیقه',
value: '1'
},
{
label: '۲ دقیقه',
value: '2'
},
{
label: '۳ دقیقه',
value: '3'
}
]}
onChange={() => null}
selected='1'
/>
</div>
const { data, isLoading, error } = useMailServerData();
console.log('Mail Server Data:', data); // برای debug
if (isLoading) {
return (
<div className='bg-white rounded-4xl p-8 mt-9'>
<div className='text-center py-8'>
در حال بارگذاری...
</div>
</div>
);
}
if (error) {
return (
<div className='bg-white rounded-4xl p-8 mt-9'>
<div className='text-center py-8 text-red-500'>
خطا در بارگذاری اطلاعات
</div>
</div>
);
}
return (
<div className='bg-white rounded-4xl p-8 mt-9'>
{/* SMTP Settings */}
<div className='mt-9'>
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.setting_smtp')}
@@ -53,20 +46,57 @@ const Domain: FC = () => {
<div className='flex-1'>
<Input
label={t('setting.smtp_server')}
value={data?.data?.setupInfo?.smtpSettings?.server || ''}
readOnly
/>
<div className='mt-8'>
<Input
label={t('setting.smtp_port')}
value={data?.data?.setupInfo?.smtpSettings?.port?.toString() || ''}
readOnly
/>
</div>
<div className='mt-8'>
<Input
label={t('setting.username')}
label={t('setting.encryption')}
value={data?.data?.setupInfo?.smtpSettings?.encryption || ''}
readOnly
/>
</div>
</div>
</div>
</div>
{/* IMAP Settings */}
<div className='mt-9 border-t pt-9'>
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.setting_imap')}
</div>
<div className='xl:text-sm text-xs text-description mt-1.5'>
{t('setting.setting_imap_description')}
</div>
</div>
<div className='flex-1'>
<Input
label={t('setting.imap_server')}
value={data?.data?.setupInfo?.imapSettings?.server || ''}
readOnly
/>
<div className='mt-8'>
<Input
label={t('setting.imap_port')}
value={data?.data?.setupInfo?.imapSettings?.port?.toString() || ''}
readOnly
/>
</div>
<div className='mt-8'>
<Input
label={t('setting.password')}
label={t('setting.encryption')}
value={data?.data?.setupInfo?.imapSettings?.encryption || ''}
readOnly
/>
</div>
</div>
@@ -76,4 +106,4 @@ const Domain: FC = () => {
)
}
export default Domain
export default MailServer
@@ -0,0 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import { getServerInfo } from "../service/Service";
export const useMailServerData = () => {
return useQuery({
queryKey: ["mail-server-data"],
queryFn: getServerInfo,
});
};
@@ -0,0 +1,6 @@
import axios from "@/config/axios";
export const getServerInfo = async () => {
const { data } = await axios.get("/mail-server/info");
return data;
};