Skip to content

Commit 8d3479b

Browse files
authored
Merge pull request #51 from CS3219-AY2425S1/izzhafeez/cs3-29-m515-profile-page
Izzhafeez/cs3 29 m515 profile page
2 parents e21f1a3 + 80711a4 commit 8d3479b

File tree

12 files changed

+487
-32
lines changed

12 files changed

+487
-32
lines changed

frontend/package-lock.json

Lines changed: 62 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@hookform/resolvers": "^3.9.0",
1213
"@next/font": "^14.2.13",
1314
"@radix-ui/react-checkbox": "^1.1.1",
1415
"@radix-ui/react-dropdown-menu": "^2.1.1",
16+
"@radix-ui/react-label": "^2.1.0",
1517
"@radix-ui/react-slot": "^1.1.0",
1618
"@react-oauth/google": "^0.12.1",
1719
"@tanstack/react-table": "^8.20.5",
@@ -22,11 +24,13 @@
2224
"next": "14.2.11",
2325
"react": "^18",
2426
"react-dom": "^18",
27+
"react-hook-form": "^7.53.0",
2528
"react-icons": "^5.3.0",
2629
"react-pro-sidebar": "^1.1.0",
2730
"sweetalert2": "^11.14.1",
2831
"tailwind-merge": "^2.5.2",
29-
"tailwindcss-animate": "^1.0.7"
32+
"tailwindcss-animate": "^1.0.7",
33+
"zod": "^3.23.8"
3034
},
3135
"devDependencies": {
3236
"@types/js-cookie": "^3.0.6",

frontend/src/api/user.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { User } from "@/types/user";
2+
3+
export const setGetProfile = async (access_token: string, user: User): Promise<User> => {
4+
const username = user.username;
5+
const bio = user.bio;
6+
const linkedin = user.linkedin;
7+
const github = user.github;
8+
9+
// POST request
10+
// url from environment variable
11+
const url = process.env.REACT_APP_USER_URL + "/";
12+
const response = await fetch(url, {
13+
method: "POST",
14+
headers: {
15+
"Content-Type": "application/json",
16+
Authorization: `Bearer ${access_token}`,
17+
},
18+
body: JSON.stringify({
19+
username,
20+
bio,
21+
linkedin,
22+
github,
23+
}),
24+
});
25+
26+
// return response.json();
27+
return {
28+
username: "Hong Shan",
29+
bio: "I live in Redhill",
30+
linkedin: "www.linkedin.com/in/hongshan",
31+
github: "www.github.com/hongshan",
32+
}
33+
}

frontend/src/app/(auth)/dashboard/pages/Dashboard.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@ const initialUserInterviewMetaData: UserInterviewMetadata = {
2121
};
2222

2323
const AuthDashboard = () => {
24-
const { user } = useAuth();
24+
const { token } = useAuth();
2525

2626
const [userInterviewMetadata, setUserInterviewMetadata] =
2727
useState<UserInterviewMetadata>(initialUserInterviewMetaData);
2828

2929
useEffect(() => {
30-
if (!user) return;
31-
30+
if (!token) return;
3231
setUserInterviewMetadata(getUserInterviewMetadata());
33-
}, [user]);
32+
}, [token]);
3433

3534
const dateFormattingOption: Intl.DateTimeFormatOptions = {
3635
year: "numeric",

frontend/src/app/(auth)/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const menuItemStyles: MenuItemStyles = {
6464
const Layout = ({ children }: { children: ReactNode }) => {
6565
const [isHovered, setIsHovered] = useState(false);
6666

67-
const { user, login, logout } = useAuth();
67+
const { token, login, logout } = useAuth();
6868
const router = useRouter();
6969
const pathname = usePathname();
7070

@@ -105,7 +105,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
105105

106106
return (<div className="flex h-full overflow-y-auto">
107107
<Sidebar
108-
className="sticky top-0 h-full"
108+
className="sticky top-0 h-screen"
109109
rootStyles={{
110110
borderColor: "#171C28",
111111
}}
@@ -127,7 +127,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
127127
/>
128128
))}
129129
</Menu>
130-
{user && <Menu
130+
{token && <Menu
131131
menuItemStyles={menuItemStyles}
132132
rootStyles={{
133133
marginBottom: "60px",

frontend/src/app/(auth)/page.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import LandingPage from "./dashboard/pages/LandingPage";
55
import Dashboard from "./dashboard/pages/Dashboard";
66

77
const Home = () => {
8-
const { user } = useAuth();
9-
console.log(user);
8+
const { token } = useAuth();
109

1110
return (
12-
user?.access_token ? <Dashboard/> : <LandingPage/>
11+
token ? <Dashboard/> : <LandingPage/>
1312
);
1413
};
1514

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"use client";
2+
3+
import { useAuth } from "@/components/auth/AuthContext";
4+
import { Button } from "@/components/ui/button";
5+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
6+
import { Input } from "@/components/ui/input";
7+
import { useForm } from "react-hook-form";
8+
import { setGetProfile } from "@/api/user";
9+
import { useEffect, useState } from "react";
10+
import { User } from "@/types/user";
11+
import { z } from "zod";
12+
import { zodResolver } from "@hookform/resolvers/zod";
13+
import Swal from "sweetalert2";
14+
15+
const formSchema = z.object({
16+
username: z.string()
17+
.min(5, "Username must be at least 5 characters"),
18+
bio: z.string(),
19+
linkedin: z.string()
20+
.refine((val) => val.length == 0 || val.includes("linkedin.com/in/"),
21+
{ message: "Invalid URL" }),
22+
github: z.string()
23+
.refine((val) => val.length == 0 || val.includes("github.com/"),
24+
{ message: "Invalid URL" }),
25+
});
26+
27+
const ProfilePage = () => {
28+
const { token } = useAuth();
29+
const [user, setUser] = useState<User>({});
30+
31+
const form = useForm<z.infer<typeof formSchema>>({
32+
resolver: zodResolver(formSchema),
33+
defaultValues: {
34+
username: "",
35+
bio: "",
36+
linkedin: "",
37+
github: "",
38+
},
39+
});
40+
41+
useEffect(() => {
42+
setGetProfile(token, {}).then((data) => {
43+
setUser(data);
44+
form.reset(data);
45+
}).catch((error) => {
46+
console.error("Profile Fetch Failed:", error);
47+
Swal.fire({
48+
icon: "error",
49+
title: "Profile Fetch Failed",
50+
text: "Please try again later",
51+
});
52+
});
53+
}, [token, form]);
54+
55+
const onSubmit = (data: z.infer<typeof formSchema>) => {
56+
setGetProfile(token, data).then((data) => {
57+
setUser(data);
58+
form.reset(data);
59+
Swal.fire({
60+
icon: "success",
61+
title: "Profile Updated",
62+
text: "Your profile has been updated successfully",
63+
});
64+
}).catch((error) => {
65+
console.error("Profile Update Failed:", error);
66+
Swal.fire({
67+
icon: "error",
68+
title: "Profile Update Failed",
69+
text: "Please try again later",
70+
})
71+
});
72+
};
73+
74+
return (
75+
<div className="mx-auto max-w-xl my-10 p-4">
76+
<h1 className="text-white font-extrabold text-h1">Welcome, {user?.username}!</h1>
77+
78+
<Form {...form}>
79+
<form className="my-10 grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
80+
<FormField
81+
control={form.control}
82+
name="username"
83+
render={({ field }) => (
84+
<FormItem>
85+
<FormLabel className="text-yellow-500 text-lg">USERNAME</FormLabel>
86+
<FormControl>
87+
<Input placeholder="username" {...field} className="focus:border-yellow-500 text-white"/>
88+
</FormControl>
89+
{/* <FormDescription>This is your public display name.</FormDescription> */}
90+
<FormMessage/>
91+
</FormItem>
92+
)}
93+
/>
94+
<FormField
95+
control={form.control}
96+
name="bio"
97+
render={({ field }) => (
98+
<FormItem>
99+
<FormLabel className="text-yellow-500 text-lg">BIO</FormLabel>
100+
<FormControl>
101+
<Input placeholder="I am a..." {...field} className="focus:border-yellow-500 text-white"/>
102+
</FormControl>
103+
{/* <FormDescription>This is your public display name.</FormDescription> */}
104+
<FormMessage/>
105+
</FormItem>
106+
)}
107+
/>
108+
<FormField
109+
control={form.control}
110+
name="linkedin"
111+
render={({ field }) => (
112+
<FormItem>
113+
<FormLabel className="text-yellow-500 text-lg">LINKEDIN URL</FormLabel>
114+
<FormControl>
115+
<Input placeholder="https://www.linkedin.com/in/..." {...field} className="focus:border-yellow-500 text-white"/>
116+
</FormControl>
117+
{/* <FormDescription>This is your public display name.</FormDescription> */}
118+
<FormMessage/>
119+
</FormItem>
120+
)}
121+
/>
122+
<FormField
123+
control={form.control}
124+
name="github"
125+
render={({ field }) => (
126+
<FormItem>
127+
<FormLabel className="text-yellow-500 text-lg">GITHUB URL</FormLabel>
128+
<FormControl>
129+
<Input placeholder="https://github.com/..." {...field} className="focus:border-yellow-500 text-white"/>
130+
</FormControl>
131+
{/* <FormDescription>This is your public display name.</FormDescription> */}
132+
<FormMessage/>
133+
</FormItem>
134+
)}
135+
/>
136+
<Button type="submit" className="bg-yellow-500 hover:bg-yellow-300 px-4 py-2 my-2 rounded-md text-black">Save Changes</Button>
137+
</form>
138+
</Form>
139+
140+
</div>
141+
);
142+
}
143+
144+
export default ProfilePage;

0 commit comments

Comments
 (0)