user profile page

This commit is contained in:
2025-02-09 10:33:52 +08:00
parent d21f94b168
commit 7edea0b967
7 changed files with 216 additions and 36 deletions

View File

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

View File

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

View File

@@ -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>
{() => ( {() => (

View File

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

View File

@@ -0,0 +1,10 @@
export interface UserProfile {
id: string;
email: string;
nationalRegistrationIdentityCardNumber: string;
firstName: string;
lastName: string;
dateOfBirth: string;
whoAmI: string;
resumeName: string;
}

View File

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

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