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

@@ -18,6 +18,7 @@ import { Toaster } from "react-hot-toast";
import { IsAuthenticated } from "./utils/auth";
import { HelmetProvider } from "react-helmet-async";
import InActivePayment from "./pages/inActivePayment";
import AdvancedVerification from "./pages/advancedVerification";
import { useSelector } from "react-redux";
import { RootState } from "./redux/store";
import Page404 from "./pages/404";
@@ -72,6 +73,10 @@ const App = () => {
isPaymentGateway ? <Navigate to="/" /> : <InActivePayment />
}
/>
<Route
path="/advanced-verification"
element={<AdvancedVerification />}
/>
<Route path="*" element={<Page404 />} />
</Routes>
</HelmetProvider>

View File

@@ -35,10 +35,10 @@ export default function MuiModal({
onClose={handleCloseCustom || handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
className="relative w-full h-full flex justify-center items-center !border-none !outline-none !ring-0"
className="relative w-full h-full flex justify-center items-center !border-none !outline-none !ring-0 overflow-y-auto p-4"
>
<motion.div
className="max-md:w-72 md:w-80 lg:w-96 min-h-80 shadow-xl shadow-[rgb(20,20,20)] bg-[rgb(20,20,20,0.99)] rounded-lg p-4 relative flex flex-col justify-between items-center gap-4"
className="max-md:w-72 md:w-80 lg:w-96 max-h-[90vh] shadow-xl shadow-[rgb(20,20,20)] bg-[rgb(20,20,20,0.99)] rounded-lg p-4 relative flex flex-col justify-between items-center gap-4 my-auto overflow-y-auto"
initial={{ y: "-100vw", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5 }}
@@ -54,7 +54,7 @@ export default function MuiModal({
<IoClose className="w-7 h-7" />
</button>
</motion.div>
<motion.div className="w-full h-full flex flex-col justify-between text-center items-center">
<motion.div className="w-full flex-1 flex flex-col justify-between text-center items-center overflow-y-auto">
{children}
</motion.div>
<motion.div

View File

@@ -5,27 +5,28 @@ const Dropdown = ({ isOpen, setIsOpen, children }: DropdownProps) => {
const dropdownRef = useRef<HTMLDivElement>(null);
const childrenArray = Children.toArray(children);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [setIsOpen]);
if (childrenArray.length !== 2) {
console.error("ِDropDown باید دقیقاً دو فرزند داشته باشد.");
return null;
}
const [ButtonChild, DropDownChild] = childrenArray;
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div className="relative inline-block text-left" ref={dropdownRef}>

View File

@@ -4,8 +4,8 @@ import { ProgressSpaceProps } from "../../types/props";
const ProgressSpace = ({ startSpace, endSpace }: ProgressSpaceProps) => {
let startSpacePercentage;
let remaining;
let startSpaceToNumber = Number(startSpace);
let endSpaceToNumber = Number(endSpace);
const startSpaceToNumber = Number(startSpace);
const endSpaceToNumber = Number(endSpace);
const isMaxSpace = startSpaceToNumber >= endSpaceToNumber;

View File

@@ -14,16 +14,15 @@ const LandingLayout = () => {
if (location.hash === "#shoppingConfig") {
navigate("/shopping-config");
}
}, [location]);
}, [location, navigate]);
const { configs } = useParams<{ configs: string }>();
const dispatch = useDispatch();
console.log(configs)
useEffect(() => {
if (configs === "1" || configs === "2" || configs === "3") {
dispatch(setConfigId(configs))
}
}, [configs]);
}, [configs, dispatch]);
return (
<>

View File

@@ -35,7 +35,7 @@ const Layout = () => {
dispatch(setShopId(shopId));
navigate("/");
}
}, [shopId, dispatch]);
}, [shopId, dispatch, navigate]);
// useEffect(() => {
// toast.custom(

View File

@@ -0,0 +1,91 @@
import { BiArrowBack, BiShieldQuarter } from "react-icons/bi";
import { FaTelegram } from "react-icons/fa6";
import { useNavigate } from "react-router-dom";
import Button from "../../../components/button";
import {
ADVANCED_VERIFICATION_PRICE_LIMIT,
ADVANCED_VERIFICATION_SUPPORT_URL,
buildAdvancedVerificationMessage,
formatToman,
} from "../constants";
interface AdvancedVerificationNoticeProps {
payablePrice?: number;
limit?: number;
}
const AdvancedVerificationNotice = ({
payablePrice,
limit = ADVANCED_VERIFICATION_PRICE_LIMIT,
}: AdvancedVerificationNoticeProps) => {
const navigate = useNavigate();
const hasOrderDetails = Boolean(payablePrice || limit);
const message = buildAdvancedVerificationMessage(limit);
const orderDetailsText = [
payablePrice ? `مبلغ سفارش شما ${formatToman(payablePrice)} است.` : "",
limit
? `خریدهای با مبلغ بیش از ${formatToman(limit)} نیاز به احراز هویت پیشرفته دارند.`
: "",
]
.filter(Boolean)
.join(" ");
return (
<div className="w-full max-w-2xl glass p-5 md:p-7 rounded-lg border border-yellow-400/20 shadow-2xl shadow-yellow-950/20">
<div className="w-full flex flex-col items-center gap-6 text-center">
<div className="relative flex justify-center items-center">
<span className="absolute w-28 h-28 rounded-full bg-yellow-400/10" />
<span className="absolute w-20 h-20 rounded-full bg-yellow-400/15" />
<BiShieldQuarter className="relative w-20 h-20 text-yellow-300" />
</div>
<div className="flex flex-col gap-2">
<p className="text-xs md:text-sm text-yellow-300">
نیاز به احراز هویت پیشرفته
</p>
<h1 className="text-xl md:text-2xl font-bold">
ادامه پرداخت این سفارش فعلا ممکن نیست
</h1>
</div>
<p className="max-w-xl text-sm md:text-base text-gray-200 leading-8">
{message}
</p>
{hasOrderDetails && (
<div className="w-full rounded-md bg-gray-900/60 p-4 border border-white/5 text-sm text-start">
<p className="text-gray-400">جزئیات سفارش</p>
<p className="mt-2 text-yellow-300 font-bold leading-7">
{orderDetailsText}
</p>
</div>
)}
<div className="w-full flex flex-col sm:flex-row gap-3">
<Button extra="!flex-1 !bg-[#229ED9] !text-white hover:!bg-[#1b8bc0]">
<a
href={ADVANCED_VERIFICATION_SUPPORT_URL}
target="_blank"
rel="noopener noreferrer"
className="w-full h-full flex justify-center items-center gap-2"
>
<FaTelegram className="w-6 h-6" />
<span>ارتباط با پشتیبانی تلگرام</span>
</a>
</Button>
<Button
extra="!flex-1 !bg-gray-800 !text-white hover:!bg-gray-700"
onClick={() => navigate("/")}
>
<span className="w-full flex justify-center items-center gap-2">
<BiArrowBack className="w-5 h-5" />
بازگشت به فروشگاه
</span>
</Button>
</div>
</div>
</div>
);
};
export default AdvancedVerificationNotice;

View File

@@ -0,0 +1,37 @@
export const ADVANCED_VERIFICATION_PRICE_LIMIT = 20000000;
export const ADVANCED_VERIFICATION_ROUTE = "/advanced-verification";
export const ADVANCED_VERIFICATION_SUPPORT_URL =
"https://t.me/PersianPingSupport";
const persianDigits: Record<string, string> = {
"0": "۰",
"1": "۱",
"2": "۲",
"3": "۳",
"4": "۴",
"5": "۵",
"6": "۶",
"7": "۷",
"8": "۸",
"9": "۹",
};
const toPersianDigits = (value: string): string => {
return value.replace(/\d/g, (digit) => persianDigits[digit]);
};
const separateByThree = (num: number): string => {
return (num ?? 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
export const formatToman = (amount: number): string => {
return `${toPersianDigits(separateByThree(amount))} تومان`;
};
export const buildAdvancedVerificationMessage = (
limit = ADVANCED_VERIFICATION_PRICE_LIMIT
): string => {
return `به‌دلیل برخی پروتکل‌های امنیتی و به‌منظور جلوگیری از سوءاستفاده، برای خریدهای با مبلغ بیش از ${formatToman(
limit
)}، احراز هویت پیشرفته انجام می‌شود، لطفا برای ادامه فرایند با پشتیبانی تلگرام به نشانی ${ADVANCED_VERIFICATION_SUPPORT_URL} در ارتباط باشید.`;
};

View File

@@ -0,0 +1,35 @@
import { Helmet } from "react-helmet-async";
import { useLocation } from "react-router-dom";
import Particle from "../../components/particle";
import AdvancedVerificationNotice from "./components/AdvancedVerificationNotice";
interface AdvancedVerificationLocationState {
payablePrice?: number;
limit?: number;
}
const AdvancedVerification = () => {
const location = useLocation();
const state = (location.state || {}) as AdvancedVerificationLocationState;
return (
<>
<Helmet>
<title>احراز هویت پیشرفته | پرشین پینگ</title>
<meta
name="description"
content="برای تکمیل خریدهای نیازمند احراز هویت پیشرفته با پشتیبانی پرشین پینگ در ارتباط باشید."
/>
</Helmet>
<Particle />
<div className="relative w-full min-h-screen bg-noisy flex justify-center items-center p-5">
<AdvancedVerificationNotice
payablePrice={state.payablePrice}
limit={state.limit}
/>
</div>
</>
);
};
export default AdvancedVerification;

View File

@@ -16,28 +16,29 @@ import callof2 from "../../../../../assets/product-image/callof/cp-callofduty360
import callof3 from "../../../../../assets/product-image/callof/cp-callofduty880.png";
import callof4 from "../../../../../assets/product-image/callof/cp-callofduty2400.png";
const ListAllProduct = ({ activeId }: { activeId: number }) => {
const allProducts = [
[
{ id: 1, title: "پابجی 1", image: pubg1 },
{ id: 2, title: "پابجی 2", image: pubg2 },
{ id: 3, title: "پابجی 3", image: pubg3 },
{ id: 4, title: "پابجی 4", image: pubg4 },
],
[
{ id: 1, title: "کالاف 1", image: callof1 },
{ id: 2, title: "کالاف 2", image: callof2 },
{ id: 3, title: "کالاف 3", image: callof3 },
{ id: 4, title: "کالاف 4", image: callof4 },
],
const allProducts = [
[
{ id: 1, title: "پابجی 1", image: pubg1 },
{ id: 2, title: "پابجی 2", image: pubg2 },
{ id: 3, title: "پابجی 3", image: pubg3 },
{ id: 4, title: "پابجی 4", image: pubg4 },
],
[
{ id: 1, title: "کالاف 1", image: callof1 },
{ id: 2, title: "کالاف 2", image: callof2 },
{ id: 3, title: "کالاف 3", image: callof3 },
{ id: 4, title: "کالاف 4", image: callof4 },
],
[
{ id: 1, title: "جم کلش 1", image: clash1 },
{ id: 2, title: "جم کلش 2", image: clash2 },
{ id: 3, title: "جم کلش 3", image: clash3 },
{ id: 4, title: "جم کلش 4", image: clash4 },
],
];
[
{ id: 1, title: "جم کلش 1", image: clash1 },
{ id: 2, title: "جم کلش 2", image: clash2 },
{ id: 3, title: "جم کلش 3", image: clash3 },
{ id: 4, title: "جم کلش 4", image: clash4 },
],
];
const ListAllProduct = ({ activeId }: { activeId: number }) => {
const [product, setProduct] = useState(allProducts[activeId]);
useEffect(() => {
setProduct(allProducts[activeId]);

View File

@@ -1,5 +1,6 @@
import ProfileDetails from "./section/ProfileDetails";
import ProductConfigList from "./section/ProductConfigList";
import TelegramBanner from "./section/TelegramBanner";
import { useQueryParams } from "../../hooks/useQueryParams";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
@@ -17,23 +18,22 @@ const Profile = () => {
const [message, setMessage] = useState<string>("");
useEffect(() => {
if (success) {
if(success === "1"){
setIsOpen(true)
setIsPayment(true)
setMessage("موفقیت آمیز")
}else if(success === "0"){
setIsOpen(true)
setIsPayment(false)
setMessage("خطا")
}else{
toast.error("متاسفانه کد پرداخت به درستی دریافت نشد")
}
} else {
console.error("Track ID is missing or invalid.");
if (success === null) {
return;
}
}, []);
if (success === "1") {
setIsOpen(true);
setIsPayment(true);
setMessage("موفقیت آمیز");
} else if (success === "0") {
setIsOpen(true);
setIsPayment(false);
setMessage("خطا");
} else {
toast.error("متاسفانه کد پرداخت به درستی دریافت نشد");
}
}, [success]);
const { data, isLoading, isFetching, error } = useQuery({
queryKey: ["profile"],
@@ -58,6 +58,7 @@ const Profile = () => {
isFetching={isFetching}
error={error}
/>
<TelegramBanner />
<ProductConfigList
data={data?.data}
userTitle={data?.data?.result?.user_profile?.name}

View File

@@ -16,12 +16,15 @@ const ProductConfigList = ({
error,
userTitle,
}: ProfileDetailsProps) => {
const configs = data?.result?.configs;
const showInitialLoading = (isLoading || isFetching) && !data;
return (
<div className="w-full h-full p-5 glass flex flex-col gap-3">
<ProfileImage />
<div className="w-full h-full flex justify-center items-center">
<Button extra="!py-3 max-md:!py-1 max-md:px-2 !bg-gray-800 !text-white">
{isLoading || isFetching ? (
{showInitialLoading ? (
<LoaderIcon className="m-1.5" />
) : error ? (
"error"
@@ -31,7 +34,7 @@ const ProductConfigList = ({
</Button>
</div>
{data?.result?.configs?.length !== 0 && (
{configs?.length !== 0 && (
<p className="max-md:text-lg text-xl font-bold text-center">
لیست کانفیگ ها
</p>
@@ -40,7 +43,7 @@ const ProductConfigList = ({
className="w-full h-full p-3 max-sm:px-0 flex flex-col gap-3
justify-center items-center"
>
{isLoading || isFetching ? (
{showInitialLoading ? (
Array(3)
.fill(null)
.map((_, i) => (
@@ -52,7 +55,7 @@ const ProductConfigList = ({
<Alert type="danger" message="خطایی با این موضوع رخ داد: ">
{error.message}
</Alert>
) : data?.result?.configs?.length === 0 ? (
) : configs?.length === 0 ? (
<Link to={'/configs/2'} className="w-full h-full flex flex-col gap-3 justify-center items-center">
<IoAlertCircleSharp className="max-md:w-[3.5rem] max-md:h-[3.5rem] w-[5rem] h-[5rem]" />
<div
@@ -66,19 +69,27 @@ const ProductConfigList = ({
</div>
</Link>
) : (
data?.result?.configs?.map((item) => {
configs?.map((item, index) => {
return (
<Fragment key={item.id}>
<Fragment key={`${item.id}-${index}`}>
<ConfigItem
id={String(item.id)}
orderId={item.order_id}
configName={item.title}
dateBuy={item.created_at || "تاریخ نامعتبر"}
isDisabled={item.is_expired}
isShowTraffic={item.show_traffic}
textCopy={item.url}
type={item.type}
username={item.username}
categoryId={item.category_id}
useMaxTraffic={item.use_max_traffic}
primaryIpv4={item.primary_ipv4}
secondaryIpv4={item.secondary_ipv4}
primaryIpv6={item.primary_ipv6}
secondaryIpv6={item.secondary_ipv6}
dnsIpPending={item.dns_ip_pending}
dnsRegistrationExpired={item.dns_registration_expired}
/>
</Fragment>
);

View File

@@ -0,0 +1,24 @@
import { FaTelegram } from "react-icons/fa";
const TELEGRAM_CHANNEL = "https://t.me/PersianPing";
const TelegramBanner = () => {
return (
<a
href={TELEGRAM_CHANNEL}
target="_blank"
rel="noopener noreferrer"
className="w-full flex items-center justify-between gap-3 glass rounded-xl px-4 py-3 text-sm md:text-base hover:brightness-110 transition-all"
>
<div className="flex items-center gap-2 text-[#29A8EB]">
<FaTelegram className="text-2xl md:text-3xl shrink-0" />
<span className="font-bold">عضویت در کانال تلگرام</span>
</div>
<p className="text-gray-400 text-xs md:text-sm text-right">
برای دریافت آخرین اخبار و اطلاعیه‌ها حتماً در کانال تلگرام ما عضو باشید.
</p>
</a>
);
};
export default TelegramBanner;

View File

@@ -1,7 +1,8 @@
import { IoCopy } from "react-icons/io5";
import { IoCopy, IoDownload } from "react-icons/io5";
import Button from "../../../../components/button";
import { ConfigItemProps } from "../../../../types/props";
import ModalConfigDetails from "./ModalConfigDetails";
import ModalDnsConfig from "./ModalDnsConfig";
import { useEffect, useState } from "react";
import Dropdown, { DropdownItem } from "../../../../components/dropdown";
import ProgressSpace from "../../../../components/progressSpace";
@@ -12,9 +13,11 @@ import { setCategoryId } from "../../../../redux/slices/categoryId";
import { useQuery } from "@tanstack/react-query";
import useAxios from "../../../../hooks/useAxios";
import LoaderSpaceConfig from "./LoaderSpaceConfig";
import { downloadWireGuardConfig } from "../../utils/wireguardConfig";
const ConfigItem = ({
id,
orderId,
dateBuy,
configName,
isDisabled,
@@ -22,9 +25,17 @@ const ConfigItem = ({
textCopy,
type,
categoryId,
useMaxTraffic
useMaxTraffic,
username: configUsername,
primaryIpv4,
secondaryIpv4,
primaryIpv6,
secondaryIpv6,
dnsIpPending,
dnsRegistrationExpired,
}: ConfigItemProps) => {
const [isOpen, setIsOpen] = useState(false);
const [isOpenDnsModal, setIsOpenDnsModal] = useState(false);
const [isOpenDropDown, setIsOpenDropDown] = useState(false);
const [configId, setConfigId] = useState("");
const dispatch = useDispatch();
@@ -49,12 +60,24 @@ const ConfigItem = ({
setIsOpen(true);
}
};
const configType = type ?? "";
const isWireGuard = configType.toLowerCase() === "wireguard";
const isDns = configType.toLowerCase() === "dns";
const downloadWireGuardFile = () => {
if (!textCopy) {
toast.error("کانفیگ وایرگارد موجود نیست");
return;
}
downloadWireGuardConfig(textCopy, configUsername);
toast.success("فایل کانفیگ دانلود شد");
};
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
useEffect(() => {
if (type === "gaming" && textCopy) {
if (configType === "gaming" && textCopy) {
const [user, pass] = textCopy.split(",");
setUsername(user);
setPassword(pass);
@@ -62,7 +85,7 @@ const ConfigItem = ({
setUsername("");
setPassword("");
}
}, [textCopy, type]);
}, [configType, textCopy]);
const queryUpdateConfig = useQuery({
queryKey: ["updateConfig", isOpenDropDown || isOpen],
@@ -98,6 +121,7 @@ const ConfigItem = ({
type={type}
username={username}
password={password}
wireguardUsername={configUsername}
categoryId={categoryId}
isLoadSpaceConfig={
queryUpdateConfig.isLoading || queryUpdateConfig.isFetching
@@ -105,6 +129,20 @@ const ConfigItem = ({
error={queryUpdateConfig?.error ? queryUpdateConfig?.error : null}
useMaxTraffic={useMaxTraffic}
/>
{isDns && (
<ModalDnsConfig
isOpen={isOpenDnsModal}
setIsOpen={setIsOpenDnsModal}
nameConfig={configName}
orderId={orderId}
primaryIpv4={primaryIpv4}
secondaryIpv4={secondaryIpv4}
primaryIpv6={primaryIpv6}
secondaryIpv6={secondaryIpv6}
dnsIpPending={dnsIpPending}
dnsRegistrationExpired={dnsRegistrationExpired}
/>
)}
<div className="relative w-full h-full rounded-md">
{isDisabled || useMaxTraffic && (
<div className="absolute inset-0 flex items-center justify-center bg-opacity-50 rounded-md backdrop-blur-sm z-10">
@@ -129,7 +167,14 @@ const ConfigItem = ({
{configName}
</li>
<div className="max-md:hidden flex flex-row-reverse gap-2 justify-end items-center">
{type === "gaming" ? (
{isDns ? (
<Button
onClick={() => setIsOpenDnsModal(true)}
extra="!m-0 !px-2 !py-1.5 shadow-md shadow-white"
>
مدیریت DNS
</Button>
) : configType === "gaming" ? (
<>
<Button
onClick={() => {
@@ -153,16 +198,28 @@ const ConfigItem = ({
</Button>
</>
) : (
<Button
onClick={() => {
setCopyText(textCopy);
copyToClipboard(textCopy);
}}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
>
کپی
<IoCopy />
</Button>
<>
{isWireGuard ? (
<Button
onClick={downloadWireGuardFile}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
>
دانلود فایل
<IoDownload />
</Button>
) : (
<Button
onClick={() => {
setCopyText(textCopy);
copyToClipboard(textCopy);
}}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
>
کپی
<IoCopy />
</Button>
)}
</>
)}
<Button
extra="!m-0 !px-2 !py-1.5 shadow-md"
@@ -206,16 +263,26 @@ const ConfigItem = ({
</Dropdown>
)}
</div>
<div className="max-[400px]:w-full md:hidden flex flex-row-reverse gap-2 justify-end items-center">
<Button
extra="!m-0 !px-2 max-[400px]:w-full !py-1 shadow-md !font-bold !text-black text-lg"
onClick={() => {
setConfigId(id);
setIsOpen(true);
}}
>
مشاهده مشخصات سرویس
</Button>
<div className="w-full md:hidden flex flex-row-reverse gap-2 justify-end items-stretch">
{isDns && (
<Button
extra="!m-0 flex-1 !px-2 !py-2 shadow-md !font-bold text-sm whitespace-nowrap"
onClick={() => setIsOpenDnsModal(true)}
>
مدیریت DNS
</Button>
)}
{!isDns && (
<Button
extra="!m-0 flex-1 !px-2 !py-2 shadow-md !font-bold !text-black text-sm whitespace-nowrap"
onClick={() => {
setConfigId(id);
setIsOpen(true);
}}
>
مشخصات سرویس
</Button>
)}
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction, useState } from "react";
import MuiModal from "../../../../common/modal";
import Button from "../../../../components/button";
import { IoCopy } from "react-icons/io5";
import { IoCopy, IoDownload } from "react-icons/io5";
import Alert from "../../../../components/alert/Alert";
import ProgressSpace from "../../../../components/progressSpace";
import toast from "react-hot-toast";
@@ -11,6 +11,7 @@ import { setCategoryId } from "../../../../redux/slices/categoryId";
import Input from "../../../../components/input";
import LoaderSpaceConfig from "./LoaderSpaceConfig";
import { AxiosError } from "axios";
import { downloadWireGuardConfig } from "../../utils/wireguardConfig";
const ModalConfigDetails = ({
isOpen,
@@ -25,6 +26,7 @@ const ModalConfigDetails = ({
type,
username,
password,
wireguardUsername,
categoryId,
isLoadSpaceConfig,
error,
@@ -39,18 +41,20 @@ const ModalConfigDetails = ({
textCopy: string;
maxTraffic: number | "unlimited";
traffic: number;
type: string;
type: string | null;
username: string;
password: string;
wireguardUsername?: string | null;
categoryId: number;
isLoadSpaceConfig: boolean;
error: AxiosError | any;
error: AxiosError | Error | null;
useMaxTraffic: boolean;
}) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [_, setCopyText] = useState<string>("");
const [isCopyError, setIsCopyError] = useState(false);
const configType = type ?? "";
const isWireGuard = configType.toLowerCase() === "wireguard";
const copyToClipboard = async (textToCopy: string) => {
try {
@@ -62,6 +66,16 @@ const ModalConfigDetails = ({
toast.error(`Failed to copy: , ${err}`);
}
};
const downloadWireGuardFile = () => {
if (!textCopy) {
toast.error("کانفیگ وایرگارد موجود نیست");
return;
}
downloadWireGuardConfig(textCopy, wireguardUsername);
toast.success("فایل کانفیگ دانلود شد");
};
return (
<MuiModal
isOpenModal={isOpen}
@@ -89,7 +103,7 @@ const ModalConfigDetails = ({
</div>
) : (
<>
{type === "gaming" ? (
{configType === "gaming" ? (
<>
{isCopyError ? (
<div className="w-full h-full flex flex-col gap-2">
@@ -112,7 +126,6 @@ const ModalConfigDetails = ({
<>
<Button
onClick={() => {
setCopyText(password);
copyToClipboard(password);
}}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
@@ -122,7 +135,6 @@ const ModalConfigDetails = ({
</Button>
<Button
onClick={() => {
setCopyText(username);
copyToClipboard(username);
}}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
@@ -151,16 +163,27 @@ const ModalConfigDetails = ({
</div>
</>
) : (
<Button
onClick={() => {
setCopyText(textCopy);
copyToClipboard(textCopy);
}}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
>
کپی
<IoCopy />
</Button>
<>
{isWireGuard ? (
<Button
onClick={downloadWireGuardFile}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
>
دانلود فایل
<IoDownload />
</Button>
) : (
<Button
onClick={() => {
copyToClipboard(textCopy);
}}
extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white"
>
کپی
<IoCopy />
</Button>
)}
</>
)}
</>
)}

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;

View File

@@ -0,0 +1,71 @@
const WIREGUARD_FIELDS = [
"Address",
"PrivateKey",
"DNS",
"MTU",
"PublicKey",
"AllowedIPs",
"Endpoint",
];
export const formatWireGuardConfig = (config: string) => {
const fieldPattern = new RegExp(
`\\s+(${WIREGUARD_FIELDS.join("|")})\\s*=`,
"g"
);
const lines = config
.replace(/\r\n?/g, "\n")
.replace(/\s*(\[(?:Interface|Peer)\])\s*/g, "\n$1\n")
.replace(fieldPattern, "\n$1 =")
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const formattedLines = lines.reduce<string[]>((result, line) => {
if (line.startsWith("[") && result.length > 0) {
result.push("");
}
result.push(line);
return result;
}, []);
return `${formattedLines.join("\n")}\n`;
};
export const getWireGuardConfigFileName = (
username: string | null | undefined
) => {
const randomTwoDigitNumber = Math.floor(Math.random() * 90) + 10;
const defaultName = `persianping${randomTwoDigitNumber}`;
const baseName = (username?.trim() || defaultName)
.trim()
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, "-");
const safeName = baseName || defaultName;
return safeName.toLowerCase().endsWith(".conf")
? safeName
: `${safeName}.conf`;
};
export const downloadWireGuardConfig = (
config: string,
username: string | null | undefined
) => {
const fileName = getWireGuardConfigFileName(username);
const file = new Blob([formatWireGuardConfig(config)], {
type: "application/octet-stream",
});
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
};

View File

@@ -60,6 +60,14 @@ const ListRules = () => {
نخواهد شد.
</p>
</li>
<li className="flex flex-row mt-2 gap-2 w-full bg-gray-800 p-2 rounded-md items-center">
<img src={Warning} alt="warning" className="w-[2rem]" />
<p className="font-normal max-sm:text-xs max-md:text-sm md:text-lg text-start">
به‌دلیل برخی پروتکل‌های امنیتی و به‌منظور جلوگیری از سوءاستفاده،
برای خریدهای با مبلغ بیش از ۲۰ میلیون تومان، احراز هویت پیشرفته
انجام می‌شود.
</p>
</li>
<li className="flex flex-row mt-2 gap-2 w-full bg-gray-800 p-2 rounded-md items-center">
<img src={Warning} alt="warning" className="w-[2rem]" />
<p className="font-normal max-sm:text-xs max-md:text-sm md:text-lg text-start">

View File

@@ -53,7 +53,10 @@ const ProductList = () => {
activeId={activeId}
setActiveId={setActiveId}
/>
<ListAllProduct activeId={activeId} />
<ListAllProduct
activeId={activeId}
categories={data?.data?.result?.categories}
/>
</div>
</div>
);

View File

@@ -9,11 +9,25 @@ import { RootState } from "../../../../../redux/store";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
const ListAllProduct = ({ activeId }: { activeId: number }) => {
interface Category {
id: number | string;
label?: string;
}
const ListAllProduct = ({
activeId,
categories,
}: {
activeId: number;
categories?: Category[];
}) => {
const axios = useAxios();
const [selectedConfigId, setSelectedConfigId] = useState<number | null>(null);
const { token } = useSelector((state: RootState) => state.token);
const navigate = useNavigate();
const isDnsCategory =
categories?.find((category) => Number(category.id) === activeId)?.label?.toLowerCase() ===
"dns";
const { data, error, isLoading, isFetching } = useQuery({
queryKey: ["configs", activeId],
queryFn: () => {
@@ -70,6 +84,7 @@ const ListAllProduct = ({ activeId }: { activeId: number }) => {
isOpen={selectedConfigId !== null}
setIsOpen={(open) => { if (!open) setSelectedConfigId(null); }}
configId={selectedConfigId}
isDns={isDnsCategory}
/>
)}
<div

View File

@@ -8,21 +8,33 @@ import toast from "react-hot-toast";
import Alert from "../../../../../components/alert/Alert";
import { useFormik } from "formik";
import Input from "../../../../../components/input";
import { BiMinus, BiPlus } from "react-icons/bi";
import { BiInfoCircle, BiMinus, BiPlus } from "react-icons/bi";
import ModalDnsServiceInfo from "./ModalDnsServiceInfo";
import { queryClient } from "../../../../../main";
import { AxiosError, AxiosResponse } from "axios";
import { useCallback, useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { debounce } from "@mui/material";
import { useDispatch } from "react-redux";
import { setIsPaymentGateway } from "../../../../../redux/slices/isPaymentGateway";
import { useNavigate } from "react-router-dom";
import {
ADVANCED_VERIFICATION_PRICE_LIMIT,
ADVANCED_VERIFICATION_ROUTE,
buildAdvancedVerificationMessage,
} from "../../../../advancedVerification/constants";
const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
const axios = useAxios();
const dispatch = useDispatch();
const navigate = useNavigate();
const separateByThree = (num: number): string => {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const ModalBuy = ({ isOpen, setIsOpen, configId, isDns }: ModalBuyProps) => {
const axios = useAxios();
const dispatch = useDispatch();
const navigate = useNavigate();
const separateByThree = (num: number): string => {
return (num ?? 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const toNumber = (value: unknown): number => {
const normalizedValue =
typeof value === "string" ? value.replace(/,/g, "") : value;
const parsedValue = Number(normalizedValue);
return Number.isFinite(parsedValue) ? parsedValue : 0;
};
const { data, error, isLoading, isFetching, refetch } = useQuery({
queryKey: ["configSee", configId],
@@ -32,29 +44,59 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
enabled: isOpen,
});
const [price, setPrice] = useState(0);
const [isInfoOpen, setIsInfoOpen] = useState(false);
const payablePrice = toNumber(
price === 0 ? data?.data?.result?.price || 0 : price
);
const advancedVerificationLimit = toNumber(
data?.data?.result?.advanced_verification_limit ??
ADVANCED_VERIFICATION_PRICE_LIMIT
);
const advancedVerificationMessage =
buildAdvancedVerificationMessage(advancedVerificationLimit);
const advancedVerificationFromApi =
data?.data?.result?.requires_advanced_verification;
const hasAdvancedVerificationApiFlag =
advancedVerificationFromApi !== undefined &&
advancedVerificationFromApi !== null;
const requiresAdvancedVerification =
hasAdvancedVerificationApiFlag
? Boolean(advancedVerificationFromApi)
: payablePrice > advancedVerificationLimit;
const goToAdvancedVerification = () => {
setIsOpen(false);
navigate(ADVANCED_VERIFICATION_ROUTE, {
state: {
payablePrice,
limit: advancedVerificationLimit,
},
});
};
const formik = useFormik({
initialValues: {
count: 1,
discount: "",
status: "view",
},
validationSchema: Yup.object({
count: Yup.number().min(1, "حداقل یک کانفیگ باید باشد"),
discount: Yup.string(),
status: Yup.string(),
}),
initialValues: {
count: 1,
discount: "",
status: "view",
},
validationSchema: Yup.object({
count: Yup.number().min(1, "حداقل یک کانفیگ باید باشد"),
discount: Yup.string(),
status: Yup.string(),
}),
onSubmit: (values) => {
console.log(values);
},
});
const { setFieldValue } = formik;
useEffect(() => {
if (!isOpen) {
formik.setFieldValue("count", 1);
formik.setFieldValue("discount", "");
setPrice(0);
}
}, [isOpen]);
if (!isOpen) {
setFieldValue("count", 1);
setFieldValue("discount", "");
setPrice(0);
}
}, [isOpen, setFieldValue]);
const mutation = useMutation({
mutationKey: ["buyConfig"],
mutationFn: (values: BuyConfigDataProps) => {
@@ -81,7 +123,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
return `${res.data.alert}`;
},
error: (err: AxiosError) => {
formik.setFieldValue("discount", "");
setFieldValue("discount", "");
setPrice(0);
let errorMessage = "یک خطای غیرمنتظره رخ داد";
if (
err.response?.data &&
@@ -94,7 +137,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
},
});
};
const chnageCountDebouncedMutation = useCallback(
const chnageCountDebouncedMutation = useMemo(
() =>
debounce((values) => {
toast.promise(mutation.mutateAsync(values), {
loading: "درحال بررسی...",
@@ -103,7 +147,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
return `${res.data.alert}`;
},
error: (err: AxiosError) => {
formik.setFieldValue("discount", "");
setFieldValue("discount", "");
setPrice(0);
refetch();
let errorMessage = "یک خطای غیرمنتظره رخ داد";
if (
@@ -117,17 +162,17 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
},
});
}, 1000),
[]
[mutation, refetch, setFieldValue]
);
const chnageCountHandler = (status: "inc" | "dec") => {
if (status === "inc") {
formik.setFieldValue("count", formik.values.count + 1);
setFieldValue("count", formik.values.count + 1);
chnageCountDebouncedMutation({
...formik.values,
count: formik.values.count + 1,
});
} else if (status === "dec") {
formik.setFieldValue("count", formik.values.count - 1);
setFieldValue("count", formik.values.count - 1);
chnageCountDebouncedMutation({
...formik.values,
count: formik.values.count - 1,
@@ -136,7 +181,12 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
};
const submitHandler = () => {
console.log(formik.values);
if (requiresAdvancedVerification) {
goToAdvancedVerification();
return;
}
console.log(formik.values);
toast.promise(mutation.mutateAsync({ ...formik.values, status: "buy" }), {
loading: "درحال انتقال به درگاه پرداخت...",
success: (res: AxiosResponse) => {
@@ -144,31 +194,62 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
return `${res.data.alert}`;
},
error: (err: AxiosError) => {
formik.setFieldValue("discount", "");
let errorMessage = "یک خطای غیرمنتظره رخ داد";
if (
err.response?.data &&
typeof err.response.data === "object" &&
"alert" in err.response.data
) {
errorMessage = (err.response.data as { alert: string }).alert;
}
toast.error(`خطایی رخ داد : ${errorMessage}`);
dispatch(setIsPaymentGateway(false));
navigate("/noactive-payment");
return errorMessage;
},
setFieldValue("discount", "");
setPrice(0);
let errorMessage = "یک خطای غیرمنتظره رخ داد";
if (
err.response?.data &&
typeof err.response.data === "object" &&
"alert" in err.response.data
) {
errorMessage = (err.response.data as { alert: string }).alert;
}
const responseData = err.response?.data;
const isResponseObject =
responseData !== null && typeof responseData === "object";
const requiresAdvancedVerificationFromApi =
isResponseObject &&
(("requires_advanced_verification" in responseData &&
Boolean(
(responseData as { requires_advanced_verification?: boolean })
.requires_advanced_verification
)) ||
("code" in responseData &&
(responseData as { code?: string }).code ===
"ADVANCED_VERIFICATION_REQUIRED"));
const isAdvancedVerificationError =
requiresAdvancedVerificationFromApi ||
(err.response?.status === 422 &&
errorMessage.includes("احراز هویت پیشرفته"));
if (isAdvancedVerificationError) {
goToAdvancedVerification();
}
if (err.response?.status === 500) {
dispatch(setIsPaymentGateway(false));
navigate("/noactive-payment");
}
return errorMessage;
},
});
};
return (
<>
<ModalDnsServiceInfo isOpen={isInfoOpen} setIsOpen={setIsInfoOpen} />
<MuiModal
isOpenModal={isOpen}
setIsOpenModal={setIsOpen}
submitText={data?.data?.result?.max_config === 0 ? undefined : "خرید"}
submitOnClick={submitHandler}
disabled={isLoading || isFetching}
nameService={
submitText={data?.data?.result?.max_config === 0 ? undefined : "خرید"}
submitOnClick={submitHandler}
disabled={
isLoading ||
isFetching
}
nameService={
isLoading || isFetching
? "درحال بارگذاری..."
: data?.data?.result
@@ -212,6 +293,15 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
</div>
) : (
<div className="w-full h-full flex flex-col gap-3 justify-between items-center text-sm md:text-xl">
{isDns && (
<Button
extra="!m-0 !w-full !py-1.5 !bg-gray-800 !text-secondTheme border border-secondTheme/40 flex flex-row-reverse justify-center items-center gap-2 shadow-md text-sm md:text-base"
onClick={() => setIsInfoOpen(true)}
>
<BiInfoCircle className="w-5 h-5" />
توضیحات سرویس
</Button>
)}
<Alert type="danger">
{" "}
کاربر گرامی لطفا از خاموش بودن vpn خود اطمینان حاصل فرمایید تا در
@@ -224,11 +314,15 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
<h1 className="font-bold">
مبلغ قابل پرداخت :{" "}
<span className="px-[2px] rounded-md bg-secondTheme text-gray-950">
{separateByThree(
price === 0 ? data?.data?.result?.price || 0 : price
)}
{separateByThree(payablePrice)}
</span>
</h1>
{requiresAdvancedVerification &&
data?.data?.result?.max_config !== 0 && (
<Alert type="warning" extra="!mb-0 text-start">
{advancedVerificationMessage}
</Alert>
)}
{data?.data?.result?.max_config === 0 ? (
<>
<Alert type="info" extra="mt-5">
@@ -238,7 +332,7 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
</>
) : (
<>
<h3 className="text-xs md:text-sm text-gray-400">
<h3 className="text-xs md:text-sm text-gray-400 mt-2">
در صورت نداشتن کد تخفیف پرداخت رو بزنید
</h3>
<form
@@ -309,7 +403,11 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
</div>
<div className="w-full h-full bg-gray-900/50 rounded-md shadow flex justify-between items-center p-3">
<p>محدودیت کاربر</p>
<p>{data?.data?.result?.limit ?? "بدون محدودیت کاربر"}</p>
{!data?.data?.result?.limit ? (
<p>نامحدود</p>
) : (
<p>{data?.data?.result?.limit}</p>
)}
</div>
<div className="w-full h-full bg-gray-900/50 rounded-md shadow flex justify-between items-center p-3">
<p>مدت زمان</p>
@@ -318,8 +416,15 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
</div>
</div>
)}
{data?.data?.result?.max_config !== 0 && !isLoading && !isFetching && (
<p className="w-full text-center text-xs text-gray-500 mt-1">
با خرید، <span className="text-gray-400">قوانین و مقررات</span> را
می‌پذیرید.
</p>
)}
</div>
</MuiModal>
</>
);
};

View File

@@ -0,0 +1,142 @@
import { Dispatch, SetStateAction } from "react";
import MuiModal from "../../../../../common/modal";
interface ModalDnsServiceInfoProps {
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
const GAMES = [
"Valorant",
"Call of Duty: Warzone",
"EA SPORTS FC 25",
"EA SPORTS FC 26",
"PUBG Mobile",
"PUBG: Battlegrounds (PC)",
"Call of Duty: Mobile (CODM)",
"Fortnite",
"League of Legends (LoL)",
"Clash Royale",
"Clash of Clans",
];
const SERVICES = [
"ChatGPT",
"Discord",
"Spotify",
"NVIDIA (App & Services)",
"GitHub، Docker، NPM، PyPI و بسیاری از ابزارها و سرویس‌های توسعه (Developer Services)",
];
const STREAMING = ["YouTube", "Twitch", "Kick"];
const BENEFITS = [
"بدون نیاز به VPN",
"راه‌اندازی در کمتر از یک دقیقه",
"مناسب برای کامپیوتر، موبایل و کنسول بازی",
"پشتیبانی و بروزرسانی مداوم سرویس‌ها",
];
const Section = ({
emoji,
title,
children,
}: {
emoji: string;
title: string;
children: React.ReactNode;
}) => (
<div className="w-full flex flex-col gap-2 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">
<span>{emoji}</span>
<span>{title}</span>
</div>
{children}
</div>
);
const ModalDnsServiceInfo = ({ isOpen, setIsOpen }: ModalDnsServiceInfoProps) => {
return (
<MuiModal
isOpenModal={isOpen}
setIsOpenModal={setIsOpen}
title="توضیحات سرویس"
nameService="DNS"
>
<div className="w-full h-full flex flex-col gap-3 max-sm:text-xs max-md:text-sm text-base">
<Section emoji="🎮" title="بازی‌های پشتیبانی‌شده">
<ul className="flex flex-col gap-1.5">
{GAMES.map((game) => (
<li
key={game}
className="flex flex-row-reverse items-center gap-2 text-gray-200"
>
<span>🔹</span>
<span dir="ltr" className="text-start">
{game}
</span>
</li>
))}
</ul>
</Section>
<Section emoji="💻" title="سرویس‌ها و پلتفرم‌ها">
<ul className="flex flex-col gap-1.5">
{SERVICES.map((service) => (
<li
key={service}
className="flex flex-row-reverse items-start gap-2 text-gray-200"
>
<span className="shrink-0">✔️</span>
<span className="text-start leading-6">{service}</span>
</li>
))}
</ul>
</Section>
<Section emoji="📺" title="سرویس‌های استریم">
<ul className="flex flex-col gap-1.5">
{STREAMING.map((item) => (
<li
key={item}
className="flex flex-row-reverse items-center justify-between gap-2 text-gray-200"
>
<div className="flex flex-row-reverse items-center gap-2">
<span>▶️</span>
<span dir="ltr">{item}</span>
</div>
<span className="text-[10px] md:text-xs text-yellow-300/90 whitespace-nowrap">
فقط روی اینترنت خانگی
</span>
</li>
))}
</ul>
</Section>
<Section emoji="⚡️" title="مزایای سرویس">
<ul className="flex flex-col gap-1.5">
{BENEFITS.map((benefit) => (
<li
key={benefit}
className="flex flex-row-reverse items-center gap-2 text-gray-200"
>
<span>✅</span>
<span>{benefit}</span>
</li>
))}
</ul>
</Section>
<div className="w-full flex flex-row-reverse items-start gap-2 bg-secondTheme/10 border border-secondTheme/30 text-secondTheme rounded-lg p-3">
<span className="shrink-0">📌</span>
<span className="text-start leading-6">
بازی و سرویس‌های جدید به‌صورت مداوم به DNS اضافه می‌شوند و این
لیست همواره در حال بروزرسانی است.
</span>
</div>
</div>
</MuiModal>
);
};
export default ModalDnsServiceInfo;

36
src/types/props.d.ts vendored
View File

@@ -121,14 +121,35 @@ export interface MuiModalProps {
export interface ConfigItemProps {
id: string;
orderId?: number;
dateBuy: string;
configName: string;
isDisabled: boolean;
isShowTraffic: boolean;
textCopy: string;
type: string;
type: string | null;
categoryId: number;
useMaxTraffic: boolean;
username?: string | null;
primaryIpv4?: string | null;
secondaryIpv4?: string | null;
primaryIpv6?: string | null;
secondaryIpv6?: string | null;
dnsIpPending?: boolean;
dnsRegistrationExpired?: boolean;
}
export interface ModalDnsConfigProps {
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
nameConfig: string;
orderId?: number;
primaryIpv4?: string | null;
secondaryIpv4?: string | null;
primaryIpv6?: string | null;
secondaryIpv6?: string | null;
dnsIpPending?: boolean;
dnsRegistrationExpired?: boolean;
}
// نوع Item برای آیتم‌های داخل Dropdown
@@ -178,6 +199,7 @@ interface ModalBuyProps {
// unknown
// >;
configId: number;
isDns?: boolean;
}
interface BuyConfigDataProps {
@@ -195,17 +217,25 @@ type UserProfile = {
// تعریف تایپ پیکربندی‌ها
type Config = {
id: number;
id: number | string;
order_id?: number;
title: string;
url: string;
created_at: string | null;
show_traffic: boolean;
max_traffic: number;
traffic: number;
type: string;
type: string | null;
username?: string | null;
category_id: number;
is_expired: boolean;
use_max_traffic: boolean;
primary_ipv4?: string | null;
secondary_ipv4?: string | null;
primary_ipv6?: string | null;
secondary_ipv6?: string | null;
dns_ip_pending?: boolean;
dns_registration_expired?: boolean;
};
// تعریف تایپ داده‌های برگشتی