feat: add new feature and fix bugs
This commit is contained in:
239
src/pages/profile/section/components/ModalDnsConfig.tsx
Normal file
239
src/pages/profile/section/components/ModalDnsConfig.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user