Skip to content

Commit ff3e5d6

Browse files
committed
refactor: move profile info into a seperate component
1 parent b9bc6c5 commit ff3e5d6

File tree

5 files changed

+210
-186
lines changed

5 files changed

+210
-186
lines changed

backend/src/routes/profile.ts

Lines changed: 60 additions & 0 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

@@ -49,4 +51,62 @@ router.get("/:slug", async (req: Request, res: Response) => {
4951
}
5052
});
5153

54+
router.patch("/edit", async (req: Request, res: Response) => {
55+
try {
56+
const authenticatedUser = req.user as typeof users.$inferSelect;
57+
const { id } = authenticatedUser;
58+
59+
const { pfpUrl, atcoderHandle, leetcodeHandle, codechefHandle } = req.body;
60+
61+
if (
62+
pfpUrl === undefined &&
63+
atcoderHandle === undefined &&
64+
leetcodeHandle === undefined &&
65+
codechefHandle === undefined
66+
) {
67+
res.status(400).json({
68+
success: false,
69+
message: "No valid fields provided for update.",
70+
});
71+
return;
72+
}
73+
74+
const updateFields: Partial<typeof users.$inferInsert> = {};
75+
76+
if (pfpUrl !== undefined) updateFields.pfpUrl = pfpUrl;
77+
if (atcoderHandle !== undefined) updateFields.atcoderHandle = atcoderHandle;
78+
if (leetcodeHandle !== undefined)
79+
updateFields.leetcodeHandle = leetcodeHandle;
80+
if (codechefHandle !== undefined)
81+
updateFields.codechefHandle = codechefHandle;
82+
83+
await db.update(users).set(updateFields).where(eq(users.id, id));
84+
85+
res
86+
.status(200)
87+
.json({ success: true, message: "Profile updated successfully." });
88+
} catch (err: any) {
89+
const message = err.message || "";
90+
91+
if (message.includes("atcoder_handle")) {
92+
res
93+
.status(409)
94+
.json({ success: false, message: "AtCoder handle already in use." });
95+
return;
96+
} else if (message.includes("leetcode_handle")) {
97+
res
98+
.status(409)
99+
.json({ success: false, message: "LeetCode handle already in use." });
100+
return;
101+
} else if (message.includes("codechef_handle")) {
102+
res
103+
.status(409)
104+
.json({ success: false, message: "CodeChef handle already in use." });
105+
return;
106+
}
107+
108+
res.status(500).json({ success: false, message: "Internal server error." });
109+
}
110+
});
111+
52112
export default router;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React from "react";
2+
import ProfileAvatar from "./ProfileAvatar";
3+
import { getRatingColor, getRatingLevel } from "../utils";
4+
import type { User } from "../types/User";
5+
6+
interface ProfileInfoProps {
7+
profile: User | null;
8+
}
9+
10+
const ProfileInfo: React.FC<ProfileInfoProps> = ({ profile }) => {
11+
const cfRatingNumber = profile?.cfRating || 0;
12+
13+
return (
14+
<div className="mb-8">
15+
<div className="bg-highlight-dark rounded-lg p-6 transition-all duration-300 hover:shadow-lg">
16+
<div className="flex flex-col md:flex-row items-start md:items-center gap-6">
17+
{/* Profile Picture */}
18+
<div className="flex-shrink-0">
19+
<ProfileAvatar pfpUrl={profile?.pfpUrl || null} />
20+
</div>
21+
22+
{/* Profile Details */}
23+
<div className="flex-grow">
24+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
25+
<div className="space-y-4">
26+
<div className="group">
27+
<div className="text-sm text-gray-400 mb-1">Name</div>
28+
<div className="text-white font-medium text-lg group-hover:text-blue-400 transition-colors duration-300">
29+
{profile?.name || "N/A"}
30+
</div>
31+
</div>
32+
33+
<div className="group">
34+
<div className="text-sm text-gray-400 mb-1">Email</div>
35+
<div className="text-white font-medium break-all group-hover:text-blue-400 transition-colors duration-300">
36+
{profile?.email || "N/A"}
37+
</div>
38+
</div>
39+
</div>
40+
41+
<div className="space-y-4">
42+
{/* Codeforces Handle */}
43+
<div className="group">
44+
<div className="text-sm text-gray-400 mb-1">
45+
Codeforces Handle
46+
</div>
47+
<div className="text-white font-medium">
48+
{profile?.cfHandle ? (
49+
<a
50+
href={`https://codeforces.com/profile/${profile.cfHandle}`}
51+
target="_blank"
52+
rel="noopener noreferrer"
53+
className="hover:text-blue-300 transition-colors duration-300"
54+
>
55+
{profile.cfHandle}
56+
</a>
57+
) : (
58+
<span className="group-hover:text-blue-400 transition-colors duration-300">
59+
N/A
60+
</span>
61+
)}
62+
</div>
63+
</div>
64+
65+
{/* Codeforces Rating */}
66+
<div className="group">
67+
<div className="text-sm text-gray-400 mb-1">
68+
Codeforces Rating
69+
</div>
70+
<div
71+
className={`font-bold text-xl ${getRatingColor(cfRatingNumber)} group-hover:scale-105 transition-transform duration-300 inline-block animate-pulse`}
72+
>
73+
{profile?.cfRating ? (
74+
<span>
75+
{profile.cfRating}{" "}
76+
<span className="md:hidden">
77+
({getRatingLevel(profile.cfRating)})
78+
</span>
79+
</span>
80+
) : (
81+
"Unrated"
82+
)}
83+
</div>
84+
</div>
85+
86+
{/* Optional Handles */}
87+
{profile?.leetcodeHandle && (
88+
<div className="group">
89+
<div className="text-sm text-gray-400 mb-1">LeetCode</div>
90+
<a
91+
href={`https://leetcode.com/${profile.leetcodeHandle}`}
92+
target="_blank"
93+
rel="noopener noreferrer"
94+
className="text-white font-medium hover:text-yellow-400 transition-colors duration-300"
95+
>
96+
{profile.leetcodeHandle}
97+
</a>
98+
</div>
99+
)}
100+
101+
{profile?.codechefHandle && (
102+
<div className="group">
103+
<div className="text-sm text-gray-400 mb-1">CodeChef</div>
104+
<a
105+
href={`https://www.codechef.com/users/${profile.codechefHandle}`}
106+
target="_blank"
107+
rel="noopener noreferrer"
108+
className="text-white font-medium hover:text-purple-400 transition-colors duration-300"
109+
>
110+
{profile.codechefHandle}
111+
</a>
112+
</div>
113+
)}
114+
115+
{profile?.atcoderHandle && (
116+
<div className="group">
117+
<div className="text-sm text-gray-400 mb-1">AtCoder</div>
118+
<a
119+
href={`https://atcoder.jp/users/${profile.atcoderHandle}`}
120+
target="_blank"
121+
rel="noopener noreferrer"
122+
className="text-white font-medium hover:text-green-400 transition-colors duration-300"
123+
>
124+
{profile.atcoderHandle}
125+
</a>
126+
</div>
127+
)}
128+
</div>
129+
</div>
130+
</div>
131+
</div>
132+
</div>
133+
</div>
134+
);
135+
};
136+
137+
export default ProfileInfo;

frontend/src/routes/profile/$slug.tsx

Lines changed: 5 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
11
import { createFileRoute, useParams } from "@tanstack/react-router";
22
import axios from "axios";
33
import { useEffect, useState } from "react";
4-
import { getRatingColor, getRatingLevel, toTitleCase } from "../../utils";
5-
import ProfileAvatar from "../../components/ProfileAvatar";
4+
import { toTitleCase } from "../../utils";
65
import ProfileHeader from "../../components/ProfileHeader";
76
import RatingGraph from "../../components/RatingGraph";
87
import ProblemRatingBar from "../../components/ProblemRatingsBar";
98
import TagPieChart from "../../components/TagPieChart";
109
import StreakHeatmap from "../../components/StreakHeatMap";
1110
import ProgressLevel from "../../components/ProgressLevel";
1211
import LoadingIndicator from "../../components/LoadingIndicator";
13-
14-
interface Profile {
15-
name: string | null;
16-
email: string | null;
17-
pfpUrl: string | null;
18-
cfHandle: string | null;
19-
cfRating: number | null;
20-
}
12+
import ProfileInfo from "../../components/ProfileInfo";
13+
import type { User } from "../../types/User";
2114

2215
export const Route = createFileRoute("/profile/$slug")({
2316
component: RouteComponent,
2417
});
2518

2619
function RouteComponent() {
27-
const [profile, setProfile] = useState<Profile | null>(null);
20+
const [profile, setProfile] = useState<User | null>(null);
2821
const [loading, setLoading] = useState(true);
2922
const [error, setError] = useState<string | null>(null);
3023
const { slug } = useParams({ from: "/profile/$slug" });
@@ -45,12 +38,6 @@ function RouteComponent() {
4538
});
4639
}, []);
4740

48-
// Helper to ensure cfRating is number or null
49-
const cfRatingNumber =
50-
profile?.cfRating !== undefined && profile?.cfRating !== null
51-
? Number(profile.cfRating)
52-
: null;
53-
5441
if (loading) return <LoadingIndicator />;
5542

5643
if (error)
@@ -80,83 +67,7 @@ function RouteComponent() {
8067
</div>
8168
</div>
8269

83-
{/* Profile Information Section */}
84-
<div className="mb-8">
85-
<div className="bg-highlight-dark rounded-lg p-6 transition-all duration-300 hover:shadow-lg">
86-
<div className="flex flex-col md:flex-row items-start md:items-center gap-6">
87-
{/* Profile Picture */}
88-
<div className="flex-shrink-0">
89-
<ProfileAvatar pfpUrl={profile?.pfpUrl || null} />
90-
</div>
91-
92-
{/* Profile Details */}
93-
<div className="flex-grow">
94-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
95-
<div className="space-y-4">
96-
<div className="group">
97-
<div className="text-sm text-gray-400 mb-1">Name</div>
98-
<div className="text-white font-medium text-lg group-hover:text-blue-400 transition-colors duration-300">
99-
{profile?.name || "N/A"}
100-
</div>
101-
</div>
102-
103-
<div className="group">
104-
<div className="text-sm text-gray-400 mb-1">Email</div>
105-
<div className="text-white font-medium break-all group-hover:text-blue-400 transition-colors duration-300">
106-
{profile?.email || "N/A"}
107-
</div>
108-
</div>
109-
</div>
110-
111-
<div className="space-y-4">
112-
<div className="group">
113-
<div className="text-sm text-gray-400 mb-1">
114-
Codeforces Handle
115-
</div>
116-
<div className="text-white font-medium">
117-
{profile?.cfHandle ? (
118-
<a
119-
href={`https://codeforces.com/profile/${profile.cfHandle}`}
120-
target="_blank"
121-
rel="noopener noreferrer"
122-
className="hover:text-blue-300 transition-colors duration-300"
123-
>
124-
{profile.cfHandle}
125-
</a>
126-
) : (
127-
<span className="group-hover:text-blue-400 transition-colors duration-300">
128-
N/A
129-
</span>
130-
)}
131-
</div>
132-
</div>
133-
134-
<div className="group">
135-
<div className="text-sm text-gray-400 mb-1">
136-
Codeforces Rating
137-
</div>
138-
<div
139-
className={`font-bold text-xl ${getRatingColor(cfRatingNumber)} group-hover:scale-105 transition-transform duration-300 inline-block animate-pulse`}
140-
>
141-
{profile?.cfRating ? (
142-
<span>
143-
{`${profile?.cfRating}`}{" "}
144-
<span className="md:hidden">
145-
({getRatingLevel(profile.cfRating)})
146-
</span>
147-
</span>
148-
) : (
149-
"Unrated"
150-
)}
151-
</div>
152-
</div>
153-
</div>
154-
</div>
155-
</div>
156-
</div>
157-
</div>
158-
</div>
159-
70+
<ProfileInfo profile={profile} />
16071
{/* Progress Section */}
16172
<div>
16273
{profile?.cfRating && profile?.cfHandle && (

0 commit comments

Comments
 (0)