feat: add new feature and fix bugs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -35,7 +35,7 @@ const Layout = () => {
|
||||
dispatch(setShopId(shopId));
|
||||
navigate("/");
|
||||
}
|
||||
}, [shopId, dispatch]);
|
||||
}, [shopId, dispatch, navigate]);
|
||||
|
||||
// useEffect(() => {
|
||||
// toast.custom(
|
||||
|
||||
@@ -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;
|
||||
37
src/pages/advancedVerification/constants.ts
Normal file
37
src/pages/advancedVerification/constants.ts
Normal 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} در ارتباط باشید.`;
|
||||
};
|
||||
35
src/pages/advancedVerification/index.tsx
Normal file
35
src/pages/advancedVerification/index.tsx
Normal 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;
|
||||
@@ -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]);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
24
src/pages/profile/section/TelegramBanner.tsx
Normal file
24
src/pages/profile/section/TelegramBanner.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
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;
|
||||
71
src/pages/profile/utils/wireguardConfig.ts
Normal file
71
src/pages/profile/utils/wireguardConfig.ts
Normal 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);
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
@@ -53,7 +53,10 @@ const ProductList = () => {
|
||||
activeId={activeId}
|
||||
setActiveId={setActiveId}
|
||||
/>
|
||||
<ListAllProduct activeId={activeId} />
|
||||
<ListAllProduct
|
||||
activeId={activeId}
|
||||
categories={data?.data?.result?.categories}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
36
src/types/props.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
// تعریف تایپ دادههای برگشتی
|
||||
|
||||
Reference in New Issue
Block a user