feat : dynamic profile api and add logout btn

This commit is contained in:
alij
2024-06-22 16:27:12 +04:30
parent 3d0eb5a66d
commit ac4a5f1e43
8 changed files with 347 additions and 57 deletions

View File

@@ -0,0 +1,5 @@
import { useLocation } from "react-router-dom";
export const useQueryParams = () => {
return new URLSearchParams(useLocation().search);
};

View File

@@ -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 (
<>
{!mutation.isPending && (
<ModalPayment
isOpen={isOpen}
setIsOpen={setIsOpen}
isPayment={isPayment}
message={message}
/>
)}
<div className="relative h-full w-full flex flex-col gap-4">
<ProfileDetails />
<ProductConfigList />
<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>
</>
);
};

View File

@@ -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"
>
{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="سرویس تست 1 روزه"
dateBuy="1402/2/3"
configName={item.title}
dateBuy={item.created_at || "تاریخ نامعتبر"}
isDisabled={false}
isShowTraffic={item.show_traffic}
textCopy={item.url}
/>
<ConfigItem
configName="سرویس مولتی 1"
dateBuy="1403/2/3"
isDisabled={false}
/>
<ConfigItem
configName="سرویس کاهش پینگ"
dateBuy="1402/4/3"
isDisabled={true}
/>
</Fragment>
);
})
)}
</div>
</div>
);

View File

@@ -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>
);

View File

@@ -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,13 +66,20 @@ 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>
{isShowTraffic && (
<Dropdown isOpen={isOpenDropDown} setIsOpen={setIsOpenDropDown}>
<Button
id="dropdownDefaultButton"
@@ -68,6 +94,7 @@ const ConfigItem = ({ dateBuy, configName, isDisabled }: ConfigItemProps) => {
<ProgressSpace startSpace={100} endSpace={200} />
</DropdownItem>
</Dropdown>
)}
</div>
<div className="md:hidden flex flex-row-reverse gap-2 justify-end items-center">
<Button

View 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;

View File

@@ -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>

View 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;