domain compelete api attachment
This commit is contained in:
+73
-26
@@ -1,32 +1,79 @@
|
||||
import { FC } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useWorkspaces } from './hooks/useHomeData'
|
||||
import { workspaceItem } from './types/HomeTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
const Home: FC = () => {
|
||||
|
||||
const getWorkspaces = useWorkspaces()
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedWorkspace(localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (getWorkspaces.isError && getWorkspaces.error instanceof Error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const error = getWorkspaces.error as any;
|
||||
if (error.response?.status === 403) {
|
||||
localStorage.removeItem(import.meta.env.VITE_WORKSPACE_ID)
|
||||
window.location.href = 'https://console.danakcorp.com'
|
||||
}
|
||||
}
|
||||
}, [getWorkspaces.isError, getWorkspaces.data])
|
||||
|
||||
|
||||
const handleChangeWorkspace = (id: string) => {
|
||||
localStorage.setItem(import.meta.env.VITE_WORKSPACE_ID, id)
|
||||
setSelectedWorkspace(id)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||
<div className="text-center py-12 md:py-20">
|
||||
<h1 className="text-2xl md:text-3xl xl:text-4xl font-bold text-gray-800 mb-4">
|
||||
خوش آمدید به سیستم مدیریت ایمیل
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-gray-600 mb-8 max-w-2xl mx-auto">
|
||||
از منوی کناری میتوانید به بخشهای مختلف سیستم دسترسی داشته باشید
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 md:gap-6 mt-8 md:mt-12">
|
||||
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
|
||||
<h3 className="text-lg md:text-xl font-semibold mb-2">پیامهای دریافتی</h3>
|
||||
<p className="text-xs md:text-sm text-gray-600">مدیریت ایمیلهای واردشده</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
|
||||
<h3 className="text-lg md:text-xl font-semibold mb-2">پیامهای ارسالی</h3>
|
||||
<p className="text-xs md:text-sm text-gray-600">مشاهده ایمیلهای ارسال شده</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
|
||||
<h3 className="text-lg md:text-xl font-semibold mb-2">پیشنویسها</h3>
|
||||
<p className="text-xs md:text-sm text-gray-600">ادامه نوشتن ایمیلهای ناتمام</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
|
||||
<h3 className="text-lg md:text-xl font-semibold mb-2">تنظیمات</h3>
|
||||
<p className="text-xs md:text-sm text-gray-600">پیکربندی سیستم و حساب کاربری</p>
|
||||
</div>
|
||||
<div className='w-full flex gap-6'>
|
||||
<div className='flex-1'>
|
||||
|
||||
<div className='mt-8 flex-wrap text-sm flex gap-6 items-center'>
|
||||
{
|
||||
getWorkspaces.data?.data?.workspaces?.map((item: workspaceItem) => {
|
||||
return (
|
||||
<div className={`p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 bg-white rounded-3xl cursor-pointer transition-all duration-300 hover:shadow-lg ${selectedWorkspace === item.id ? 'border border-black' : ''}`}
|
||||
onClick={() => handleChangeWorkspace(item.id)}
|
||||
>
|
||||
<div className='mt-5'>
|
||||
{item.businessName}
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
{item.plan?.name}
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center mt-5 w-full'>
|
||||
<div className=' flex gap-1'>
|
||||
<div>تاریخ پایان: </div>
|
||||
<div className='text-description text-sm'>{moment(item?.endDate).format('jYYYY/jMM/jDD')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className='mt-5 flex justify-end w-full'>
|
||||
|
||||
<a onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}} target='_blank' href={`${import.meta.env.VITE_DZONE_URL}/${item.slug}`}>
|
||||
{`${import.meta.env.VITE_DZONE_URL}/${item.slug}`}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
<div className='px-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px]'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Add } from 'iconsax-react'
|
||||
import { FC, useState } from 'react'
|
||||
import DefaulModal from '../../../components/DefaulModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../../components/Input'
|
||||
import Select from '../../../components/Select'
|
||||
import SwitchComponent from '../../../components/Switch'
|
||||
|
||||
const BoxNewAccessbility: FC = () => {
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex-1 min-w-[40%] xl:min-w-[20%] flex justify-center items-center h-[160px]"
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: -1,
|
||||
borderRadius: '22px',
|
||||
}}
|
||||
>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="none"
|
||||
// rx="22"
|
||||
ry="22"
|
||||
stroke="#8C90A3"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="6,14"
|
||||
// strokeDashoffset="25"
|
||||
/>
|
||||
</svg>
|
||||
<div style={{ zIndex: 1 }}>
|
||||
<Add size={38} color="#8C90A3" />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<DefaulModal
|
||||
open={open}
|
||||
close={() => setOpen(false)}
|
||||
isHeader={true}
|
||||
title_header={t('home.create_new_access')}
|
||||
>
|
||||
<div className='mt-6'>
|
||||
<div className='flex items-center gap-6'>
|
||||
<div className='min-w-[210px]'>
|
||||
<Input
|
||||
variant='search'
|
||||
className='bg-opacity-25 bg-white'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
items={[
|
||||
{ value: '1', label: '1' },
|
||||
{ value: '2', label: '2' },
|
||||
{ value: '3', label: '3' },
|
||||
]}
|
||||
placeholder={t('all')}
|
||||
className='bg-opacity-25 bg-white w-[100px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex flex-wrap gap-6'>
|
||||
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
|
||||
<div className='flex gap-4'>
|
||||
<div className='size-10 rounded-xl bg-red-300'></div>
|
||||
<div>
|
||||
<div className='text-sm'>
|
||||
دی منو
|
||||
</div>
|
||||
<div className='text-xs text-description'>
|
||||
منو رستوران
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<SwitchComponent
|
||||
active
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
|
||||
<div className='flex gap-4'>
|
||||
<div className='size-10 rounded-xl bg-blue-300'></div>
|
||||
<div>
|
||||
<div className='text-sm'>
|
||||
دی منو
|
||||
</div>
|
||||
<div className='text-xs text-description'>
|
||||
منو رستوران
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<SwitchComponent
|
||||
active
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
|
||||
<div className='flex gap-4'>
|
||||
<div className='size-10 rounded-xl bg-green-300'></div>
|
||||
<div>
|
||||
<div className='text-sm'>
|
||||
دی منو
|
||||
</div>
|
||||
<div className='text-xs text-description'>
|
||||
منو رستوران
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<SwitchComponent
|
||||
active
|
||||
onChange={() => { }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 min-w-[40%]'></div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BoxNewAccessbility
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowLeft } from 'iconsax-react'
|
||||
import CoverImage from '../../../assets/images/banner.png'
|
||||
|
||||
const DanakLearning: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
return (
|
||||
<div className='bg-white w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div>
|
||||
{t('home.danak_learning')}
|
||||
</div>
|
||||
<div className='flex gap-2 items-center text-[10px]'>
|
||||
<div>{t('home.see_all')}</div>
|
||||
<ArrowLeft size={12} color='black' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-7'>
|
||||
<div className='flex gap-3'>
|
||||
<div>
|
||||
<img src={CoverImage} alt='danak-learning-1' className='w-36 h-24 object-cover rounded-2xl' />
|
||||
</div>
|
||||
|
||||
<div className='text-xs'>
|
||||
<div className='leading-5'>
|
||||
لورم ایپسوم متن ساختگی با تولید سادگی
|
||||
</div>
|
||||
<div className='flex gap-1 text-[11px] mt-3 text-description'>
|
||||
<div>دیزاین ,</div>
|
||||
<div>۲۴ دقیقه</div>
|
||||
</div>
|
||||
<div className='text-description text-[11px] mt-2'>
|
||||
آذر ماه 1403
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DanakLearning
|
||||
@@ -0,0 +1,37 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
icon: ReactNode,
|
||||
color: string,
|
||||
count: number,
|
||||
description: string,
|
||||
to?: string
|
||||
}
|
||||
|
||||
const ItemDashboard: FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Link to={props.to ? props.to : ''} className='p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl'>
|
||||
<div className='size-10 rounded-full bg-[#EEF0F7] flex justify-center items-center'>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
{props.title}
|
||||
</div>
|
||||
|
||||
<div className='mt-5 text-xs text-description flex gap-1.5 items-center'>
|
||||
<div style={{ background: props.color }} className='size-1.5 rounded-full'></div>
|
||||
<div className='flex gap-0.5'>
|
||||
<div>{props.count}</div>
|
||||
<div className='whitespace-nowrap'>
|
||||
{props.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default ItemDashboard
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/HomeService";
|
||||
|
||||
export const useWorkspaces = () => {
|
||||
return useQuery({
|
||||
queryKey: ["workspaces"],
|
||||
queryFn: api.getWorkspaces,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import danakAxios from "../../../config/axiosDanak";
|
||||
|
||||
export const getWorkspaces = async () => {
|
||||
const { data } = await danakAxios.get(
|
||||
`/subscriptions/workspaces/${import.meta.env.VITE_SERVICE_ID}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { UserItemType } from "../../users/types/UserTypes";
|
||||
|
||||
export type workspaceItem = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
plan: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
duration: number;
|
||||
price: number;
|
||||
originalPrice: number;
|
||||
isActive: boolean;
|
||||
service: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
isDanakSuggest: boolean;
|
||||
description: string;
|
||||
author: string;
|
||||
createDate: string;
|
||||
userCount: number;
|
||||
serviceLanguage: string;
|
||||
softwareLanguage: string;
|
||||
metaDescription: string;
|
||||
isActive: boolean;
|
||||
showInSlider: boolean;
|
||||
link: string;
|
||||
icon: string;
|
||||
coverUrl: string;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
deletedAt: string | null;
|
||||
};
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
businessName: string;
|
||||
businessPhone: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
status: string;
|
||||
staff: UserItemType[];
|
||||
};
|
||||
@@ -12,7 +12,7 @@ import Signture from './signture/Signture'
|
||||
|
||||
const Setting: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_PERSONALITY)
|
||||
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
|
||||
+118
-109
@@ -1,145 +1,154 @@
|
||||
import Button from '@/components/Button'
|
||||
import Input from '@/components/Input'
|
||||
import Table from '@/components/Table'
|
||||
import { FC } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { Copy } from 'iconsax-react'
|
||||
|
||||
interface DNSRecord extends Record<string, unknown> {
|
||||
id: number
|
||||
status: 'verified' | 'pending'
|
||||
type: string
|
||||
name: string
|
||||
value: string
|
||||
number: number
|
||||
}
|
||||
import { TickCircle, Copy, CloseCircle, Clock, TickSquare } from 'iconsax-react'
|
||||
import CreateDomain from './components/CreateDomain'
|
||||
import { useGetDnsRecords } from './hooks/useDomainData'
|
||||
import { DnsRecordType } from './types/Types'
|
||||
|
||||
const Domain: FC = () => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const { data, isLoading } = useGetDnsRecords(disabled)
|
||||
const [copiedField, setCopiedField] = useState<string | null>(null)
|
||||
|
||||
// نمونه دادههای DNS records طبق تصویر
|
||||
const dnsRecords: DNSRecord[] = [
|
||||
{
|
||||
id: 1,
|
||||
status: 'verified',
|
||||
type: 'TXT',
|
||||
name: 'example.com',
|
||||
value: 'v=spf1 include:_spf.example.com ~all',
|
||||
number: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
status: 'verified',
|
||||
type: 'MX | Priority:10',
|
||||
name: 'example.com',
|
||||
value: '10 mail.example.com',
|
||||
number: 1
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
status: 'verified',
|
||||
type: 'Cname | No CDN',
|
||||
name: 'example.com',
|
||||
value: 'v=spf1 include:_spf.example.com ~all',
|
||||
number: 1
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
status: 'verified',
|
||||
type: 'TXT',
|
||||
name: 'example.com',
|
||||
value: 'v=spf1 include:_spf.example.com ~all',
|
||||
number: 1
|
||||
useEffect(() => {
|
||||
if (data?.data?.overallStatus?.isVerified && !disabled) {
|
||||
setDisabled(true)
|
||||
}
|
||||
]
|
||||
}, [data])
|
||||
|
||||
const getStatusIcon = (status: DnsRecordType['status']) => {
|
||||
switch (status) {
|
||||
case 'VERIFIED':
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TickCircle size={20} color="#22C55E" variant="Bold" />
|
||||
<span className="text-green-500 text-xs">{t('domain.status.verified')}</span>
|
||||
</div>
|
||||
)
|
||||
case 'PENDING':
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={20} color="#F59E0B" variant="Bold" />
|
||||
<span className="text-yellow-500 text-xs">{t('domain.status.pending')}</span>
|
||||
</div>
|
||||
)
|
||||
case 'FAILED':
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<CloseCircle size={20} color="#EF4444" variant="Bold" />
|
||||
<span className="text-red-500 text-xs">{t('domain.status.failed')}</span>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string, fieldKey: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedField(fieldKey)
|
||||
setTimeout(() => {
|
||||
setCopiedField(null)
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'شماره',
|
||||
key: 'number',
|
||||
width: '80px',
|
||||
title: t('domain.table.record_type'),
|
||||
key: 'type',
|
||||
width: '100px',
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: 'مقدار رکورد',
|
||||
key: 'value',
|
||||
render: (record: DNSRecord) => (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<span className="truncate max-w-[300px]">{record.value}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'نام رکورد',
|
||||
title: t('domain.table.record_name'),
|
||||
key: 'name',
|
||||
render: (record: DNSRecord) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="text-blue-500 hover:text-blue-700">
|
||||
<Copy size={16} color='#0038FF' />
|
||||
</button>
|
||||
<span>{record.name}</span>
|
||||
</div>
|
||||
render: (record: DnsRecordType) => {
|
||||
const fieldKey = `name-${record.id}`
|
||||
const isCopied = copiedField === fieldKey
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => copyToClipboard(record.name, fieldKey)}
|
||||
className={`transition-all duration-200 ${isCopied ? 'text-green-500' : 'text-blue-500 hover:text-blue-700'}`}
|
||||
>
|
||||
{isCopied ? (
|
||||
<TickSquare size={16} color='#22C55E' variant="Bold" />
|
||||
) : (
|
||||
<Copy size={16} color='#0038FF' />
|
||||
)}
|
||||
</button>
|
||||
<span className="truncate max-w-[200px] dltr">{record.name}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('domain.table.record_value'),
|
||||
key: 'value',
|
||||
render: (record: DnsRecordType) => {
|
||||
const fieldKey = `value-${record.id}`
|
||||
const isCopied = copiedField === fieldKey
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => copyToClipboard(record.value, fieldKey)}
|
||||
className={`transition-all duration-200 ${isCopied ? 'text-green-500' : 'text-blue-500 hover:text-blue-700'}`}
|
||||
>
|
||||
{isCopied ? (
|
||||
<TickSquare size={16} color='#22C55E' variant="Bold" />
|
||||
) : (
|
||||
<Copy size={16} color='#0038FF' />
|
||||
)}
|
||||
</button>
|
||||
<span className="truncate max-w-[300px] dltr">{record.value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('domain.table.priority'),
|
||||
key: 'priority',
|
||||
width: '80px',
|
||||
align: 'center' as const,
|
||||
render: (record: DnsRecordType) => (
|
||||
<span>{record.priority || '-'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'نوع رکورد',
|
||||
key: 'type',
|
||||
title: t('domain.table.ttl'),
|
||||
key: 'ttl',
|
||||
width: '80px',
|
||||
align: 'center' as const,
|
||||
render: (record: DnsRecordType) => (
|
||||
<span>{record.ttl}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'وضعیت',
|
||||
title: t('domain.table.status'),
|
||||
key: 'status',
|
||||
render: (record: DNSRecord) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{record.status === 'verified' && (
|
||||
<>
|
||||
<TickCircle size={20} color="#22C55E" variant="Bold" />
|
||||
<span className="text-green-500 text-xs">ثبت شده</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
render: (record: DnsRecordType) => getStatusIcon(record.status)
|
||||
},
|
||||
]
|
||||
|
||||
const dnsRecords = data?.data?.dnsRecords || []
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex xl:flex-row gap-4 flex-col justify-between mt-9'>
|
||||
<div>
|
||||
<div className='xl:text-lg'>
|
||||
{t('setting.record_dns')}
|
||||
</div>
|
||||
<p className='mt-2 xl:text-sm text-xs font-extralight'>
|
||||
{t('setting.record_dns_description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Input
|
||||
placeholder={t('setting.your_domain')}
|
||||
className='xl:w-[300px]'
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='w-fit xl:px-14 px-8 border border-black'
|
||||
label={t('setting.submit')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateDomain />
|
||||
|
||||
<div className='mt-9'>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={dnsRecords}
|
||||
className="!mt-0"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import Button from '@/components/Button'
|
||||
import Input from '@/components/Input'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCreateDomain, useGetDomains } from '../hooks/useDomainData'
|
||||
import { ErrorType } from '@/helpers/types'
|
||||
|
||||
const CreateDomain: FC = () => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { mutate: createDomain, isPending } = useCreateDomain()
|
||||
const { data: domains } = useGetDomains()
|
||||
const [domain, setDomain] = useState('')
|
||||
const [isVerified, setIsVerified] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (domains?.data?.domain) {
|
||||
setDomain(domains?.data?.domain?.name)
|
||||
if (domains?.data?.domain?.isVerified) {
|
||||
setIsVerified(true)
|
||||
}
|
||||
}
|
||||
}, [domains])
|
||||
|
||||
|
||||
const handleCreateDomain = () => {
|
||||
createDomain({
|
||||
name: domain,
|
||||
notes: domain
|
||||
}, {
|
||||
onSuccess: (data) => {
|
||||
toast(data.message, 'success')
|
||||
setDomain('')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className='flex xl:flex-row gap-4 flex-col justify-between mt-9 items-end'>
|
||||
<div>
|
||||
<div className='xl:text-lg'>
|
||||
{t('setting.record_dns')}
|
||||
</div>
|
||||
<p className='mt-2 xl:text-sm text-xs font-extralight'>
|
||||
{t('setting.record_dns_description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Input
|
||||
placeholder={t('setting.your_domain')}
|
||||
className='xl:w-[300px]'
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
readOnly={isVerified}
|
||||
/>
|
||||
|
||||
{
|
||||
!isVerified &&
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='w-fit xl:px-14 px-8 border border-black'
|
||||
label={t('setting.submit')}
|
||||
onClick={handleCreateDomain}
|
||||
loading={isPending}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateDomain
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { CreateDomainType } from "../types/Types";
|
||||
import * as api from "../service/DomainService";
|
||||
|
||||
export const useCreateDomain = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateDomainType) => api.createDomain(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetDomains = () => {
|
||||
return useQuery({
|
||||
queryKey: ["domains"],
|
||||
queryFn: () => api.getDomains(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetDnsRecords = (disabled?: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: ["dns-records"],
|
||||
queryFn: () => api.getDnsRecords(),
|
||||
refetchInterval: 3000,
|
||||
enabled: !disabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const useVerifyDnsRecord = () => {
|
||||
return useQuery({
|
||||
queryKey: ["verify-dns-record"],
|
||||
queryFn: () => api.verifyDnsRecord(),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import axios from "@/config/axios";
|
||||
import { CreateDomainType } from "../types/Types";
|
||||
|
||||
export const createDomain = async (params: CreateDomainType) => {
|
||||
const { data } = await axios.post(`/domains`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getDomains = async () => {
|
||||
const { data } = await axios.get(`/domains`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getDnsRecords = async () => {
|
||||
const { data } = await axios.get(`/domains/dns-records`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const verifyDnsRecord = async () => {
|
||||
const { data } = await axios.get(`/domains/verify-dns`);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IResponse } from "@/types/response.types";
|
||||
|
||||
export type CreateDomainType = {
|
||||
name: string;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
export type DnsRecordType = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
wildduckId: string | null;
|
||||
name: string;
|
||||
type: "MX" | "SPF" | "DMARC" | "DKIM" | "TXT" | "A" | "AAAA" | "CNAME";
|
||||
value: string;
|
||||
ttl: number;
|
||||
priority: number | null;
|
||||
status: "PENDING" | "VERIFIED" | "FAILED";
|
||||
isRequired: boolean;
|
||||
isActive: boolean;
|
||||
lastVerifiedAt: string | null;
|
||||
lastCheckedAt: string | null;
|
||||
description: string;
|
||||
errorMessage: string | null;
|
||||
verificationAttempts: number;
|
||||
nextVerificationAt: string | null;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export type DnsRecordResponseType = IResponse<{
|
||||
dnsRecords: DnsRecordType[];
|
||||
}>;
|
||||
Reference in New Issue
Block a user