feat : dynamic profile api and add logout btn
This commit is contained in:
5
src/hooks/useQueryParams.ts
Normal file
5
src/hooks/useQueryParams.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export const useQueryParams = () => {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
};
|
||||
@@ -1,12 +1,94 @@
|
||||
import ProfileDetails from "./section/ProfileDetails";
|
||||
import ProductConfigList from "./section/ProductConfigList";
|
||||
import { useQueryParams } from "../../hooks/useQueryParams";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import useAxios from "../../hooks/useAxios";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import ModalPayment from "./section/components/ModalPayment";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Profile = () => {
|
||||
const query = useQueryParams();
|
||||
const axios = useAxios();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [isPayment, setIsPayment] = useState<boolean>(false);
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const navigate = useNavigate();
|
||||
const mutation = useMutation({
|
||||
mutationKey: ["statusPayment"],
|
||||
mutationFn: (values: { trackId: string }) => {
|
||||
return axios.post("/home/config/confirm", values);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setIsOpen(true);
|
||||
setIsPayment(true);
|
||||
setMessage(data?.data?.result?.alert);
|
||||
navigate("/profile", { replace: true });
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
||||
if (
|
||||
err.response?.data &&
|
||||
typeof err.response.data === "object" &&
|
||||
"alert" in err.response.data
|
||||
) {
|
||||
errorMessage = (err.response.data as { alert: string }).alert;
|
||||
}
|
||||
setIsOpen(true);
|
||||
setIsPayment(false);
|
||||
setMessage(errorMessage);
|
||||
navigate("/profile", { replace: true });
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
const trackId = query.get("trackId");
|
||||
|
||||
if (trackId) {
|
||||
toast.promise(mutation.mutateAsync({ trackId }), {
|
||||
loading: "درحال تایید درگاه پرداخت...",
|
||||
success: (res: AxiosResponse) => {
|
||||
return `${res.data.alert}`;
|
||||
},
|
||||
error: (err: AxiosError) => {
|
||||
let errorMessage = "یک خطای غیرمنتظره رخ داد";
|
||||
if (
|
||||
err.response?.data &&
|
||||
typeof err.response.data === "object" &&
|
||||
"alert" in err.response.data
|
||||
) {
|
||||
errorMessage = (err.response.data as { alert: string }).alert;
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.error("Track ID is missing or invalid.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const {data,isLoading,isFetching,error} = useQuery({
|
||||
queryKey:["profile",mutation],
|
||||
queryFn:()=>{
|
||||
return axios.get('/profile');
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className="relative h-full w-full flex flex-col gap-4">
|
||||
<ProfileDetails />
|
||||
<ProductConfigList />
|
||||
</div>
|
||||
<>
|
||||
{!mutation.isPending && (
|
||||
<ModalPayment
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
isPayment={isPayment}
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
<div className="relative h-full w-full flex flex-col gap-4">
|
||||
<ProfileDetails data={data?.data} isLoading={isLoading} isFetching={isFetching} error={error} userTitle=""/>
|
||||
<ProductConfigList data={data?.data} userTitle={data?.data?.result?.user_profile?.name} isLoading={isLoading} isFetching={isFetching} error={error} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
import { LoaderIcon } from "react-hot-toast";
|
||||
import Alert from "../../../components/alert/Alert";
|
||||
import Button from "../../../components/button";
|
||||
import { ProfileDetailsProps } from "../../../types/props";
|
||||
import ConfigItem from "./components/ConfigItem";
|
||||
import ConfigItemLoading from "./components/ConfigItemLoading";
|
||||
import ProfileImage from "./components/ProfileImage";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
|
||||
const ProductConfigList = () => {
|
||||
const ProductConfigList = ({
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
userTitle,
|
||||
}: ProfileDetailsProps) => {
|
||||
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 ? (
|
||||
<LoaderIcon className="m-1.5" />
|
||||
) : error ? (
|
||||
"error"
|
||||
) : (
|
||||
userTitle
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="max-md:text-lg text-xl font-bold text-center">
|
||||
لیست کانفیگ ها
|
||||
</p>
|
||||
@@ -12,21 +36,33 @@ const ProductConfigList = () => {
|
||||
className="w-full h-full p-3 flex flex-col gap-3
|
||||
justify-center items-center"
|
||||
>
|
||||
<ConfigItem
|
||||
configName="سرویس تست 1 روزه"
|
||||
dateBuy="1402/2/3"
|
||||
isDisabled={false}
|
||||
/>
|
||||
<ConfigItem
|
||||
configName="سرویس مولتی 1"
|
||||
dateBuy="1403/2/3"
|
||||
isDisabled={false}
|
||||
/>
|
||||
<ConfigItem
|
||||
configName="سرویس کاهش پینگ"
|
||||
dateBuy="1402/4/3"
|
||||
isDisabled={true}
|
||||
/>
|
||||
{isLoading || isFetching ? (
|
||||
Array(3)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<Fragment key={i}>
|
||||
<ConfigItemLoading />
|
||||
</Fragment>
|
||||
))
|
||||
) : error ? (
|
||||
<Alert type="danger" message="خطایی با این موضوع رخ داد: ">
|
||||
{error.message}
|
||||
</Alert>
|
||||
) : (
|
||||
data?.result?.configs?.map((item) => {
|
||||
return (
|
||||
<Fragment key={item.id}>
|
||||
<ConfigItem
|
||||
configName={item.title}
|
||||
dateBuy={item.created_at || "تاریخ نامعتبر"}
|
||||
isDisabled={false}
|
||||
isShowTraffic={item.show_traffic}
|
||||
textCopy={item.url}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import { MdEdit } from "react-icons/md";
|
||||
import { MdLogout } from "react-icons/md";
|
||||
import Button from "../../../components/button";
|
||||
import ModalEditProfile from "./components/ModalEditProfile";
|
||||
import { useState } from "react";
|
||||
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";
|
||||
|
||||
const ProfileDetails = () => {
|
||||
const ProfileDetails = ({
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
}: ProfileDetailsProps) => {
|
||||
return (
|
||||
<div className="relative z-[10] w-full h-32 max-md:h-20 flex flex-row-reverse justify-between glass items-center py-2 px-4">
|
||||
<EditProfile />
|
||||
<ProductDetails />
|
||||
<ProductDetails
|
||||
enable_config={data?.result?.enable_config}
|
||||
disable_config={data?.result?.disable_config}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,32 +29,67 @@ const ProfileDetails = () => {
|
||||
export default ProfileDetails;
|
||||
|
||||
const EditProfile = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// const [isOpen, setIsOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<div className="w-full h-full flex justify-end items-center">
|
||||
<ModalEditProfile isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
<Button
|
||||
<div className="w-full h-full flex justify-end items-center gap-2">
|
||||
{/* <ModalEditProfile isOpen={isOpen} setIsOpen={setIsOpen} /> */}
|
||||
{/* <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
|
||||
extra="!py-4 max-md:!py-2 max-md:px-2 !bg-rose-600 !text-white"
|
||||
onClick={() => dispatch(setToken(""))}
|
||||
>
|
||||
<MdLogout className="scale-[1.4] max-md:scale-100" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductDetails = () => {
|
||||
const ProductDetails = ({
|
||||
enable_config,
|
||||
disable_config,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
}: {
|
||||
enable_config: number;
|
||||
disable_config: number;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | AxiosError | null;
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full h-full flex justify-start items-center">
|
||||
<div className="md:mr-4 w-auto h-auto flex flex-col max-md:text-[12px] text-lg md:gap-2">
|
||||
<p className="text-nowrap">
|
||||
<div className="text-nowrap">
|
||||
کانفیگ های فعال :{" "}
|
||||
<span className="px-1 md:px-2 rounded-md bg-green-600"> 2 </span>
|
||||
</p>
|
||||
<p className="text-nowrap">
|
||||
<span className="px-1 md:px-2 rounded-md bg-green-600">
|
||||
{isLoading || isFetching ? (
|
||||
<LoaderIcon className="inline-block" />
|
||||
) : error ? (
|
||||
"error"
|
||||
) : (
|
||||
enable_config
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-nowrap">
|
||||
کانفیگ های غیر فعال :{" "}
|
||||
<span className="px-1 md:px-2 rounded-md bg-yellow-400"> 10 </span>
|
||||
</p>
|
||||
<span className="px-1 md:px-2 rounded-md bg-yellow-400">
|
||||
{isLoading || isFetching ? (
|
||||
<LoaderIcon className="inline-block" />
|
||||
) : error ? (
|
||||
"error"
|
||||
) : (
|
||||
disable_config
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,13 +5,30 @@ import ModalConfigDetails from "./ModalConfigDetails";
|
||||
import { useState } from "react";
|
||||
import Dropdown, { DropdownItem } from "../../../../components/dropdown";
|
||||
import ProgressSpace from "../../../../components/progressSpace";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const ConfigItem = ({ dateBuy, configName, isDisabled }: ConfigItemProps) => {
|
||||
const ConfigItem = ({
|
||||
dateBuy,
|
||||
configName,
|
||||
isDisabled,
|
||||
isShowTraffic,
|
||||
textCopy,
|
||||
}: ConfigItemProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isOpenDropDown, setIsOpenDropDown] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const handleToggle = () => {
|
||||
setIsOpenDropDown(!isOpen);
|
||||
};
|
||||
const copyToClipboard = async (textToCopy: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
toast.success("متن با موفقیت کپی شد!");
|
||||
} catch (err) {
|
||||
toast.error(`Failed to copy: , ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -21,11 +38,13 @@ const ConfigItem = ({ dateBuy, configName, isDisabled }: ConfigItemProps) => {
|
||||
dateBuy={dateBuy}
|
||||
isDisabled={isDisabled}
|
||||
nameConfig={configName}
|
||||
isShowTraffic={isShowTraffic}
|
||||
textCopy={textCopy}
|
||||
/>
|
||||
<div className="relative w-full h-full rounded-md">
|
||||
{isDisabled && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-opacity-50 rounded-md backdrop-blur-sm z-10">
|
||||
<div className="text-white font-bold p-1 bg-red-600 text-xs md:text-base rounded-md shadow-md">
|
||||
<div className="text-white font-bold max-sm:p-[2px] p-1 bg-red-600 max-sm:text-[10px] text-xs md:text-base rounded-md shadow-md">
|
||||
کانفیگ <span className="text-secondTheme">{configName}</span> غیر
|
||||
فعال است .{" "}
|
||||
<Button
|
||||
@@ -47,27 +66,35 @@ const ConfigItem = ({ dateBuy, configName, isDisabled }: ConfigItemProps) => {
|
||||
{configName}
|
||||
</li>
|
||||
<div className="max-md:hidden flex flex-row-reverse gap-2 justify-end items-center">
|
||||
<Button extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white">
|
||||
<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>
|
||||
<Button extra="!m-0 !px-2 !py-1.5 shadow-md">
|
||||
<Button
|
||||
extra="!m-0 !px-2 !py-1.5 shadow-md"
|
||||
onClick={() => navigate("/tutorial")}
|
||||
>
|
||||
آموزش
|
||||
</Button>
|
||||
<Dropdown isOpen={isOpenDropDown} setIsOpen={setIsOpenDropDown}>
|
||||
<Button
|
||||
id="dropdownDefaultButton"
|
||||
data-dropdown-toggle="dropdown"
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
extra="!m-0 !px-2 !py-1.5 shadow-md"
|
||||
>
|
||||
حجم
|
||||
</Button>
|
||||
<DropdownItem widthTailwind="w-[20rem] left-0">
|
||||
<ProgressSpace startSpace={100} endSpace={200} />
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
{isShowTraffic && (
|
||||
<Dropdown isOpen={isOpenDropDown} setIsOpen={setIsOpenDropDown}>
|
||||
<Button
|
||||
id="dropdownDefaultButton"
|
||||
data-dropdown-toggle="dropdown"
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
extra="!m-0 !px-2 !py-1.5 shadow-md"
|
||||
>
|
||||
حجم
|
||||
</Button>
|
||||
<DropdownItem widthTailwind="w-[20rem] left-0">
|
||||
<ProgressSpace startSpace={100} endSpace={200} />
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
<div className="md:hidden flex flex-row-reverse gap-2 justify-end items-center">
|
||||
<Button
|
||||
|
||||
25
src/pages/profile/section/components/ConfigItemLoading.tsx
Normal file
25
src/pages/profile/section/components/ConfigItemLoading.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
|
||||
const ConfigItemLoading = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-full h-full rounded-md">
|
||||
<div
|
||||
className={`w-full h-full p-3 rounded-md text-xs md:text-base gap-2 flex justify-between items-center text-secondTheme bg-gray-900 shadow-md shadow-gray-900`}
|
||||
>
|
||||
<Skeleton variant="rounded" animation="wave" width={100} />
|
||||
<li className="px-2 py-1 shadow-md rounded-md w-auto h-auto text-gray-900 font-bold list-none">
|
||||
<Skeleton variant="rounded" animation="wave" width={100} />
|
||||
</li>
|
||||
<div className="max-md:hidden flex flex-row-reverse gap-2 justify-end items-center">
|
||||
<Skeleton variant="rounded" animation="wave" width={100} />
|
||||
</div>
|
||||
<div className="md:hidden flex flex-row-reverse gap-2 justify-end items-center">
|
||||
<Skeleton variant="rounded" animation="wave" width={100} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ConfigItemLoading;
|
||||
@@ -4,6 +4,8 @@ import Button from "../../../../components/button";
|
||||
import { IoCopy } from "react-icons/io5";
|
||||
import Alert from "../../../../components/alert/Alert";
|
||||
import ProgressSpace from "../../../../components/progressSpace";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const ModalConfigDetails = ({
|
||||
isOpen,
|
||||
@@ -11,13 +13,27 @@ const ModalConfigDetails = ({
|
||||
isDisabled,
|
||||
dateBuy,
|
||||
nameConfig,
|
||||
isShowTraffic,
|
||||
textCopy
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
isDisabled: boolean;
|
||||
dateBuy: string;
|
||||
nameConfig: string;
|
||||
isShowTraffic: boolean;
|
||||
textCopy: string;
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const copyToClipboard = async (textToCopy: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
toast.success("متن با موفقیت کپی شد!");
|
||||
} catch (err) {
|
||||
toast.error(`Failed to copy: , ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiModal
|
||||
isOpenModal={isOpen}
|
||||
@@ -38,17 +54,17 @@ const ModalConfigDetails = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Button extra="!m-0 !px-2 !py-1.5 flex flex-row-reverse justify-center items-center gap-1 shadow-md shadow-white">
|
||||
<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>
|
||||
<Button extra="!m-0 !px-2 !py-1.5 shadow-md shadow-white">
|
||||
<Button onClick={()=> navigate('/tutorial')} extra="!m-0 !px-2 !py-1.5 shadow-md shadow-white">
|
||||
آموزش
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isDisabled && (
|
||||
{!isDisabled || isShowTraffic && (
|
||||
<div className="w-full h-full">
|
||||
<ProgressSpace startSpace={100} endSpace={200} />
|
||||
</div>
|
||||
|
||||
50
src/pages/profile/section/components/ModalPayment.tsx
Normal file
50
src/pages/profile/section/components/ModalPayment.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import MuiModal from "../../../../common/modal";
|
||||
import { MdError, MdVerified } from "react-icons/md";
|
||||
|
||||
const ModalPayment = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
isPayment,
|
||||
message,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
isPayment: boolean;
|
||||
message: string;
|
||||
}) => {
|
||||
console.log(isPayment);
|
||||
|
||||
return (
|
||||
<MuiModal isOpenModal={isOpen} setIsOpenModal={setIsOpen} title="وضعیت">
|
||||
<div className="relative w-full h-full px-4 py-2">
|
||||
<div className="w-full h-full flex flex-col gap-4 justify-center items-center">
|
||||
{isPayment ? (
|
||||
<>
|
||||
{" "}
|
||||
<MdVerified className="w-[6rem] h-[6rem] text-green-600" />
|
||||
<p className="text-base md:text-lg text-green-600">
|
||||
پرداخت موفقیت امیز بود {message}
|
||||
</p>
|
||||
<p className="text-xs md:text-sm">از اعتماد شما سپاسگذاریم</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<MdError className="w-[6rem] h-[6rem] text-rose-600" />
|
||||
<p className="text-base md:text-lg text-rose-600">
|
||||
پرداخت موفقیت امیز نبود {message}
|
||||
</p>
|
||||
<p className="text-xs md:text-sm">
|
||||
در صورت کسر وجه اگر تا 48 ساعت برگشت داده نشد با پشتیبانی تماس
|
||||
بگیرید
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</MuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalPayment;
|
||||
Reference in New Issue
Block a user