user profile page
This commit is contained in:
@@ -2,6 +2,8 @@ import { Route, Routes } from "react-router-dom";
|
|||||||
import DefaultLayout from "./layouts/DefaultLayout";
|
import DefaultLayout from "./layouts/DefaultLayout";
|
||||||
import HomePage from "./pages/HomePage";
|
import HomePage from "./pages/HomePage";
|
||||||
import ErrorPage from "./pages/ErrorPage";
|
import ErrorPage from "./pages/ErrorPage";
|
||||||
|
import { getAccessToken } from "./http";
|
||||||
|
import MemberPage from "./pages/MemberPage";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -9,7 +11,10 @@ export default function App() {
|
|||||||
<Route>
|
<Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
<Route index element={<HomePage />} />
|
<Route
|
||||||
|
index
|
||||||
|
element={getAccessToken() ? <MemberPage /> : <HomePage />}
|
||||||
|
/>
|
||||||
<Route path="*" element={<ErrorPage />} />
|
<Route path="*" element={<ErrorPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,7 +1,46 @@
|
|||||||
import { Input, Checkbox, Button, Link } from "@heroui/react";
|
import { Input, Checkbox, Button, Link } from "@heroui/react";
|
||||||
import { IconMail, IconLock } from "@tabler/icons-react";
|
import { IconMail, IconLock } from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import http, { login } from "../http";
|
||||||
|
|
||||||
export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const validateFields = () => {
|
||||||
|
if (!email || !password) {
|
||||||
|
toast.error("Both email and password are required.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!validateFields()) return;
|
||||||
|
|
||||||
|
const loginRequest = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await http.post("/User/login", loginRequest);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("Login failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token } = response.data;
|
||||||
|
login(token);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
(error as any).response?.data ||
|
||||||
|
"Something went wrong! Please try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -11,8 +50,20 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Input endContent={<IconMail />} label="Email" />
|
<Input
|
||||||
<Input endContent={<IconLock />} label="Password" type="password" />
|
endContent={<IconMail />}
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onValueChange={setEmail}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
endContent={<IconLock />}
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onValueChange={setPassword}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex py-2 px-1 justify-between">
|
<div className="flex py-2 px-1 justify-between">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -27,18 +78,12 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<Button color="primary" className="w-full">
|
<Button color="primary" className="w-full" onPress={handleLogin}>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex flex-row gap-2 w-full justify-center *:my-auto">
|
<div className="flex flex-row gap-2 w-full justify-center *:my-auto">
|
||||||
<p className="text-sm">Don't have an account?</p>
|
<p className="text-sm">Don't have an account?</p>
|
||||||
<Link
|
<Link color="primary" onPress={onSignup} className="text-sm">
|
||||||
color="primary"
|
|
||||||
onPress={() => {
|
|
||||||
onSignup();
|
|
||||||
}}
|
|
||||||
className="text-sm"
|
|
||||||
>
|
|
||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import { useState } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import SignupView from "./SignupView";
|
import SignupView from "./SignupView";
|
||||||
import LoginView from "./LoginView";
|
import LoginView from "./LoginView";
|
||||||
|
import { getAccessToken } from "../http";
|
||||||
|
|
||||||
export default function NavigationBar() {
|
export default function NavigationBar() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
const [isSignup, setIsSignup] = useState(false);
|
const [isSignup, setIsSignup] = useState(false);
|
||||||
|
const accessToken = getAccessToken();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar className="border-b-[1px] border-neutral-500/25">
|
<Navbar className="border-b-[1px] border-neutral-500/25">
|
||||||
@@ -39,6 +41,7 @@ export default function NavigationBar() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
|
{!accessToken && (
|
||||||
<NavbarContent justify="end">
|
<NavbarContent justify="end">
|
||||||
<NavbarItem className="hidden lg:flex">
|
<NavbarItem className="hidden lg:flex">
|
||||||
<Button
|
<Button
|
||||||
@@ -63,6 +66,7 @@ export default function NavigationBar() {
|
|||||||
</Button>
|
</Button>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
</NavbarContent>
|
</NavbarContent>
|
||||||
|
)}
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{() => (
|
{() => (
|
||||||
|
|||||||
@@ -41,4 +41,27 @@ http.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function login(token: string) {
|
||||||
|
setAccessToken(token);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
clearAccessToken();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccessToken() {
|
||||||
|
return localStorage.getItem("accessToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAccessToken(token: string) {
|
||||||
|
clearAccessToken();
|
||||||
|
localStorage.setItem("accessToken", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAccessToken() {
|
||||||
|
localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
export default http;
|
export default http;
|
||||||
|
|||||||
10
AceJobAgency.client/src/models/user-profile.ts
Normal file
10
AceJobAgency.client/src/models/user-profile.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
nationalRegistrationIdentityCardNumber: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
dateOfBirth: string;
|
||||||
|
whoAmI: string;
|
||||||
|
resumeName: string;
|
||||||
|
}
|
||||||
@@ -9,15 +9,22 @@ import {
|
|||||||
ModalHeader,
|
ModalHeader,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@heroui/react";
|
} from "@heroui/react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import LoginView from "../components/LoginView";
|
import LoginView from "../components/LoginView";
|
||||||
import SignupView from "../components/SignupView";
|
import SignupView from "../components/SignupView";
|
||||||
|
import { getAccessToken } from "../http";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
const [isSignup, setIsSignup] = useState(false);
|
const [isSignup, setIsSignup] = useState(false);
|
||||||
const [emailValue, setEmailValue] = useState("");
|
const [emailValue, setEmailValue] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getAccessToken()) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 w-full h-full flex flex-col justify-center bg-indigo-500/10 dark:bg-indigo-500/20">
|
<div className="absolute inset-0 w-full h-full flex flex-col justify-center bg-indigo-500/10 dark:bg-indigo-500/20">
|
||||||
<div className="relative m-auto w-max h-max flex flex-col gap-10 justify-center text-center *:mx-auto">
|
<div className="relative m-auto w-max h-max flex flex-col gap-10 justify-center text-center *:mx-auto">
|
||||||
|
|||||||
86
AceJobAgency.client/src/pages/MemberPage.tsx
Normal file
86
AceJobAgency.client/src/pages/MemberPage.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import http, { getAccessToken, logout } from "../http";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { UserProfile } from "../models/user-profile";
|
||||||
|
import { Button, Card, Divider, Input } from "@heroui/react";
|
||||||
|
|
||||||
|
export default function MemberPage() {
|
||||||
|
const accessToken = getAccessToken();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accessToken) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
http.get("/User/profile").then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
navigate("/error");
|
||||||
|
}
|
||||||
|
setUserProfile(response.data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 w-full h-full flex flex-col justify-center items-center bg-indigo-500/10 dark:bg-indigo-500/20">
|
||||||
|
{userProfile && (
|
||||||
|
<Card className="w-max p-6 flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-bold">
|
||||||
|
{userProfile.firstName} {userProfile.lastName}
|
||||||
|
</p>
|
||||||
|
<p className="opacity-70">Ace Job Agency Member (Tier 3)</p>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<InfoCell label="Email" value={userProfile.email} />
|
||||||
|
<InfoCell
|
||||||
|
label="NRIC"
|
||||||
|
value={userProfile.nationalRegistrationIdentityCardNumber}
|
||||||
|
/>
|
||||||
|
<InfoCell
|
||||||
|
label="Date of Birth"
|
||||||
|
value={new Date(userProfile.dateOfBirth).toDateString()}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p>Resume</p>
|
||||||
|
<Button
|
||||||
|
variant="flat"
|
||||||
|
isDisabled={userProfile.resumeName.length <= 0}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p>Who am I</p>
|
||||||
|
<Card className="p-4 bg-neutral-500/20 h-full">
|
||||||
|
{userProfile.whoAmI.length > 0
|
||||||
|
? userProfile.whoAmI
|
||||||
|
: "You have not wrote anything about yourself."}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div className="flex flex-row justify-between w-full">
|
||||||
|
<Button variant="light" color="danger" onPress={logout}>
|
||||||
|
Log out
|
||||||
|
</Button>
|
||||||
|
<Button color="primary">Edit profile</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoCell({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p>{label}</p>
|
||||||
|
<Input value={value} readOnly />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user