Compare commits
11 Commits
255a48ae99
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a1b03acf04 | |||
| 29d66a1621 | |||
| ae66644321 | |||
| e669ab84d9 | |||
| c4c115ccfc | |||
| 1ce33d9b74 | |||
| 800efeb784 | |||
| 9286a283c0 | |||
| bbf1fc8899 | |||
| e1a84105c7 | |||
| 897146ee12 |
12
Dockerfile
12
Dockerfile
@@ -1,12 +1,10 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM node:22-alpine AS build
|
||||
# node:22-alpine npm often dies with "Exit handler never called" in Docker builds.
|
||||
FROM docker.arvancloud.ir/node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci --no-audit --no-fund
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -16,10 +14,12 @@ ENV VITE_API_URL=${VITE_API_URL}
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:1.28-alpine AS runtime
|
||||
FROM docker.arvancloud.ir/nginxinc/nginx-unprivileged:1.28-alpine AS runtime
|
||||
|
||||
COPY --chown=nginx:nginx nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build --chown=nginx:nginx /app/dist /usr/share/nginx/html
|
||||
COPY --from=build --chown=nginx:nginx /app/robots.txt /usr/share/nginx/html/robots.txt
|
||||
COPY --from=build --chown=nginx:nginx /app/sitemap.xml /usr/share/nginx/html/sitemap.xml
|
||||
|
||||
USER nginx
|
||||
|
||||
|
||||
8
robots.txt
Normal file
8
robots.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /profile
|
||||
Disallow: /login
|
||||
Disallow: /advanced-verification
|
||||
Disallow: /noactive-payment
|
||||
|
||||
Sitemap: https://persain-game.com/sitemap.xml
|
||||
27
sitemap.xml
Normal file
27
sitemap.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://persain-game.com/</loc>
|
||||
<lastmod>2026-11-02</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://persain-game.com/tutorial</loc>
|
||||
<lastmod>2026-11-02</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://persain-game.com/rule</loc>
|
||||
<lastmod>2026-11-02</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://persain-game.com/about</loc>
|
||||
<lastmod>2026-11-02</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -23,6 +23,7 @@ import { useSelector } from "react-redux";
|
||||
import { RootState } from "./redux/store";
|
||||
import Page404 from "./pages/404";
|
||||
import RedirectToHome from "./utils/redirect-to-home ";
|
||||
import SiteAnnouncementModal from "./components/siteAnnouncement/SiteAnnouncementModal";
|
||||
// import LandingLayout from "./layout/LandingLayout";
|
||||
// import Landing from "./pages/landing";
|
||||
|
||||
@@ -40,6 +41,7 @@ const App = () => {
|
||||
}}
|
||||
/>
|
||||
<ProgressBar />
|
||||
<SiteAnnouncementModal />
|
||||
<Routes>
|
||||
{/* <Route path="/" element={<LandingLayout />}>
|
||||
<Route index element={<Landing />}></Route>
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function MuiModal({
|
||||
submitOnClick,
|
||||
disabled,
|
||||
handleCloseCustom,
|
||||
aboveSubmit,
|
||||
children,
|
||||
}: MuiModalProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -57,22 +58,27 @@ export default function MuiModal({
|
||||
<motion.div className="w-full flex-1 flex flex-col justify-between text-center items-center overflow-y-auto">
|
||||
{children}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className={`w-full h-full flex ${
|
||||
submitText ? "justify-end" : "justify-center"
|
||||
} items-center gap-2 md:gap-3 text-lg md:text-xl`}
|
||||
>
|
||||
<Button onClick={handleCloseCustom || handleClose}> بستن </Button>
|
||||
{submitText && (
|
||||
<Button
|
||||
extra={`${disabled && "!bg-slate-300"}`}
|
||||
onClick={submitOnClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{" "}
|
||||
{submitText}{" "}
|
||||
</Button>
|
||||
<motion.div className="w-full h-full flex flex-col gap-2">
|
||||
{aboveSubmit && (
|
||||
<div className="w-full flex justify-center">{aboveSubmit}</div>
|
||||
)}
|
||||
<motion.div
|
||||
className={`w-full h-full flex ${
|
||||
submitText ? "justify-end" : "justify-center"
|
||||
} items-center gap-2 md:gap-3 text-lg md:text-xl`}
|
||||
>
|
||||
<Button onClick={handleCloseCustom || handleClose}> بستن </Button>
|
||||
{submitText && (
|
||||
<Button
|
||||
extra={`${disabled && "!bg-slate-300"}`}
|
||||
onClick={submitOnClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{" "}
|
||||
{submitText}{" "}
|
||||
</Button>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Particles, { initParticlesEngine } from "@tsparticles/react";
|
||||
import {
|
||||
type Container,
|
||||
type ISourceOptions,
|
||||
MoveDirection,
|
||||
OutMode,
|
||||
@@ -26,10 +25,6 @@ export default function Particle() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const particlesLoaded = async (container?: Container): Promise<void> => {
|
||||
console.log(container);
|
||||
};
|
||||
|
||||
const options: ISourceOptions = useMemo(
|
||||
() => ({
|
||||
background: {
|
||||
@@ -37,7 +32,9 @@ export default function Particle() {
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
fpsLimit: 60,
|
||||
fpsLimit: 30,
|
||||
pauseOnBlur: true,
|
||||
pauseOnOutsideViewport: true,
|
||||
interactivity: {
|
||||
events: {
|
||||
onClick: {
|
||||
@@ -69,20 +66,6 @@ export default function Particle() {
|
||||
enable: true,
|
||||
opacity: 0.8,
|
||||
width: 1,
|
||||
shadow: {
|
||||
enable: true,
|
||||
blur: 5,
|
||||
color: "#71fbb2",
|
||||
},
|
||||
warp: {
|
||||
enable: true,
|
||||
speed: 2,
|
||||
},
|
||||
animation: {
|
||||
enable: true,
|
||||
speed: 0.5,
|
||||
sync: true,
|
||||
},
|
||||
},
|
||||
move: {
|
||||
direction: MoveDirection.none,
|
||||
@@ -110,7 +93,7 @@ export default function Particle() {
|
||||
value: { min: 1, max: 5 },
|
||||
},
|
||||
},
|
||||
detectRetina: true,
|
||||
detectRetina: false,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
@@ -121,11 +104,7 @@ export default function Particle() {
|
||||
|
||||
if (init) {
|
||||
return (
|
||||
<Particles
|
||||
id="tsparticles"
|
||||
particlesLoaded={particlesLoaded}
|
||||
options={options}
|
||||
/>
|
||||
<Particles id="tsparticles" options={options} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
128
src/components/siteAnnouncement/SiteAnnouncementModal.tsx
Normal file
128
src/components/siteAnnouncement/SiteAnnouncementModal.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import Modal from "@mui/material/Modal";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaTelegram } from "react-icons/fa6";
|
||||
import { IoClose } from "react-icons/io5";
|
||||
import { MdCampaign } from "react-icons/md";
|
||||
import useAxios from "../../hooks/useAxios";
|
||||
import Button from "../button";
|
||||
|
||||
type SiteAnnouncement = {
|
||||
enabled: boolean;
|
||||
title?: string;
|
||||
body?: string;
|
||||
support_url?: string;
|
||||
support_label?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "site_announcement_dismissed_version";
|
||||
|
||||
const SiteAnnouncementModal = () => {
|
||||
const axios = useAxios();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ["site-announcement"],
|
||||
queryFn: () => axios.get("/home/announcement"),
|
||||
});
|
||||
|
||||
const announcement = data?.data?.result as SiteAnnouncement | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!announcement?.enabled) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const dismissedVersion = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (dismissedVersion && dismissedVersion === String(announcement.version)) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
}, [announcement]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (announcement?.version) {
|
||||
sessionStorage.setItem(STORAGE_KEY, String(announcement.version));
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
if (!announcement?.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="site-announcement-title"
|
||||
aria-describedby="site-announcement-body"
|
||||
className="relative w-full h-full flex justify-center items-center p-4"
|
||||
>
|
||||
<motion.div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className="w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-2xl border border-secondTheme/20 bg-[rgb(18,18,18,0.98)] p-5 md:p-6 shadow-2xl shadow-black/50 outline-none"
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.28 }}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-secondTheme">
|
||||
<MdCampaign className="w-7 h-7 shrink-0" aria-hidden="true" />
|
||||
<h2
|
||||
id="site-announcement-title"
|
||||
className="text-xl md:text-2xl font-bold text-white"
|
||||
>
|
||||
{announcement.title}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
aria-label="بستن اطلاعیه"
|
||||
className="min-w-11 min-h-11 rounded-lg flex items-center justify-center text-gray-300 hover:text-white hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-secondTheme"
|
||||
>
|
||||
<IoClose className="w-7 h-7" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p
|
||||
id="site-announcement-body"
|
||||
className="mt-5 text-sm md:text-base leading-8 text-gray-200 whitespace-pre-line text-right"
|
||||
>
|
||||
{announcement.body}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col sm:flex-row gap-3">
|
||||
{announcement.support_url && (
|
||||
<a
|
||||
href={announcement.support_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-1 min-h-11 px-4 py-2 rounded-md bg-[#229ED9] text-white hover:bg-[#1b8bc0] focus-visible:ring-2 focus-visible:ring-sky-300 flex items-center justify-center gap-2"
|
||||
>
|
||||
<FaTelegram className="w-5 h-5" aria-hidden="true" />
|
||||
<span>
|
||||
Support: {announcement.support_label || "@PersianPingSupport"}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
<Button
|
||||
extra="!flex-1 !min-h-11 !bg-gray-800 !text-white hover:!bg-gray-700 focus-visible:ring-2 focus-visible:ring-secondTheme"
|
||||
onClick={handleClose}
|
||||
>
|
||||
متوجه شدم
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteAnnouncementModal;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { MdLogout } from "react-icons/md";
|
||||
import { MdEdit, MdLogout } from "react-icons/md";
|
||||
import Button from "../../../components/button";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setToken } from "../../../redux/slices/token";
|
||||
import { AxiosError } from "axios";
|
||||
import { LoaderIcon } from "react-hot-toast";
|
||||
import { ProfileDetailsProps } from "../../../types/props";
|
||||
import { useState } from "react";
|
||||
import ModalEditProfile from "./components/ModalEditProfile";
|
||||
|
||||
const ProfileDetails = ({
|
||||
data,
|
||||
@@ -14,7 +16,13 @@ const ProfileDetails = ({
|
||||
}: ProfileDetailsProps) => {
|
||||
return (
|
||||
<div className="relative z-[10] w-full min-h-32 max-md:min-h-20 flex flex-row-reverse max-[400px]:gap-3 justify-between flex-wrap-reverse glass items-center py-2 px-4">
|
||||
<EditProfile />
|
||||
<EditProfile
|
||||
identityVerified={data?.result?.user_profile?.is_identity_verified}
|
||||
nationalId={data?.result?.user_profile?.national_id}
|
||||
email={data?.result?.user_profile?.email}
|
||||
mobile={data?.result?.user_profile?.mobile}
|
||||
cardNumber={data?.result?.user_profile?.card_number}
|
||||
/>
|
||||
<ProductDetails
|
||||
enable_config={data?.result?.enable_config}
|
||||
disable_config={data?.result?.disable_config}
|
||||
@@ -28,18 +36,38 @@ const ProfileDetails = ({
|
||||
|
||||
export default ProfileDetails;
|
||||
|
||||
const EditProfile = () => {
|
||||
// const [isOpen, setIsOpen] = useState(false);
|
||||
const EditProfile = ({
|
||||
identityVerified,
|
||||
nationalId,
|
||||
email,
|
||||
mobile,
|
||||
cardNumber,
|
||||
}: {
|
||||
identityVerified?: boolean;
|
||||
nationalId?: string | null;
|
||||
email?: string | null;
|
||||
mobile?: string | null;
|
||||
cardNumber?: string | null;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<div className="max-[400px]:w-full w-auto h-full flex justify-end items-center gap-2">
|
||||
{/* <ModalEditProfile isOpen={isOpen} setIsOpen={setIsOpen} /> */}
|
||||
{/* <Button
|
||||
<ModalEditProfile
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
identityVerified={identityVerified}
|
||||
nationalId={nationalId}
|
||||
email={email}
|
||||
mobile={mobile}
|
||||
cardNumber={cardNumber}
|
||||
/>
|
||||
<Button
|
||||
extra="!py-4 max-md:!py-2 max-md:px-2"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<MdEdit className="scale-[1.4] max-md:scale-100" />
|
||||
</Button> */}
|
||||
</Button>
|
||||
<Button
|
||||
extra="!py-4 max-[400px]:w-full max-md:!py-2 max-md:px-2 !bg-rose-600 flex gap-1 justify-center items-center !text-white"
|
||||
onClick={() => dispatch(setToken(""))}
|
||||
|
||||
@@ -1,23 +1,329 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import MuiModal from "../../../../common/modal";
|
||||
import Input from "../../../../components/input";
|
||||
import Button from "../../../../components/button";
|
||||
import Alert from "../../../../components/alert/Alert";
|
||||
import useAxios from "../../../../hooks/useAxios";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import toast from "react-hot-toast";
|
||||
import { queryClient } from "../../../../main";
|
||||
|
||||
const extractErrorMessage = (err: AxiosError): string => {
|
||||
const data = err.response?.data;
|
||||
if (data && typeof data === "object") {
|
||||
if ("alert" in data && (data as { alert?: string }).alert) {
|
||||
return (data as { alert: string }).alert;
|
||||
}
|
||||
if ("message" in data && (data as { message?: string }).message) {
|
||||
return (data as { message: string }).message;
|
||||
}
|
||||
}
|
||||
return "یک خطای غیرمنتظره رخ داد";
|
||||
};
|
||||
|
||||
const ModalEditProfile = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
nationalId,
|
||||
identityVerified = false,
|
||||
email,
|
||||
mobile,
|
||||
cardNumber,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
nationalId?: string | null;
|
||||
identityVerified?: boolean;
|
||||
email?: string | null;
|
||||
mobile?: string | null;
|
||||
cardNumber?: string | null;
|
||||
}) => {
|
||||
const axios = useAxios();
|
||||
|
||||
const [nationalCode, setNationalCode] = useState(nationalId ?? "");
|
||||
const [emailValue, setEmailValue] = useState(email ?? "");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||
const [cardNumberValue, setCardNumberValue] = useState(cardNumber ?? "");
|
||||
|
||||
const [newMobile, setNewMobile] = useState("");
|
||||
const [mobileStep, setMobileStep] = useState<"idle" | "code-sent">("idle");
|
||||
const [mobileCode, setMobileCode] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setNationalCode(nationalId ?? "");
|
||||
setEmailValue(email ?? "");
|
||||
setPassword("");
|
||||
setPasswordConfirmation("");
|
||||
setCardNumberValue(cardNumber ?? "");
|
||||
setNewMobile("");
|
||||
setMobileStep("idle");
|
||||
setMobileCode("");
|
||||
}, [isOpen, nationalId, email, cardNumber]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationKey: ["saveProfile"],
|
||||
mutationFn: async () => {
|
||||
const requests = [];
|
||||
|
||||
if (!identityVerified && nationalCode.trim() && nationalCode.trim() !== (nationalId ?? "")) {
|
||||
requests.push(
|
||||
axios.post("/profile/national-id", {
|
||||
national_id: nationalCode.trim(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const updatePayload: Record<string, string> = {};
|
||||
if (emailValue.trim() !== (email ?? "")) {
|
||||
updatePayload.email = emailValue.trim();
|
||||
}
|
||||
if (password) {
|
||||
updatePayload.password = password;
|
||||
updatePayload.password_confirmation = passwordConfirmation;
|
||||
}
|
||||
if (cardNumberValue.trim() !== (cardNumber ?? "")) {
|
||||
updatePayload.card_number = cardNumberValue.trim();
|
||||
}
|
||||
|
||||
if (Object.keys(updatePayload).length > 0) {
|
||||
requests.push(axios.post("/profile/update", updatePayload));
|
||||
}
|
||||
|
||||
if (requests.length === 0) {
|
||||
throw new Error("هیچ تغییری برای ذخیره وجود ندارد");
|
||||
}
|
||||
|
||||
return Promise.all(requests);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
});
|
||||
|
||||
const submitHandler = () => {
|
||||
if (!identityVerified && nationalCode.trim() && nationalCode.trim().length !== 10) {
|
||||
toast.error("کد ملی باید ۱۰ رقم باشد");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password && password.length < 6) {
|
||||
toast.error("رمز عبور باید حداقل ۶ کاراکتر باشد");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password && password !== passwordConfirmation) {
|
||||
toast.error("تکرار رمز عبور مطابقت ندارد");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(saveMutation.mutateAsync(), {
|
||||
loading: "درحال ذخیره...",
|
||||
success: () => {
|
||||
setIsOpen(false);
|
||||
return "اطلاعات با موفقیت ذخیره شد";
|
||||
},
|
||||
error: (err: AxiosError | Error) => {
|
||||
if (err instanceof Error && !("response" in err)) {
|
||||
return err.message;
|
||||
}
|
||||
return extractErrorMessage(err as AxiosError);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const requestMobileCodeMutation = useMutation({
|
||||
mutationKey: ["requestMobileChange"],
|
||||
mutationFn: (mobileNumber: string) => {
|
||||
return axios.post("/profile/mobile/request-change", {
|
||||
mobile: mobileNumber,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const confirmMobileCodeMutation = useMutation({
|
||||
mutationKey: ["confirmMobileChange"],
|
||||
mutationFn: (code: string) => {
|
||||
return axios.post("/profile/mobile/confirm-change", { code });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
});
|
||||
|
||||
const requestMobileCodeHandler = () => {
|
||||
if (newMobile.trim().length !== 11) {
|
||||
toast.error("شماره موبایل باید ۱۱ رقم باشد");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(requestMobileCodeMutation.mutateAsync(newMobile.trim()), {
|
||||
loading: "درحال ارسال کد...",
|
||||
success: (res: AxiosResponse) => {
|
||||
setMobileStep("code-sent");
|
||||
return `${res.data.alert}`;
|
||||
},
|
||||
error: (err: AxiosError) => extractErrorMessage(err),
|
||||
});
|
||||
};
|
||||
|
||||
const confirmMobileCodeHandler = () => {
|
||||
if (mobileCode.trim().length !== 4) {
|
||||
toast.error("کد تایید باید ۴ رقم باشد");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.promise(confirmMobileCodeMutation.mutateAsync(mobileCode.trim()), {
|
||||
loading: "درحال تایید...",
|
||||
success: (res: AxiosResponse) => {
|
||||
setMobileStep("idle");
|
||||
setNewMobile("");
|
||||
setMobileCode("");
|
||||
return `${res.data.alert}`;
|
||||
},
|
||||
error: (err: AxiosError) => extractErrorMessage(err),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiModal
|
||||
isOpenModal={isOpen}
|
||||
setIsOpenModal={setIsOpen}
|
||||
submitText="تایید"
|
||||
submitText="ذخیره"
|
||||
submitOnClick={submitHandler}
|
||||
title="ویرایش پروفایل"
|
||||
>
|
||||
<div className="relative w-full h-full px-2 md:px-4">
|
||||
<Input type="text" label="ویرایش نام :" labelStyle="!text-start" extra="!w-full" />
|
||||
<div className="relative w-full h-full px-2 md:px-4 flex flex-col gap-4">
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
label="شماره موبایل فعلی :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full opacity-60"
|
||||
value={mobile ?? ""}
|
||||
disabled
|
||||
/>
|
||||
{identityVerified ? (
|
||||
<Alert type="info" extra="!mb-0 text-start">
|
||||
شماره موبایل و کد ملی شما تأیید شدهاند و قابل تغییر نیستند.
|
||||
</Alert>
|
||||
) : mobileStep === "idle" ? (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
label="تغییر شماره موبایل :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full"
|
||||
placeholder="شماره موبایل جدید"
|
||||
value={newMobile}
|
||||
onChange={(e) =>
|
||||
setNewMobile(e.target.value.replace(/[^0-9]/g, ""))
|
||||
}
|
||||
/>
|
||||
{newMobile.trim().length === 11 && (
|
||||
<Button extra="!py-2" onClick={requestMobileCodeHandler}>
|
||||
ارسال کد تایید
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Alert type="warning" extra="!mb-0 text-start">
|
||||
کد تایید به شماره {newMobile} پیامک شد.
|
||||
</Alert>
|
||||
<Input
|
||||
type="text"
|
||||
label="کد تایید :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full"
|
||||
placeholder="کد ۴ رقمی"
|
||||
value={mobileCode}
|
||||
onChange={(e) =>
|
||||
setMobileCode(e.target.value.replace(/[^0-9]/g, ""))
|
||||
}
|
||||
/>
|
||||
<div className="w-full flex gap-2">
|
||||
<Button extra="!py-2" onClick={confirmMobileCodeHandler}>
|
||||
تایید و تغییر شماره
|
||||
</Button>
|
||||
<Button
|
||||
extra="!py-2 !bg-gray-600 !text-white"
|
||||
onClick={() => {
|
||||
setMobileStep("idle");
|
||||
setMobileCode("");
|
||||
}}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
label="ایمیل :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full"
|
||||
placeholder="ایمیل (اختیاری)"
|
||||
value={emailValue}
|
||||
onChange={(e) => setEmailValue(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Input
|
||||
type="password"
|
||||
label="رمز عبور جدید :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full"
|
||||
placeholder="در صورت تمایل به تغییر رمز پر کنید"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
{password && (
|
||||
<Input
|
||||
type="password"
|
||||
label="تکرار رمز عبور جدید :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full"
|
||||
placeholder="تکرار رمز عبور جدید"
|
||||
value={passwordConfirmation}
|
||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
label="شماره کارت :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full"
|
||||
placeholder="شماره کارت (اختیاری)"
|
||||
value={cardNumberValue}
|
||||
onChange={(e) =>
|
||||
setCardNumberValue(e.target.value.replace(/[^0-9]/g, ""))
|
||||
}
|
||||
/>
|
||||
|
||||
{!identityVerified && (
|
||||
<Alert type="warning" extra="!mb-0 text-start">
|
||||
کد ملی برای احراز هویت شاهکار در خریدهای با مبلغ بالا لازم است.
|
||||
</Alert>
|
||||
)}
|
||||
<Input
|
||||
type="text"
|
||||
label="کد ملی :"
|
||||
labelStyle="!text-start"
|
||||
extra="!w-full"
|
||||
placeholder="کد ملی ۱۰ رقمی"
|
||||
value={nationalCode}
|
||||
disabled={identityVerified}
|
||||
onChange={(e) =>
|
||||
setNationalCode(e.target.value.replace(/[^0-9]/g, ""))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</MuiModal>
|
||||
);
|
||||
|
||||
@@ -1,86 +1,63 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Warning from "../../../assets/gifs/warning.gif";
|
||||
import useAxios from "../../../hooks/useAxios";
|
||||
import Alert from "../../../components/alert/Alert";
|
||||
import Button from "../../../components/button";
|
||||
|
||||
type TermsContent = {
|
||||
title: string;
|
||||
introduction: string;
|
||||
notice: string;
|
||||
preamble: string;
|
||||
clauses: string[];
|
||||
};
|
||||
|
||||
const Rules = () => {
|
||||
const axios = useAxios();
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ["siteTerms"],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<{ result: TermsContent }>("/home/terms");
|
||||
return response.data.result;
|
||||
},
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <p role="status" className="p-4 text-center">در حال دریافت قوانین و مقررات...</p>;
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<div className="p-4 flex flex-col items-center gap-3">
|
||||
<Alert type="warning">دریافت قوانین و مقررات انجام نشد. دوباره تلاش کنید.</Alert>
|
||||
<Button onClick={() => refetch()}>تلاش دوباره</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col px-4 py-4 text-start">
|
||||
<h1 className="font-bold text-xl md:text-2xl text-center">
|
||||
قوانین و مقررات
|
||||
</h1>
|
||||
<p className="font-normal text-sm mt-2 md:text-lg text-start">
|
||||
لطفا با دقت زیاد متن توضیح زیر را بخوانید :<br />
|
||||
قوانین پرشین پینگ لطفا قبل از خریداری به نکاتی که در زیر گفته شده دقت
|
||||
کنید و با آگاهی به این شرایط خرید بفرمایید:
|
||||
</p>
|
||||
<p className="font-bold text-sm mt-2 md:text-lg text-yellow-500 text-start">
|
||||
این سرویس تابع قوانین ایران میباشد
|
||||
</p>
|
||||
<p className="font-normal text-sm mt-2 md:text-lg text-start">
|
||||
با تهیه و استفاده از اکانت پرشین پینگ میپذیرید:
|
||||
</p>
|
||||
<ListRules />
|
||||
<div className="relative w-full h-full flex flex-col px-4 py-4 text-start" dir="rtl">
|
||||
<h1 className="font-bold text-xl md:text-2xl text-center">{data.title}</h1>
|
||||
{data.introduction && (
|
||||
<p className="font-normal text-sm mt-2 md:text-lg text-start whitespace-pre-line">{data.introduction}</p>
|
||||
)}
|
||||
{data.notice && (
|
||||
<p className="font-bold text-sm mt-2 md:text-lg text-yellow-500 text-start whitespace-pre-line">{data.notice}</p>
|
||||
)}
|
||||
{data.preamble && (
|
||||
<p className="font-normal text-sm mt-2 md:text-lg text-start whitespace-pre-line">{data.preamble}</p>
|
||||
)}
|
||||
<ul className="w-full h-full mt-2">
|
||||
{data.clauses.map((clause, index) => (
|
||||
<li key={index} className="flex flex-row mt-2 gap-2 w-full bg-gray-800 p-2 rounded-md items-center">
|
||||
<img src={Warning} alt="" className="w-[2rem] shrink-0" />
|
||||
<p className="font-normal max-sm:text-xs max-md:text-sm md:text-lg text-start whitespace-pre-line">{clause}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rules;
|
||||
|
||||
const ListRules = () => {
|
||||
return (
|
||||
<div className="w-full h-full mt-2">
|
||||
<ul>
|
||||
<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">
|
||||
در صورتی که سرویس خریداری شده یک کاربره باشد و اگر سرویس خود را در
|
||||
اختیار شخص دیگری قرار دهید سرویس شما به صورت خودکار مسدود خواهد شد.{" "}
|
||||
</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">
|
||||
در صورتی که مشکلی برای سرویسها به وجود بیاید تمام سعی خود را
|
||||
میکنیم تا هر چه سریعتر مشکل را برطرف کنیم. بازه زمانی بین 10 دقیقه
|
||||
الی حداکثر 24 ساعت میباشد.{" "}
|
||||
</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">
|
||||
تمامی سرویسهای ما مخصوص بازی میباشد و برای کاهش پینگ در بازیهای
|
||||
آنلاین و عبور از تحریم بازیها استفاده میشود و هرگونه استفاده غیر
|
||||
مجاز از سرویسها به عنوان V.p.n و استفادههای دیگر ممنوع میباشد و
|
||||
در صورت مشاهده سرویس شما مسدود میگردد و هزینهای هم برگشت داده
|
||||
نخواهد شد.
|
||||
</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">
|
||||
⚠️ اتصال سرویسها
|
||||
<br />
|
||||
با توجه به شرایط فعلی، هیچ تضمینی برای اتصال دائمی سرویسها وجود ندارد. ممکن است اتصال مجدداً قطع شود یا حتی در برخی مواقع امکان برقراری آن فراهم نشود. در حال حاضر، وضعیت کاملاً مشخص و قابل پیشبینی نیست.
|
||||
<br />
|
||||
با این حال، ما مانند همیشه تمام تلاش خود را به کار میگیریم. خوشبختانه تا این لحظه، تلاشهایمان بینتیجه نبوده و هر بار پس از بروز مشکل، توانستهایم اتصال را مجدداً برقرار کرده و پایداری سرویسها را حفظ کنیم.
|
||||
<br />
|
||||
از همراهی و صبوری شما سپاسگزاریم.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
const [isInfoOpen, setIsInfoOpen] = useState(false);
|
||||
const [needsShahkarVerification, setNeedsShahkarVerification] = useState(false);
|
||||
const [nationalCode, setNationalCode] = useState("");
|
||||
const identityVerified = Boolean(data?.data?.result?.is_identity_verified);
|
||||
const [hasReadDescription, setHasReadDescription] = useState(false);
|
||||
const payablePrice = toNumber(
|
||||
price === 0 ? data?.data?.result?.price || 0 : price
|
||||
);
|
||||
@@ -60,6 +62,10 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
data?.data?.result?.has_extra_description
|
||||
);
|
||||
const extraDescriptionSections = data?.data?.result?.extra_description ?? [];
|
||||
const hasConnectionNotice = Boolean(
|
||||
data?.data?.result?.has_connection_notice
|
||||
);
|
||||
const connectionNotice = data?.data?.result?.connection_notice ?? "";
|
||||
const advancedVerificationFromApi =
|
||||
data?.data?.result?.requires_advanced_verification;
|
||||
const hasAdvancedVerificationApiFlag =
|
||||
@@ -103,10 +109,14 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
setPrice(0);
|
||||
setNeedsShahkarVerification(false);
|
||||
setNationalCode("");
|
||||
setHasReadDescription(false);
|
||||
}
|
||||
}, [isOpen, setFieldValue]);
|
||||
const mutation = useMutation({
|
||||
mutationKey: ["buyConfig"],
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
mutationFn: (values: BuyConfigDataProps) => {
|
||||
return axios.post(`home/config/${configId}/buy`, values);
|
||||
},
|
||||
@@ -189,12 +199,17 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
};
|
||||
|
||||
const submitHandler = () => {
|
||||
if (hasExtraDescription && !hasReadDescription) {
|
||||
toast.error("لطفا ابتدا توضیحات سرویس را مطالعه و تایید کنید");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiresAdvancedVerification) {
|
||||
goToAdvancedVerification();
|
||||
return;
|
||||
}
|
||||
|
||||
if (needsShahkarVerification && nationalCode.trim().length !== 10) {
|
||||
if (needsShahkarVerification && !identityVerified && nationalCode.trim().length !== 10) {
|
||||
toast.error("لطفا کد ملی ۱۰ رقمی را بهدرستی وارد کنید");
|
||||
return;
|
||||
}
|
||||
@@ -202,7 +217,8 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
const payload = {
|
||||
...formik.values,
|
||||
status: "buy",
|
||||
...(needsShahkarVerification ? { nationalCode: nationalCode.trim() } : {}),
|
||||
...(hasExtraDescription ? { hasReadDescription: true } : {}),
|
||||
...(needsShahkarVerification && !identityVerified ? { nationalCode: nationalCode.trim() } : {}),
|
||||
};
|
||||
|
||||
toast.promise(mutation.mutateAsync(payload), {
|
||||
@@ -227,6 +243,9 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
}
|
||||
|
||||
const responseData = err.response?.data;
|
||||
if (responseData && typeof responseData === "object" && "message" in responseData && typeof responseData.message === "string") {
|
||||
errorMessage = responseData.message;
|
||||
}
|
||||
const isResponseObject =
|
||||
responseData !== null && typeof responseData === "object";
|
||||
const requiresAdvancedVerificationFromApi =
|
||||
@@ -244,13 +263,14 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
(err.response?.status === 422 &&
|
||||
errorMessage.includes("احراز هویت پیشرفته"));
|
||||
|
||||
const requiresShahkarVerificationFromApi =
|
||||
isResponseObject &&
|
||||
"requires_shahkar_verification" in responseData &&
|
||||
Boolean(
|
||||
(responseData as { requires_shahkar_verification?: boolean })
|
||||
.requires_shahkar_verification
|
||||
);
|
||||
const shahkarError = responseData as {
|
||||
requires_shahkar_verification?: boolean;
|
||||
result?: { requires_shahkar_verification?: boolean };
|
||||
} | undefined;
|
||||
const requiresShahkarVerificationFromApi = Boolean(
|
||||
shahkarError?.requires_shahkar_verification ||
|
||||
shahkarError?.result?.requires_shahkar_verification
|
||||
);
|
||||
|
||||
if (requiresShahkarVerificationFromApi) {
|
||||
setNeedsShahkarVerification(true);
|
||||
@@ -280,10 +300,7 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
setIsOpenModal={setIsOpen}
|
||||
submitText={data?.data?.result?.max_config === 0 ? undefined : "خرید"}
|
||||
submitOnClick={submitHandler}
|
||||
disabled={
|
||||
isLoading ||
|
||||
isFetching
|
||||
}
|
||||
disabled={isLoading || isFetching}
|
||||
nameService={
|
||||
isLoading || isFetching
|
||||
? "درحال بارگذاری..."
|
||||
@@ -291,6 +308,19 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
? data?.data.result.title
|
||||
: "خطا رخ داد"
|
||||
}
|
||||
aboveSubmit={
|
||||
hasExtraDescription ? (
|
||||
<label className="w-full flex flex-row-reverse items-center justify-center gap-2 text-xs md:text-sm text-gray-300 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasReadDescription}
|
||||
onChange={(e) => setHasReadDescription(e.target.checked)}
|
||||
className="w-4 h-4 accent-secondTheme"
|
||||
/>
|
||||
توضیحات سرویس را مطالعه کردم
|
||||
</label>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<div className="relative w-full h-full px-2 md:px-4">
|
||||
{error && (
|
||||
@@ -342,10 +372,12 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
کاربر گرامی لطفا از خاموش بودن vpn خود اطمینان حاصل فرمایید تا در
|
||||
درگاه پرداخت مشکلی نداشته باشید{" "}
|
||||
</Alert>
|
||||
<div className="w-full bg-gray-800 rounded-md p-2 text-xs text-gray-400 leading-6 text-right">
|
||||
<p className="font-bold text-yellow-400 text-xs mb-1">⚠️ اتصال سرویسها</p>
|
||||
<p>با توجه به شرایط فعلی، هیچ تضمینی برای اتصال دائمی سرویسها وجود ندارد. ممکن است اتصال مجدداً قطع شود یا حتی در برخی مواقع امکان برقراری آن فراهم نشود. با این حال، تمام تلاش خود را میکنیم تا هر چه سریعتر مشکل را برطرف کنیم. از همراهی و صبوری شما سپاسگزاریم.</p>
|
||||
</div>
|
||||
{hasConnectionNotice && (
|
||||
<div className="w-full bg-gray-800 rounded-md p-2 text-xs text-gray-400 leading-6 text-right">
|
||||
<p className="font-bold text-yellow-400 text-xs mb-1">⚠️ اتصال سرویس</p>
|
||||
<p>{connectionNotice}</p>
|
||||
</div>
|
||||
)}
|
||||
<h1 className="font-bold">
|
||||
مبلغ قابل پرداخت :{" "}
|
||||
<span className="px-[2px] rounded-md bg-secondTheme text-gray-950">
|
||||
@@ -358,7 +390,7 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
{advancedVerificationMessage}
|
||||
</Alert>
|
||||
)}
|
||||
{needsShahkarVerification && (
|
||||
{needsShahkarVerification && !identityVerified && (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Alert type="warning" extra="!mb-0 text-start">
|
||||
برای خریدهای با مبلغ بالا، احراز هویت شاهکار (تطبیق شماره
|
||||
@@ -471,7 +503,7 @@ const ModalBuy = ({ isOpen, setIsOpen, configId }: ModalBuyProps) => {
|
||||
)}
|
||||
{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> را
|
||||
با خرید، <a href="/rule" target="_blank" rel="noopener noreferrer" className="text-gray-400 underline">قوانین و مقررات</a> را
|
||||
میپذیرید.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import MuiModal from "../../../../../common/modal";
|
||||
|
||||
interface DescriptionSection {
|
||||
export interface DescriptionSection {
|
||||
emoji?: string;
|
||||
title?: string;
|
||||
items?: string[];
|
||||
@@ -41,7 +41,6 @@ const ModalDnsServiceInfo = ({
|
||||
isOpenModal={isOpen}
|
||||
setIsOpenModal={setIsOpen}
|
||||
title="توضیحات سرویس"
|
||||
nameService="توضیحات سرویس"
|
||||
>
|
||||
<div className="w-full h-full flex flex-col gap-3 max-sm:text-xs max-md:text-sm text-base">
|
||||
{(sections ?? []).map((section, sectionIndex) => (
|
||||
|
||||
5
src/types/props.d.ts
vendored
5
src/types/props.d.ts
vendored
@@ -116,6 +116,7 @@ export interface MuiModalProps {
|
||||
submitOnClick?: () => void;
|
||||
disabled?: boolean;
|
||||
handleCloseCustom ?: () => void;
|
||||
aboveSubmit?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -217,6 +218,10 @@ type UserProfile = {
|
||||
name: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
national_id?: string | null;
|
||||
is_identity_verified?: boolean;
|
||||
shahkar_verified_at?: string | null;
|
||||
card_number?: string | null;
|
||||
};
|
||||
|
||||
// تعریف تایپ پیکربندیها
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { copyFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
function copySeoFiles() {
|
||||
return {
|
||||
name: "copy-seo-files",
|
||||
closeBundle() {
|
||||
const root = resolve(__dirname);
|
||||
const outDir = resolve(root, "dist");
|
||||
copyFileSync(resolve(root, "robots.txt"), resolve(outDir, "robots.txt"));
|
||||
copyFileSync(resolve(root, "sitemap.xml"), resolve(outDir, "sitemap.xml"));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [react(), copySeoFiles()],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user