feat : add page profile (private component) and add section , modal for profile page
This commit is contained in:
@@ -8,6 +8,7 @@ import About from "./pages/about";
|
||||
import ProgressBar from "./common/ProgressBar";
|
||||
import AuthLayout from "./layout/AuthLayout";
|
||||
import Login from "./pages/login";
|
||||
import Profile from "./pages/profile";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
@@ -19,11 +20,11 @@ const App = () => {
|
||||
<Route path="tutorial" element={<Tutorial />} />
|
||||
<Route path="rule" element={<Rule />} />
|
||||
<Route path="about" element={<About />} />
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route path="private" element={<p>private</p>} />
|
||||
<Route path="private" element={<PrivateRoute />}>
|
||||
<Route index element={<Profile />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="login" element={<AuthLayout />} >
|
||||
<Route path="profile" element={<AuthLayout />} >
|
||||
<Route index element={<Login />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -4,13 +4,15 @@ import { motion } from "framer-motion";
|
||||
import { IoClose } from "react-icons/io5";
|
||||
import Button from "../../components/button";
|
||||
import { MuiModalProps } from "../../types/props";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Input from "../../components/input";
|
||||
import { PiMinus, PiPlus } from "react-icons/pi";
|
||||
|
||||
export default function MuiModal({
|
||||
isOpenModal,
|
||||
setIsOpenModal,
|
||||
title,
|
||||
nameService,
|
||||
submitText,
|
||||
submitOnClick,
|
||||
children,
|
||||
}: MuiModalProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpen = () => setOpen(true);
|
||||
@@ -18,7 +20,6 @@ export default function MuiModal({
|
||||
setOpen(false);
|
||||
setIsOpenModal(false);
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpenModal) handleOpen();
|
||||
@@ -41,58 +42,18 @@ export default function MuiModal({
|
||||
>
|
||||
<motion.div className="w-full h-full flex justify-between">
|
||||
<h1 className="text-lg md:text-xl font-bold">
|
||||
خرید سرویس <span className="text-green-600"> مولتی 1 </span>
|
||||
{title} {nameService && <span className="text-green-600"> {nameService} </span>}
|
||||
</h1>
|
||||
<button onClick={handleClose}>
|
||||
<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">
|
||||
<div className="w-full h-full px-2 py-2 flex flex-col gap-3">
|
||||
<h1 className="text-lg md:text-xl">
|
||||
مبلغ قابل پرداخت :{" "}
|
||||
<span className="px-2 py-[1px] rounded-md bg-secondTheme text-black">
|
||||
100/000
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-gray-500">
|
||||
در صورت نداشتن کد تخفیف پرداخت رو بزنید
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full h-full p-2 flex gap-2 justify-center">
|
||||
<Input type="text" id="1" name="offer" />
|
||||
<Button extra="!bg-green-600"> اعمال </Button>
|
||||
</div>
|
||||
<div className="w-full h-full px-4 py-2 flex gap-3 justify-center">
|
||||
<p className="text-sm md:text-base text-gray-500">تعداد:</p>
|
||||
<div className="bg-gray-700 rounded-md flex justify-between gap-1 items-center px-1">
|
||||
<PiMinus className="cursor-pointer" />
|
||||
<input
|
||||
type="number"
|
||||
className="w-10 bg-transparent focus:ring-0 outline-none border-none ring-0"
|
||||
value={1}
|
||||
/>
|
||||
<PiPlus className="cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full px-4 py-2 flex flex-col gap-3 justify-center">
|
||||
<div className="flex justify-between px-4 py-1 shadow-md shadow-gray-800 rounded-md bg-gray-800">
|
||||
<p className="text-sm md:text-base">حجم :</p>
|
||||
<p className="text-sm md:text-base">10 گیگ</p>
|
||||
</div>
|
||||
<div className="flex justify-between px-4 py-1 shadow-md shadow-gray-800 rounded-md bg-gray-800">
|
||||
<p className="text-sm md:text-base">محدودیت کاربر :</p>
|
||||
<p className="text-sm md:text-base">بدون محدودیت</p>
|
||||
</div>
|
||||
<div className="flex justify-between px-4 py-1 shadow-md shadow-gray-800 rounded-md bg-gray-800">
|
||||
<p className="text-sm md:text-base">مدت زمان :</p>
|
||||
<p className="text-sm md:text-base">یک ماه</p>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</motion.div>
|
||||
<motion.div className="w-full h-full flex justify-end items-center gap-2 md:gap-3 text-lg md:text-xl">
|
||||
<Button onClick={() => handleClose()}> بستن </Button>
|
||||
<Button onClick={() => navigate("login")}> باشه </Button>
|
||||
<Button onClick={submitOnClick}> {submitText} </Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ButtonProps } from "../../types/props";
|
||||
|
||||
function Button({ children, onClick, extra, active }: ButtonProps) {
|
||||
function Button({ children, onClick, extra, active,...attrs }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`px-4 py-1 md:px-5 md:py-2 rounded-md transition duration-300 ease-in-out transform bg-white text-[#333] hover:bg-gray-600 hover:text-white ${
|
||||
@@ -10,6 +10,7 @@ function Button({ children, onClick, extra, active }: ButtonProps) {
|
||||
} ${extra}`}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ const Input = ({
|
||||
type,
|
||||
name,
|
||||
placeholder,
|
||||
labelStyle,
|
||||
extra,
|
||||
...attrs
|
||||
}: InputProps) => {
|
||||
@@ -14,7 +15,7 @@ const Input = ({
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
className={`block mb-2 text-sm font-medium text-white ${labelStyle}`}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
67
src/components/productBox/components/MuiModalBuy.tsx
Normal file
67
src/components/productBox/components/MuiModalBuy.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import MuiModal from "../../../common/modal";
|
||||
import Input from "../../input";
|
||||
import Button from "../../button";
|
||||
import { PiMinus, PiPlus } from "react-icons/pi";
|
||||
|
||||
const MuiModalBuy = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
return (
|
||||
<MuiModal
|
||||
isOpenModal={isOpen}
|
||||
setIsOpenModal={setIsOpen}
|
||||
submitText="خرید"
|
||||
title="خرید سرویس"
|
||||
nameService="مولتی 1"
|
||||
>
|
||||
<div className="w-full h-full px-2 py-2 flex flex-col gap-3">
|
||||
<h1 className="text-lg md:text-xl">
|
||||
مبلغ قابل پرداخت :{" "}
|
||||
<span className="px-2 py-[1px] rounded-md bg-secondTheme text-black">
|
||||
100/000
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-gray-500">
|
||||
در صورت نداشتن کد تخفیف پرداخت رو بزنید
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full h-full p-2 flex gap-2 justify-center">
|
||||
<Input type="text" id="1" name="offer" />
|
||||
<Button extra="!bg-green-600"> اعمال </Button>
|
||||
</div>
|
||||
<div className="w-full h-full px-4 py-2 flex gap-3 justify-center">
|
||||
<p className="text-sm md:text-base text-gray-500">تعداد:</p>
|
||||
<div className="bg-gray-700 rounded-md flex justify-between gap-1 items-center px-1">
|
||||
<PiMinus className="cursor-pointer" />
|
||||
<input
|
||||
type="number"
|
||||
className="w-10 bg-transparent focus:ring-0 outline-none border-none ring-0"
|
||||
value={1}
|
||||
/>
|
||||
<PiPlus className="cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full px-4 py-2 flex flex-col gap-3 justify-center">
|
||||
<div className="flex justify-between px-4 py-1 shadow-md shadow-gray-800 rounded-md bg-gray-800">
|
||||
<p className="text-sm md:text-base">حجم :</p>
|
||||
<p className="text-sm md:text-base">10 گیگ</p>
|
||||
</div>
|
||||
<div className="flex justify-between px-4 py-1 shadow-md shadow-gray-800 rounded-md bg-gray-800">
|
||||
<p className="text-sm md:text-base">محدودیت کاربر :</p>
|
||||
<p className="text-sm md:text-base">بدون محدودیت</p>
|
||||
</div>
|
||||
<div className="flex justify-between px-4 py-1 shadow-md shadow-gray-800 rounded-md bg-gray-800">
|
||||
<p className="text-sm md:text-base">مدت زمان :</p>
|
||||
<p className="text-sm md:text-base">یک ماه</p>
|
||||
</div>
|
||||
</div>
|
||||
</MuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MuiModalBuy;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AM, EC, GA, US, GD, GB } from "country-flag-icons/react/1x1";
|
||||
import { ProductBoxProps } from "../../types/props";
|
||||
import Button from "../button";
|
||||
import MuiModal from "../../common/modal";
|
||||
import { useState } from "react";
|
||||
import MuiModalBuy from "./components/MuiModalBuy";
|
||||
|
||||
const ProductBox = ({
|
||||
title,
|
||||
@@ -26,7 +26,7 @@ const ProductBox = ({
|
||||
: variant === 3 && "shadow-indigo-800 bg-indigo-600"
|
||||
} bg-cover bg-center bg-banner`}
|
||||
>
|
||||
<MuiModal isOpenModal={isOpen} setIsOpenModal={setIsOpen} />
|
||||
<MuiModalBuy isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
<h1 className="max-md:text-xl md:text-2xl font-bold">{title}</h1>
|
||||
<div className="w-full h-full p-2 flex gap-2 justify-between items-center">
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
|
||||
13
src/pages/profile/index.tsx
Normal file
13
src/pages/profile/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import ProfileDetails from "./section/ProfileDetails";
|
||||
import ProductConfigList from "./section/ProductConfigList";
|
||||
|
||||
const Profile = () => {
|
||||
return (
|
||||
<div className="relative h-full w-full flex flex-col gap-4">
|
||||
<ProfileDetails />
|
||||
<ProductConfigList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
33
src/pages/profile/section/ProductConfigList.tsx
Normal file
33
src/pages/profile/section/ProductConfigList.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import ConfigItem from "./components/ConfigItem";
|
||||
|
||||
const ProductConfigList = () => {
|
||||
return (
|
||||
<div className="w-full h-full p-4 glass flex flex-col gap-3 pt-14">
|
||||
<p className="max-md:text-lg text-xl font-bold text-center">
|
||||
لیست کانفیگ ها
|
||||
</p>
|
||||
<div
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductConfigList;
|
||||
60
src/pages/profile/section/ProfileDetails.tsx
Normal file
60
src/pages/profile/section/ProfileDetails.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { MdEdit } from "react-icons/md";
|
||||
import Button from "../../../components/button";
|
||||
import ModalEditProfile from "./components/ModalEditProfile";
|
||||
import { useState } from "react";
|
||||
|
||||
const ProfileDetails = () => {
|
||||
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 />
|
||||
<ProfileImage />
|
||||
<ProductDetails />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileDetails;
|
||||
|
||||
const EditProfile = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<div className="w-full h-full flex justify-end items-center">
|
||||
<ModalEditProfile isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
<Button
|
||||
extra="!py-4 max-md:!py-2 max-md:px-2 md:ml-4"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<MdEdit className="scale-[1.4] max-md:scale-100" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileImage = () => {
|
||||
return (
|
||||
<div className="relative w-full h-full flex justify-center items-center">
|
||||
<div className="absolute w-auto h-auto border-4 border-secondTheme rounded-full max-md:scale-90 max-md:top-1/3 top-1/2">
|
||||
<svg height="100" width="100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle r="45" cx="50" cy="50" fill="#111" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductDetails = () => {
|
||||
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">
|
||||
کانفیگ های فعال :{" "}
|
||||
<span className="px-2 rounded-md bg-green-600"> 2 </span>
|
||||
</p>
|
||||
<p className="text-nowrap">
|
||||
کانفیگ های غیر فعال :{" "}
|
||||
<span className="px-2 rounded-md bg-yellow-400"> 10 </span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
34
src/pages/profile/section/components/ConfigItem.tsx
Normal file
34
src/pages/profile/section/components/ConfigItem.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IoCopy } from "react-icons/io5";
|
||||
import Button from "../../../../components/button";
|
||||
import { ConfigItemProps } from "../../../../types/props";
|
||||
|
||||
const ConfigItem = ({ dateBuy, configName, isDisabled }: ConfigItemProps) => {
|
||||
return (
|
||||
<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">
|
||||
<p className="text-white font-bold px-1 bg-red-600 text-xs md:text-base rounded-md shadow-md shadow-red-600">
|
||||
کانفیگ <span className="text-secondTheme">{configName}</span> غیر فعال است
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<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 ${
|
||||
isDisabled ? "filter blur-sm" : ""
|
||||
}`}
|
||||
>
|
||||
<p>تاریخ خرید : {dateBuy}</p>
|
||||
<li className="px-2 py-1 bg-secondTheme shadow-md shadow-secondTheme rounded-md w-auto h-auto text-gray-900 font-bold list-none">
|
||||
{configName}
|
||||
</li>
|
||||
<div className="flex flex-row-reverse gap-2 justify-end items-center">
|
||||
<Button extra="!m-0 !px-2 !py-1.5 shadow-md shadow-white">
|
||||
<IoCopy />
|
||||
</Button>
|
||||
<Button extra="!m-0 !px-2 !py-1 shadow-md shadow-white">آموزش</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ConfigItem;
|
||||
26
src/pages/profile/section/components/ModalEditProfile.tsx
Normal file
26
src/pages/profile/section/components/ModalEditProfile.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import MuiModal from "../../../../common/modal";
|
||||
import Input from "../../../../components/input";
|
||||
|
||||
const ModalEditProfile = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
return (
|
||||
<MuiModal
|
||||
isOpenModal={isOpen}
|
||||
setIsOpenModal={setIsOpen}
|
||||
submitText="تایید"
|
||||
title="ویرایش پروفایل"
|
||||
>
|
||||
<div className="relative w-full h-full px-2 md:px-4">
|
||||
<Input type="text" label="ویرایش نام :" labelStyle="!text-start" extra="!w-full" />
|
||||
</div>
|
||||
</MuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalEditProfile;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { isAuthenticated } from "../utils/auth";
|
||||
|
||||
const PrivateRoute = () => {
|
||||
return !isAuthenticated() && <Navigate to="/" />;
|
||||
return !isAuthenticated() ? <Outlet /> : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
|
||||
16
src/types/props.d.ts
vendored
16
src/types/props.d.ts
vendored
@@ -50,7 +50,7 @@ export interface NavData {
|
||||
}
|
||||
|
||||
// begin ListTutorial.tsx
|
||||
import { Component, InputHTMLAttributes, SetStateAction } from "react";
|
||||
import React, { Component, InputHTMLAttributes, SetStateAction } from "react";
|
||||
|
||||
export type TutorialList = { id: number; name: string }[];
|
||||
|
||||
@@ -92,10 +92,22 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
type: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
labelStyle?: string;
|
||||
extra?: string;
|
||||
}
|
||||
|
||||
export interface MuiModalProps {
|
||||
isOpenModal: boolean;
|
||||
setIsOpenModal:SetStateAction
|
||||
setIsOpenModal: SetStateAction;
|
||||
title: string;
|
||||
nameService?: string;
|
||||
submitText: string;
|
||||
submitOnClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ConfigItemProps {
|
||||
dateBuy: string;
|
||||
configName: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user