Skip to content

Commit 64f0f6e

Browse files
authored
Merge pull request #56 from crux-bphc/feat/edit-profile
Feat/edit profile
2 parents af626a8 + eee7a6d commit 64f0f6e

File tree

10 files changed

+398
-194
lines changed

10 files changed

+398
-194
lines changed

backend/src/routes/profile.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { Request, Response } from "express";
33
import { requireAuth } from "../middlewares/auth";
44
import { getUserByHandle } from "../utils/users";
55
import { users } from "../drizzle/schema";
6+
import { eq } from "drizzle-orm";
7+
import { db } from "../drizzle/db";
68

79
const router = express.Router();
810

@@ -23,13 +25,7 @@ router.get("/", async (req, res) => {
2325
res.status(500).json({ success: false, message: "User not found!" });
2426
return;
2527
}
26-
res.status(200).json({
27-
name,
28-
email,
29-
pfpUrl,
30-
cfHandle,
31-
cfRating: userData.cfRating,
32-
});
28+
res.status(200).json(userData);
3329
} catch (err: any) {
3430
res.status(500).json({ success: false, message: err.message });
3531
}
@@ -49,4 +45,62 @@ router.get("/:slug", async (req: Request, res: Response) => {
4945
}
5046
});
5147

48+
router.patch("/edit", async (req: Request, res: Response) => {
49+
try {
50+
const authenticatedUser = req.user as typeof users.$inferSelect;
51+
const { id } = authenticatedUser;
52+
53+
const { pfpUrl, atcoderHandle, leetcodeHandle, codechefHandle } = req.body;
54+
55+
if (
56+
pfpUrl === undefined &&
57+
atcoderHandle === undefined &&
58+
leetcodeHandle === undefined &&
59+
codechefHandle === undefined
60+
) {
61+
res.status(400).json({
62+
success: false,
63+
message: "No valid fields provided for update.",
64+
});
65+
return;
66+
}
67+
68+
const updateFields: Partial<typeof users.$inferInsert> = {};
69+
70+
if (pfpUrl !== undefined) updateFields.pfpUrl = pfpUrl;
71+
if (atcoderHandle !== undefined) updateFields.atcoderHandle = atcoderHandle;
72+
if (leetcodeHandle !== undefined)
73+
updateFields.leetcodeHandle = leetcodeHandle;
74+
if (codechefHandle !== undefined)
75+
updateFields.codechefHandle = codechefHandle;
76+
77+
await db.update(users).set(updateFields).where(eq(users.id, id));
78+
79+
res
80+
.status(200)
81+
.json({ success: true, message: "Profile updated successfully." });
82+
} catch (err: any) {
83+
const message = err.message || "";
84+
85+
if (message.includes("atcoder_handle")) {
86+
res
87+
.status(409)
88+
.json({ success: false, message: "AtCoder handle already in use." });
89+
return;
90+
} else if (message.includes("leetcode_handle")) {
91+
res
92+
.status(409)
93+
.json({ success: false, message: "LeetCode handle already in use." });
94+
return;
95+
} else if (message.includes("codechef_handle")) {
96+
res
97+
.status(409)
98+
.json({ success: false, message: "CodeChef handle already in use." });
99+
return;
100+
}
101+
102+
res.status(500).json({ success: false, message: "Internal server error." });
103+
}
104+
});
105+
52106
export default router;
19.7 KB
Loading

frontend/public/logos/leetcode.png

39.8 KB
Loading
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { useState } from "react";
2+
import { X, Loader, CheckCircle } from "lucide-react";
3+
import axios from "axios";
4+
import type { User } from "../types/User";
5+
6+
interface EditProfileModalProps {
7+
initialValues: {
8+
pfpUrl: string;
9+
atcoderHandle: string;
10+
leetcodeHandle: string;
11+
codechefHandle: string;
12+
};
13+
onClose: () => void;
14+
onSuccess: (updatedUser: User) => void;
15+
}
16+
17+
export default function EditProfileModal({
18+
initialValues,
19+
onClose,
20+
onSuccess,
21+
}: EditProfileModalProps) {
22+
const [formData, setFormData] = useState(initialValues);
23+
const [loading, setLoading] = useState(false);
24+
const [error, setError] = useState<string | null>(null);
25+
const [success, setSuccess] = useState(false);
26+
27+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
28+
const { name, value } = e.target;
29+
setFormData((prev) => ({ ...prev, [name]: value }));
30+
};
31+
32+
const handleSubmit = async (e: React.FormEvent) => {
33+
e.preventDefault();
34+
setLoading(true);
35+
setError(null);
36+
37+
try {
38+
await axios.patch(
39+
`${import.meta.env.VITE_API_BASE_URL}/profile/edit`,
40+
formData,
41+
{ withCredentials: true }
42+
);
43+
44+
const res = await axios.get(
45+
`${import.meta.env.VITE_API_BASE_URL}/profile`,
46+
{ withCredentials: true }
47+
);
48+
49+
setSuccess(true);
50+
setTimeout(() => {
51+
onSuccess(res.data);
52+
}, 1500);
53+
} catch (err: any) {
54+
setError(err.response?.data?.message || "Failed to update profile.");
55+
} finally {
56+
setLoading(false);
57+
}
58+
};
59+
60+
return (
61+
<div
62+
className="fixed inset-0 bg-dark-background flex items-center justify-center z-50 shadow-5xl"
63+
role="dialog"
64+
aria-modal="true"
65+
aria-labelledby="edit-profile-title"
66+
>
67+
<div className="bg-highlight-dark text-white p-6 rounded-lg w-[90%] max-w-lg shadow-xl relative">
68+
<button
69+
className="absolute top-3 right-3 text-gray-400 hover:text-white hover:cursor-pointer"
70+
onClick={onClose}
71+
>
72+
<X size={20} />
73+
</button>
74+
75+
<h2 className="text-xl mb-6 font-semibold">Edit Your Profile</h2>
76+
77+
{success ? (
78+
<div className="flex flex-col items-center text-center">
79+
<CheckCircle className="w-12 h-12 text-green-400 mb-4" />
80+
<p className="text-lg font-semibold text-green-300">
81+
Profile updated successfully!
82+
</p>
83+
<p className="text-sm text-gray-300 mt-2">Closing shortly...</p>
84+
</div>
85+
) : (
86+
<form onSubmit={handleSubmit} className="space-y-4">
87+
<InputField
88+
label="Profile Picture URL"
89+
name="pfpUrl"
90+
value={formData.pfpUrl}
91+
onChange={handleChange}
92+
/>
93+
<InputField
94+
label="AtCoder Handle"
95+
name="atcoderHandle"
96+
value={formData.atcoderHandle}
97+
onChange={handleChange}
98+
/>
99+
<InputField
100+
label="LeetCode Handle"
101+
name="leetcodeHandle"
102+
value={formData.leetcodeHandle}
103+
onChange={handleChange}
104+
/>
105+
<InputField
106+
label="CodeChef Handle"
107+
name="codechefHandle"
108+
value={formData.codechefHandle}
109+
onChange={handleChange}
110+
/>
111+
112+
{error && <p className="text-red-400 text-sm">{error}</p>}
113+
114+
<button
115+
type="submit"
116+
className="bg-highlight-light hover:bg-gray-700 hover:cursor-pointer text-white px-4 py-2 rounded w-full transition"
117+
disabled={loading}
118+
>
119+
{loading ? (
120+
<span className="flex items-center justify-center gap-2">
121+
<Loader className="w-4 h-4 animate-spin" /> Saving...
122+
</span>
123+
) : (
124+
"Save Changes"
125+
)}
126+
</button>
127+
</form>
128+
)}
129+
</div>
130+
</div>
131+
);
132+
}
133+
134+
function InputField({
135+
label,
136+
name,
137+
value,
138+
onChange,
139+
}: {
140+
label: string;
141+
name: string;
142+
value: string;
143+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
144+
}) {
145+
return (
146+
<div>
147+
<label className="block text-sm text-gray-400 mb-1" htmlFor={name}>
148+
{label}
149+
</label>
150+
<input
151+
id={name}
152+
name={name}
153+
value={value}
154+
onChange={onChange}
155+
className="w-full px-3 py-2 bg-dark-background text-white rounded border border-gray-700 focus:outline-none focus:ring-2 focus:ring-highlight-light"
156+
/>
157+
</div>
158+
);
159+
}

frontend/src/components/ProfileAvatar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ type ProfileAvatarProps = {
66

77
const ProfileAvatar: React.FC<ProfileAvatarProps> = ({ pfpUrl }) => (
88
<div className="relative group">
9-
<div className="absolute inset-0 rounded-full bg-gradient-to-tr from-highlight-light via-purple-400 to-accent-red blur-sm opacity-60 group-hover:opacity-80 transition-opacity duration-300"></div>
109
<img
1110
src={pfpUrl || "/logo-new.png"}
1211
alt="Profile"

0 commit comments

Comments
 (0)