domain compelete api attachment

This commit is contained in:
hamid zarghami
2025-07-03 12:31:49 +03:30
parent 658fa14a8a
commit 47feb1472e
30 changed files with 1109 additions and 179 deletions
+27 -2
View File
@@ -9,6 +9,9 @@ import i18next from 'i18next'
import FaJson from './langs/fa.json'
import { I18nextProvider } from 'react-i18next'
import NewMessage from './components/newMessage/NewMessage';
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { IApiErrorRepsonse } from './types/error.types';
import ToastContainer from './components/Toast';
i18next.init({
interpolation: { escapeValue: false },
lng: 'fa',
@@ -20,12 +23,34 @@ i18next.init({
}
})
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: async (error: IApiErrorRepsonse) => {
if (error?.response?.statusCode === 401) {
console.log(401);
}
}
}),
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false
// staleTime: 86400000 // 1 day in milliseconds
}
}
});
const App: FC = () => {
return (
<BrowserRouter>
<I18nextProvider i18n={i18next}>
<Main />
<NewMessage />
<QueryClientProvider client={queryClient}>
<Main />
<NewMessage />
<ToastContainer />
</QueryClientProvider>
</I18nextProvider>
</BrowserRouter>
)
+4 -4
View File
@@ -1,5 +1,7 @@
import { FC, ReactNode } from 'react'
import { clx } from '../helpers/utils'
import MoonLoader from "react-spinners/ClipLoader"
interface ButtonProps {
label?: string;
@@ -28,10 +30,8 @@ const Button: FC<ButtonProps> = (props) => {
className={buttonClass}
>
{props.loading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-3 h-3 md:w-4 md:h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
درحال بارگذاری...
</div>
<MoonLoader className='mt-1.5' size={15} color={props.variant === 'secondary' ? '#000' : '#fff'} />
) : (
props.children || props.label
)}
+72
View File
@@ -0,0 +1,72 @@
import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react';
import React, { useState, useEffect } from 'react';
interface Toast {
id: string;
message: string;
type?: 'success' | 'error' | 'info';
isExiting?: boolean;
}
let addToast: (toast: Toast) => void;
const ToastContainer: React.FC = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
// ثبت toast جدید
const showToast = (toast: Toast) => {
setToasts((prev) => [...prev, toast]);
// حذف خودکار بعد از ۳ ثانیه
setTimeout(() => {
setToasts((prev) => prev.map(t =>
t.id === toast.id ? { ...t, isExiting: true } : t
));
// حذف از DOM بعد از اتمام انیمیشن
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== toast.id));
}, 300); // مدت زمان انیمیشن خروج
}, 3000);
};
// تخصیص تابع نمایش toast به متغیر سراسری
useEffect(() => {
addToast = showToast;
}, []);
return (
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
{toasts.map((toast) => (
<div
key={toast.id}
className={`px-4 flex items-center gap-2 backdrop-blur-2xl h-16 min-w-[300px] rounded-2xl shadow-md
${toast.isExiting ? 'animate-slide-out-right' : 'animate-slide-in-right'} ${toast.type === 'success'
? 'bg-white/70 text-black'
: toast.type === 'error'
? 'bg-white/70 text-black'
: 'bg-white/70 text-black'
}`}
style={{
animationFillMode: 'forwards',
animationDuration: '0.3s',
animationTimingFunction: 'ease-out'
}}
>
{
toast.type === 'success' ? <TickCircle className='size-5' color='green' /> :
toast.type === 'error' ? <CloseCircle className='size-5' color='red' /> :
<InfoCircle className='size-5' color='blue' />
}
{toast.message}
</div>
))}
</div>
);
};
export const toast = (message?: string, type: 'success' | 'error' | 'info' = 'info') => {
addToast({ id: Date.now().toString(), message: message || '', type });
};
export default ToastContainer;
+25
View File
@@ -0,0 +1,25 @@
import axios from "axios";
import { getToken } from "./func";
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
});
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.request.use(async function (config) {
const token = getToken();
config.headers.Authorization = "Bearer " + token;
config.headers["x-business-id"] = localStorage.getItem(
import.meta.env.VITE_WORKSPACE_ID
);
return config;
});
export default axiosInstance;
+22
View File
@@ -0,0 +1,22 @@
import axios from "axios";
import { getToken } from "./func";
const axiosDanak = axios.create({
baseURL: import.meta.env.VITE_DANAK_BASE_URL,
});
axiosDanak.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error);
}
);
axiosDanak.interceptors.request.use(async function (config) {
const token = getToken();
config.headers.Authorization = "Bearer " + token;
return config;
});
export default axiosDanak;
+61
View File
@@ -0,0 +1,61 @@
import Cookie from "js-cookie";
export function isEmail(input: string): boolean {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(input);
}
export function NumberFormat(number: number): string {
return Intl.NumberFormat("fa-IR").format(number);
}
export const getToken = () => {
return Cookie.get(import.meta.env.VITE_TOKEN_NAME);
};
export const getRefreshToken = () => {
return Cookie.get(import.meta.env.VITE_REFRESH_TOKEN_NAME);
};
const isProduction = import.meta.env.MODE === "production";
const domain = isProduction ? ".danakcorp.com" : undefined;
export const setToken = async (token: string) => {
Cookie.set(import.meta.env.VITE_TOKEN_NAME, token, { domain });
};
export const setRefreshToken = async (refreshToken: string) => {
Cookie.set(import.meta.env.VITE_REFRESH_TOKEN_NAME, refreshToken, {
domain,
});
};
export const removeToken = async () => {
Cookie.remove(import.meta.env.VITE_TOKEN_NAME, { domain, path: "/" });
};
export const removeRefreshToken = async () => {
Cookie.remove(import.meta.env.VITE_REFRESH_TOKEN_NAME, { domain, path: "/" });
};
export const timeAgo = (date: string | Date): string => {
const now = new Date();
const past = new Date(date);
const diff = Math.floor((now.getTime() - past.getTime()) / 1000); // اختلاف بر حسب ثانیه
const units = [
{ name: "سال", value: 60 * 60 * 24 * 365 },
{ name: "ماه", value: 60 * 60 * 24 * 30 },
{ name: "روز", value: 60 * 60 * 24 },
{ name: "ساعت", value: 60 * 60 },
{ name: "دقیقه", value: 60 },
{ name: "ثانیه", value: 1 },
];
for (const unit of units) {
const amount = Math.floor(diff / unit.value);
if (amount >= 1) return `${amount} ${unit.name} پیش`;
}
return "همین الان";
};
+50
View File
@@ -0,0 +1,50 @@
import axios, { AxiosRequestConfig } from "axios";
import { getToken } from "./func";
const orvalAxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
});
orvalAxiosInstance.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error);
}
);
orvalAxiosInstance.interceptors.request.use(async (config) => {
const token = getToken();
config.headers["Content-Type"] = "application/json";
config.headers["Accept"] = "application/json";
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
const businessId = localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID);
if (businessId) {
config.headers["x-business-id"] = businessId;
}
return config;
});
export const customAxiosInstance = <T = unknown>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig
): Promise<T> => {
const source = axios.CancelToken.source();
const promise = orvalAxiosInstance({
...config,
...options,
cancelToken: source.token,
}).then(({ data }) => data);
// @ts-expect-error - Adding cancel method to promise
promise.cancel = () => {
source.cancel("Query was cancelled");
};
return promise;
};
+15
View File
@@ -118,6 +118,21 @@
"description_sign": "توضیحات امضا",
"select_section": "لطفا یک بخش انتخاب کنید"
},
"domain": {
"table": {
"record_type": "نوع رکورد",
"record_name": "نام رکورد",
"record_value": "مقدار رکورد",
"priority": "اولویت",
"ttl": "TTL",
"status": "وضعیت"
},
"status": {
"verified": "تایید شده",
"pending": "در انتظار",
"failed": "ناموفق"
}
},
"select_file": "انتخاب فایل",
"upload": "آپلود",
"new_message": {
-25
View File
@@ -1,25 +0,0 @@
{
"sidebar": {
"menu": "منو",
"received": "دریافتی‌ها",
"sent": "ارسال شده‌ها",
"draft": "پیش‌نویس‌ها",
"archive": "آرشیو",
"trash": "سطل زباله",
"other": "سایر",
"favorite": "مورد علاقه",
"spam": "اسپم",
"setting": "تنظیمات",
"logout": "خروج",
"new_message": "پیام جدید"
},
"new_message": {
"title": "پیام جدید",
"to": "به:",
"recipients_placeholder": "گیرندگان را وارد کنید",
"subject": "موضوع:",
"subject_placeholder": "موضوع پیام را وارد کنید",
"message_placeholder": "متن پیام خود را بنویسید...",
"send": "ارسال"
}
}
View File
+73 -26
View File
@@ -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
+9
View File
@@ -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,
});
};
+8
View File
@@ -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;
};
+48
View File
@@ -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[];
};
+1 -1
View File
@@ -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
View File
@@ -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;
};
+33
View File
@@ -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[];
}>;
+2 -1
View File
@@ -8,10 +8,11 @@ import DraftList from '@/pages/draft/List'
import ArchiveList from '@/pages/archive/List'
import TrashList from '@/pages/Trash/List'
import DetailEmail from '@/pages/received/Detail'
import Home from '@/pages/home/Home'
const AppRouter: FC = () => {
return (
<Routes>
<Route path='/' element={<ReceivedList />} />
<Route path='/' element={<Home />} />
<Route path={Paths.received} element={<ReceivedList />} />
<Route path={Paths.setting} element={<Setting />} />
<Route path={Paths.sent} element={<SentList />} />
+14
View File
@@ -0,0 +1,14 @@
import { DefaultError } from "@tanstack/react-query";
import { IBaseResponse } from "./response.types";
interface IError {
message: string[];
}
export interface IErrorResponse extends IBaseResponse {
error: IError;
}
export interface IApiErrorRepsonse extends DefaultError {
response?: IErrorResponse;
}
+7
View File
@@ -0,0 +1,7 @@
export interface IBaseResponse {
statusCode: number;
success: boolean;
}
export interface IResponse<T> extends IBaseResponse {
data: T;
}