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 HomePage from "./pages/HomePage";
|
||||
import ErrorPage from "./pages/ErrorPage";
|
||||
import { getAccessToken } from "./http";
|
||||
import MemberPage from "./pages/MemberPage";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -9,7 +11,10 @@ export default function App() {
|
||||
<Route>
|
||||
<Route path="/">
|
||||
<Route element={<DefaultLayout />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route
|
||||
index
|
||||
element={getAccessToken() ? <MemberPage /> : <HomePage />}
|
||||
/>
|
||||
<Route path="*" element={<ErrorPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -1,7 +1,46 @@
|
||||
import { Input, Checkbox, Button, Link } from "@heroui/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 }) {
|
||||
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 (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col">
|
||||
@@ -11,8 +50,20 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input endContent={<IconMail />} label="Email" />
|
||||
<Input endContent={<IconLock />} label="Password" type="password" />
|
||||
<Input
|
||||
endContent={<IconMail />}
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onValueChange={setEmail}
|
||||
/>
|
||||
<Input
|
||||
endContent={<IconLock />}
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onValueChange={setPassword}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex py-2 px-1 justify-between">
|
||||
<Checkbox
|
||||
@@ -27,18 +78,12 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<Button color="primary" className="w-full">
|
||||
<Button color="primary" className="w-full" onPress={handleLogin}>
|
||||
Login
|
||||
</Button>
|
||||
<div className="flex flex-row gap-2 w-full justify-center *:my-auto">
|
||||
<p className="text-sm">Don't have an account?</p>
|
||||
<Link
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
onSignup();
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
<Link color="primary" onPress={onSignup} className="text-sm">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -16,11 +16,13 @@ import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import SignupView from "./SignupView";
|
||||
import LoginView from "./LoginView";
|
||||
import { getAccessToken } from "../http";
|
||||
|
||||
export default function NavigationBar() {
|
||||
const navigate = useNavigate();
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const [isSignup, setIsSignup] = useState(false);
|
||||
const accessToken = getAccessToken();
|
||||
|
||||
return (
|
||||
<Navbar className="border-b-[1px] border-neutral-500/25">
|
||||
@@ -39,30 +41,32 @@ export default function NavigationBar() {
|
||||
/>
|
||||
</Link>
|
||||
</NavbarBrand>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Button
|
||||
onPress={() => {
|
||||
setIsSignup(false);
|
||||
onOpen();
|
||||
}}
|
||||
variant="light"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setIsSignup(true);
|
||||
onOpen();
|
||||
}}
|
||||
variant="bordered"
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
{!accessToken && (
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Button
|
||||
onPress={() => {
|
||||
setIsSignup(false);
|
||||
onOpen();
|
||||
}}
|
||||
variant="light"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setIsSignup(true);
|
||||
onOpen();
|
||||
}}
|
||||
variant="bordered"
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
)}
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<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;
|
||||
|
||||
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,
|
||||
useDisclosure,
|
||||
} from "@heroui/react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import LoginView from "../components/LoginView";
|
||||
import SignupView from "../components/SignupView";
|
||||
import { getAccessToken } from "../http";
|
||||
|
||||
export default function HomePage() {
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const [isSignup, setIsSignup] = useState(false);
|
||||
const [emailValue, setEmailValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (getAccessToken()) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, []);
|
||||
|
||||
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="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