feat: add new feature and fix bugs

This commit is contained in:
2026-08-13 16:38:49 +03:30
parent 9c9450ffca
commit 9fd8543b7e
23 changed files with 1076 additions and 168 deletions

View File

@@ -0,0 +1,239 @@
import { useState } from "react";
import toast from "react-hot-toast";
import { IoCopyOutline, IoWifiOutline } from "react-icons/io5";
import { MdOutlineDns } from "react-icons/md";
import { FaBolt } from "react-icons/fa6";
import { BsShieldExclamation } from "react-icons/bs";
import { useMutation } from "@tanstack/react-query";
import { AxiosError } from "axios";
import MuiModal from "../../../../common/modal";
import Button from "../../../../components/button";
import Input from "../../../../components/input";
import Alert from "../../../../components/alert/Alert";
import { ModalDnsConfigProps } from "../../../../types/props";
import useAxios from "../../../../hooks/useAxios";
import { queryClient } from "../../../../main";
const IP_ADDRESS_REGEX =
/^(((\d{1,3}\.){3}\d{1,3})|([0-9a-fA-F:]+:+[0-9a-fA-F:]+))$/;
const CLIENT_IP_ENDPOINTS = [
{
url: "https://api64.ipify.org?format=json",
parse: async (res: Response) => (await res.json())?.ip,
},
{
url: "https://api.ipify.org?format=json",
parse: async (res: Response) => (await res.json())?.ip,
},
{
url: "https://icanhazip.com",
parse: async (res: Response) => (await res.text()).trim(),
},
];
const getErrorMessage = (err: AxiosError) => {
const responseData = err.response?.data;
if (
responseData &&
typeof responseData === "object" &&
"alert" in responseData
) {
return (responseData as { alert: string }).alert;
}
return "یک خطای غیرمنتظره رخ داد";
};
const ModalDnsConfig = ({
isOpen,
setIsOpen,
nameConfig,
orderId,
primaryIpv4,
secondaryIpv4,
primaryIpv6,
secondaryIpv6,
dnsIpPending,
dnsRegistrationExpired,
}: ModalDnsConfigProps) => {
const axios = useAxios();
const [ip, setIp] = useState("");
const [isDetecting, setIsDetecting] = useState(false);
const copyToClipboard = async (label: string, textToCopy?: string | null) => {
if (!textToCopy) return;
try {
await navigator.clipboard.writeText(textToCopy);
toast.success(`${label} کپی شد!`);
} catch (err) {
toast.error(`کپی انجام نشد: ${err}`);
}
};
const registerIpMutation = useMutation({
mutationKey: ["registerDnsIp", orderId],
mutationFn: (ipToRegister: string) => {
return axios.post(`/profile/dns/${orderId}/register-ip`, {
ip: ipToRegister,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["profile"] });
},
});
const registerIp = async (ipToRegister: string) => {
const normalizedIp = ipToRegister.trim();
if (!orderId) {
toast.error("شناسه سفارش نامعتبر است");
return;
}
if (!IP_ADDRESS_REGEX.test(normalizedIp)) {
toast.error("آی‌پی وارد شده معتبر نیست");
return;
}
setIp(normalizedIp);
try {
await toast.promise(registerIpMutation.mutateAsync(normalizedIp), {
loading: "درحال ذخیره آی‌پی...",
success: (res) => res?.data?.alert || "آی‌پی با موفقیت ذخیره شد",
error: (err: AxiosError) => getErrorMessage(err),
});
} catch {
return;
}
};
const handleAutoDetectIp = async () => {
setIsDetecting(true);
try {
for (const endpoint of CLIENT_IP_ENDPOINTS) {
try {
const res = await fetch(endpoint.url, { cache: "no-store" });
if (!res.ok) continue;
const detectedIp = await endpoint.parse(res);
if (detectedIp && IP_ADDRESS_REGEX.test(detectedIp)) {
setIp(detectedIp);
toast.success("آی‌پی کاربر دریافت شد");
return;
}
} catch {
continue;
}
}
toast.error("دریافت آی‌پی خودکار ممکن نشد");
} catch (err) {
toast.error(`دریافت آی‌پی خودکار با خطا مواجه شد: ${err}`);
} finally {
setIsDetecting(false);
}
};
const ipRows: { label: string; badge: string; value?: string | null }[] = [
{ label: "DNS نسل 4", badge: "v4", value: primaryIpv4 },
{ label: "DNS نسل 4", badge: "v4", value: secondaryIpv4 },
{ label: "DNS نسل 6", badge: "v6", value: primaryIpv6 },
{ label: "DNS نسل 6", badge: "v6", value: secondaryIpv6 },
].filter((row) => row.value);
return (
<MuiModal isOpenModal={isOpen} setIsOpenModal={setIsOpen} nameService={nameConfig}>
<div className="w-full h-full flex flex-col gap-4 max-sm:text-xs max-md:text-sm text-base">
{dnsRegistrationExpired && (
<Alert type="danger" extra="!mb-0 !rounded-lg">
مهلت ثبت آی‌پی برای این سرویس به پایان رسیده است
</Alert>
)}
{dnsIpPending && !dnsRegistrationExpired && (
<Alert type="warning" extra="!mb-0 !rounded-lg">
ثبت آی‌پی این سرویس در انتظار تایید است
</Alert>
)}
{ipRows.length > 0 && (
<div className="w-full flex flex-col gap-2">
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
<MdOutlineDns className="w-4 h-4" />
<span>آدرس‌های DNS</span>
</div>
<div className="w-full flex flex-col gap-2">
{ipRows.map((row, index) => (
<div
key={`${row.badge}-${row.value}-${index}`}
className="group w-full flex flex-col gap-1.5 bg-gray-900/60 border border-gray-700 hover:border-secondTheme/50 transition-colors rounded-lg p-2.5"
>
<div className="w-full flex flex-row-reverse items-center justify-between gap-2">
<div className="flex flex-row-reverse items-center gap-2 min-w-0">
<span className="shrink-0 text-[10px] md:text-xs font-bold px-2 py-0.5 rounded-full bg-secondTheme/10 text-secondTheme border border-secondTheme/30">
{row.badge}
</span>
<span className="text-gray-300 whitespace-nowrap">{row.label}</span>
</div>
<button
onClick={() => copyToClipboard(row.label, row.value)}
className="shrink-0 p-2 rounded-md bg-gray-800 text-gray-300 hover:bg-secondTheme hover:text-gray-900 transition-colors"
title="کپی"
>
<IoCopyOutline className="w-4 h-4" />
</button>
</div>
<div
className="w-full text-start break-all font-mono text-secondTheme/90"
dir="ltr"
>
{row.value}
</div>
</div>
))}
</div>
</div>
)}
<div className="w-full flex flex-row-reverse items-start gap-2 bg-yellow-500/10 border border-yellow-500/30 text-yellow-300 rounded-lg p-3">
<BsShieldExclamation className="w-8 h-8 md:w-5 md:h-5 shrink-0 mt-0.5" />
<span className="text-start leading-6">
در صورت روشن بودن فیلترشکن (VPN)، برای ثبت صحیح آی‌پی، لطفا ابتدا آن را خاموش کنید.
</span>
</div>
<div className="w-full flex flex-col gap-2.5 bg-gray-900/60 border border-gray-700 rounded-lg p-3">
<div className="flex flex-row-reverse items-center gap-1.5 text-secondTheme font-bold">
<IoWifiOutline className="w-4 h-4" />
<span>ثبت آی‌پی</span>
</div>
<div className="w-full flex flex-row-reverse gap-2">
<div className="flex-1 min-w-0">
<Input
type="text"
dir="ltr"
placeholder="آی‌پی خود را وارد کنید"
value={ip}
onChange={(e) => setIp(e.target.value)}
extra="!text-start"
/>
</div>
<Button
onClick={() => registerIp(ip)}
disabled={registerIpMutation.isPending || !ip.trim()}
extra="!m-0 !px-3 shadow-md shrink-0 disabled:!opacity-50"
>
ذخیره آی‌پی
</Button>
</div>
<Button
onClick={handleAutoDetectIp}
disabled={isDetecting || registerIpMutation.isPending}
extra="!m-0 !px-3 !bg-secondTheme !text-gray-900 font-bold shadow-md shadow-secondTheme/30 hover:!bg-secondTheme/80 flex flex-row-reverse justify-center items-center gap-2 disabled:!opacity-50"
>
<FaBolt className={`w-4 h-4 ${isDetecting ? "animate-pulse" : ""}`} />
{isDetecting ? "در حال تشخیص..." : "دریافت آی‌پی کاربر"}
</Button>
</div>
</div>
</MuiModal>
);
};
export default ModalDnsConfig;