Skip to content

Commit bb1e83e

Browse files
authored
Link profile service to backend (#61)
1 parent dc61b09 commit bb1e83e

File tree

9 files changed

+229
-61
lines changed

9 files changed

+229
-61
lines changed

frontend/src/app/profile/_components/EditProfile.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import {
44
Dialog,
55
DialogContent,
6-
DialogDescription,
76
DialogHeader,
87
DialogTitle,
98
} from "@/components/ui/dialog";
@@ -20,6 +19,8 @@ import { RadioGroupInput } from "@/components/form/RadioGroupInput";
2019
import { useToast } from "@/hooks/use-toast";
2120
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
2221
import { CodeXml } from "lucide-react";
22+
import { updateProfile } from "@/services/profileService";
23+
import { useRouter } from "next/navigation";
2324

2425
interface EditProfileModalProps {
2526
isOpen: boolean;
@@ -36,7 +37,7 @@ const FormSchema = z.object({
3637
displayName: z.string().min(1, "Display Name is required"),
3738
username: z.string().min(1, "Username is required"),
3839
email: z.string().email("Invalid email format"),
39-
proficiency: z.enum(["Beginner", "Intermediate", "Expert"]),
40+
proficiency: z.enum(["Beginner", "Intermediate", "Advanced"]),
4041
});
4142

4243
export function EditProfile({
@@ -45,6 +46,7 @@ export function EditProfile({
4546
userProfile,
4647
}: EditProfileModalProps) {
4748
const { toast } = useToast();
49+
const router = useRouter();
4850

4951
const form = useForm<z.infer<typeof FormSchema>>({
5052
resolver: zodResolver(FormSchema),
@@ -57,14 +59,26 @@ export function EditProfile({
5759

5860
const onSubmit = useCallback(
5961
async (data: z.infer<typeof FormSchema>) => {
60-
console.log("Form data:", data);
61-
toast({
62-
title: "Profile updated!",
63-
description: "Your profile has been updated successfully.",
64-
});
65-
setIsOpen(false);
62+
const response = await updateProfile(data);
63+
64+
if (response.statusCode !== 200) {
65+
toast({
66+
variant: "destructive",
67+
title: "Error updating profile",
68+
description: response.message,
69+
});
70+
return;
71+
} else {
72+
toast({
73+
title: "Profile updated!",
74+
description: "Your profile has been updated successfully.",
75+
});
76+
setIsOpen(false);
77+
router.refresh();
78+
}
6679
},
67-
[toast, setIsOpen]
80+
81+
[toast, setIsOpen, router]
6882
);
6983

7084
return (
@@ -78,8 +92,8 @@ export function EditProfile({
7892
className="flex flex-col space-y-4"
7993
>
8094
{/* Profile Image Upload */}
81-
<FormLabel className="pt-8">Profile Image</FormLabel>
82-
<div className="flex flex-row justify-center items-center p-2">
95+
{/*<FormLabel className="pt-8">Profile Image</FormLabel>
96+
<div className="flex flex-row justify-center items-center p-2">
8397
<input
8498
type="file"
8599
className="hidden"
@@ -103,7 +117,7 @@ export function EditProfile({
103117
</label>
104118
<DialogDescription className="pt-2">.png, .jpeg files up to 2MB. Recommended size is 256x256px.</DialogDescription>
105119
</div>
106-
</div>
120+
</div> */}
107121

108122
{/* Display Name */}
109123
<TextInput label="Display Name" name="displayName" placeholder="Display Name" />
@@ -117,7 +131,7 @@ export function EditProfile({
117131
)}
118132

119133
{/* Email */}
120-
<TextInput label="Email" name="email" placeholder="Email" />
134+
{/* <TextInput label="Email" name="email" placeholder="Email" /> */}
121135

122136
{/* Proficiency Radio Buttons */}
123137
<RadioGroupInput
@@ -126,7 +140,7 @@ export function EditProfile({
126140
options={[
127141
{ value: "Beginner", optionLabel: "Beginner" },
128142
{ value: "Intermediate", optionLabel: "Intermediate" },
129-
{ value: "Expert", optionLabel: "Expert" },
143+
{ value: "Advanced", optionLabel: "Advanced" },
130144
]}
131145
/>
132146

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client'; // Make only this component a client component
2+
3+
import { useState } from 'react';
4+
import { EditProfile } from './EditProfile';
5+
import { Profile } from '@/types/Profile';
6+
7+
interface EditProfileButtonProps {
8+
profileDetails: Profile;
9+
}
10+
11+
export default function EditProfileButton({ profileDetails }: EditProfileButtonProps) {
12+
const [isOpen, setIsOpen] = useState(false);
13+
14+
return (
15+
<>
16+
<button
17+
onClick={() => setIsOpen(true)}
18+
className="bg-primary text-sm font-semibold py-2 rounded-md"
19+
>
20+
Edit Profile
21+
</button>
22+
23+
{/* Edit Profile Modal */}
24+
{isOpen && (
25+
<EditProfile
26+
isOpen={isOpen}
27+
setIsOpen={setIsOpen}
28+
userProfile={profileDetails}
29+
/>
30+
)}
31+
</>
32+
);
33+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import UserAvatar from "@/components/UserAvatar";
2+
import { Card } from "@/components/ui/card";
3+
import { Zap } from "lucide-react";
4+
import { Profile } from "@/types/Profile";
5+
6+
interface ProfileCardProps {
7+
profile: Profile
8+
}
9+
10+
export function ProfileCard({profile}: ProfileCardProps) {
11+
return (
12+
<Card className="p-5">
13+
<div className="flex flex-col gap-5">
14+
<div className="flex items-center gap-4">
15+
<UserAvatar
16+
src={"https://non-existent.com"}
17+
name={"Jm San Diego"}
18+
isHoverEnabled={false}
19+
className="w-16 h-16"
20+
/>
21+
<div>
22+
<h4 className="leading-none">{profile.displayName}</h4>
23+
<small className="inline-block mb-1 text-card-foreground-100">@{profile.username}</small>
24+
<p>
25+
Proficiency:{" "}
26+
<span className="text-card-foreground-100">{profile.proficiency}</span>
27+
</p>
28+
<p>
29+
Elo:{" "}
30+
<span className="text-primary">
31+
1000 <Zap className="inline" size={14} />
32+
</span>
33+
</p>
34+
</div>
35+
</div>
36+
</div>
37+
</Card>
38+
);
39+
}

frontend/src/app/profile/_components/QuestionHistory/QuestionColumns/DurationColumn.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const DurationColumn: ColumnDef<Question> = {
1717
const question = QuestionSchema.parse(row.original);
1818

1919
// Placeholder duration data
20-
const tempDuration = Math.floor(Math.random() * 60) + 1; // Choose random duration value
20+
const tempDuration = 20; // Choose random duration value
2121

2222
return (
2323
<span>
Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,42 @@
1-
"use client"
2-
import { useState } from "react";
3-
import { ProfileMiniDetailsCard } from "@/components/ProfileMiniDetailsCard";
41
import { Card } from "@/components/ui/card";
52
import { BookOpen, CodeXml, Glasses, Mail } from "lucide-react";
63
import { EditProfile } from "./EditProfile";
4+
import { getProfile } from "@/services/profileService";
5+
import { Profile, ProfileResponse } from "@/types/Profile";
6+
import EditProfileButton from "./EditProfileButton";
7+
import { ProfileCard } from "./ProfileCard";
78

8-
export default function SideContents() {
9-
const [isOpen, setIsOpen] = useState(false);
9+
export default async function SideContents() {
10+
const profileResponse: ProfileResponse = await getProfile();
11+
if (!profileResponse.data) {
12+
return <div>Unable to load user profile</div>;
13+
}
1014

11-
const userProfile = {
12-
displayName: "John Doe",
13-
username: "johndoe123",
14-
15-
proficiency: "Expert",
16-
};
15+
const profileDetails: Profile = profileResponse.data
1716

1817
return (
1918
<Card className="p-4 w-full max-w-xs rounded-lg">
20-
<ProfileMiniDetailsCard />
19+
<ProfileCard profile={profileDetails} />
2120

2221
<div className="flex flex-col gap-2 mb-6">
23-
<button
24-
onClick={() => setIsOpen(true)}
25-
className="bg-primary text-sm font-semibold py-2 rounded-md"
26-
>
27-
Edit Profile
28-
</button>
29-
<button className="bg-primary text-sm font-semibold py-2 rounded-md">
22+
<EditProfileButton profileDetails={profileDetails} />
23+
{/* <button className="bg-primary text-sm font-semibold py-2 rounded-md">
3024
Change Password
31-
</button>
25+
</button> */}
3226
</div>
3327

3428
{/* User Info */}
3529
<div>
3630
<div className="flex flex-row gap-2 p-1">
3731
<Mail className="text-primary" />
3832
<h4>Email:</h4>
39-
<h4 className="text-card-foreground-100">[email protected]</h4>
40-
</div>
41-
42-
<div className="flex flex-row gap-2 p-1">
43-
<Glasses className="text-primary" />
44-
<h4>Proficiency:</h4>
45-
<h4 className="text-card-foreground-100">Expert</h4>
33+
<h4 className="text-card-foreground-100">{profileDetails.email}</h4>
4634
</div>
4735

4836
<div className="flex flex-row gap-2 p-1">
4937
<CodeXml className="text-primary" />
5038
<h4>Language:</h4>
51-
<h4 className="text-card-foreground-100">Java</h4>
39+
<h4 className="text-card-foreground-100">{profileDetails.languages.join(", ")}</h4>
5240
</div>
5341

5442
<div className="flex flex-row gap-2 p-1">
@@ -57,30 +45,27 @@ export default function SideContents() {
5745
</div>
5846

5947
<div className="flex flex-col gap-2 p-2">
60-
<TopicPreference title="Two Pointers" />
61-
<TopicPreference title="Dynamic Programming" />
48+
<TopicPreference title="Two Pointers" />
49+
<TopicPreference title="Dynamic Programming" />
6250
</div>
63-
6451
</div>
6552

6653
{/* Edit Profile Modal */}
67-
<EditProfile
54+
{/* <EditProfile
6855
isOpen={isOpen}
6956
setIsOpen={setIsOpen}
7057
userProfile={userProfile}
71-
/>
58+
/> */}
7259
</Card>
7360
);
7461
}
7562

7663
function TopicPreference({ title }: { title: string }) {
77-
return(
78-
<Card>
79-
<div className="flex flex-wrap gap-2">
80-
<label
81-
className="px-3 py-1 rounded-md bg-background-200 text-primary"
82-
>{title}</label>
83-
</div>
84-
</Card>
85-
)
86-
}
64+
return (
65+
<Card>
66+
<div className="flex flex-wrap gap-2">
67+
<label className="px-3 py-1 rounded-md bg-background-200 text-primary">{title}</label>
68+
</div>
69+
</Card>
70+
);
71+
}

frontend/src/components/ProfileMiniDetailsCard.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "@/components/ui/hover-card";
88
import { Card } from "@/components/ui/card";
99
import { Zap } from "lucide-react";
10+
import ViewProfileButton from "./ViewProfileButton";
1011

1112
interface ProfileMiniDetailsProps {
1213
isViewProfileEnabled?: boolean;
@@ -40,9 +41,7 @@ function ProfileMiniDetails({
4041
</div>
4142
</div>
4243
{isViewProfileEnabled && (
43-
<Button variant="soft" className="w-full">
44-
View Profile
45-
</Button>
44+
<ViewProfileButton />
4645
)}
4746
</div>
4847
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client"
2+
import { useRouter } from "next/navigation";
3+
import { Button } from "./ui/button";
4+
5+
export default function ViewProfileButton() {
6+
const router = useRouter();
7+
return (
8+
<Button
9+
variant="soft"
10+
className="w-full"
11+
onClick={() => {
12+
router.push("/profile");
13+
}}
14+
>
15+
View Profile
16+
</Button>
17+
);
18+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use server";
2+
import { getAccessToken } from "@/lib/auth";
3+
import { Profile, ProfileResponse, ProfileResponseSchema, ProfileSchema, UpdateProfile, UpdateProfileSchema } from "@/types/Profile";
4+
5+
export async function getProfile(): Promise<ProfileResponse> {
6+
try {
7+
const access_token = await getAccessToken();
8+
9+
const res = await fetch(process.env.PUBLIC_API_URL + `/api/users/current`, {
10+
method: "GET",
11+
headers: {
12+
"Content-Type": "application/json",
13+
Authorization: `Bearer ${access_token}`,
14+
},
15+
});
16+
17+
const data = await res.json();
18+
19+
return ProfileResponseSchema.parse(data);
20+
} catch (error) {
21+
return {
22+
statusCode: 500,
23+
message: String(error),
24+
};
25+
}
26+
}
27+
28+
export async function updateProfile(profile: UpdateProfile): Promise<ProfileResponse> {
29+
try {
30+
const access_token = await getAccessToken();
31+
const updatedProfileDetails = UpdateProfileSchema.parse(profile);
32+
const res = await fetch(process.env.PUBLIC_API_URL + `/api/users/profile`, {
33+
method: "PATCH",
34+
headers: {
35+
"Content-Type": "application/json",
36+
Authorization: `Bearer ${access_token}`,
37+
},
38+
body: JSON.stringify(updatedProfileDetails),
39+
});
40+
41+
const data = await res.json();
42+
43+
return ProfileResponseSchema.parse(data);
44+
} catch (error) {
45+
return {
46+
statusCode: 500,
47+
message: String(error),
48+
};
49+
}
50+
}

0 commit comments

Comments
 (0)