Files
persian-ping-frontend/src/pages/profile/section/components/ModalDnsConfig.tsx

240 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;