Skip to content

Commit b722c4e

Browse files
authored
Merge pull request #64 from crux-bphc/staging
First Release (v1.0.0)
2 parents 447892f + 1410ace commit b722c4e

File tree

11 files changed

+237
-160
lines changed

11 files changed

+237
-160
lines changed

.github/workflows/staging.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
name: Host a staging instance of Surge
22

3-
on: workflow_dispatch
3+
on:
4+
push:
5+
branches:
6+
- main
47

58
jobs:
69
staging:

frontend/src/components/ProblemRatingsBar.tsx renamed to frontend/src/components/Profile/ProblemRatingsBar.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,25 @@ interface Problem {
1313
rating?: number;
1414
}
1515

16+
const ProblemRatingBarSkeleton = () => {
17+
return (
18+
<div>
19+
<h2 className="text-white text-xl mb-4 mt-4">
20+
Problems <span className="text-gray-400">Solved</span>
21+
</h2>
22+
<div className="bg-highlight-dark p-4 md:p-6 rounded-lg text-sm md:text-sm">
23+
<div className="w-full h-[300px] bg-gray-600 rounded animate-pulse"></div>
24+
</div>
25+
</div>
26+
);
27+
};
28+
1629
export default function ProblemRatingBar({ handle }: { handle: string }) {
1730
const [data, setData] = useState<{ rating: number; count: number }[]>([]);
31+
const [isLoading, setIsLoading] = useState(true);
1832

1933
useEffect(() => {
34+
setIsLoading(true);
2035
axios
2136
.get(`${import.meta.env.VITE_API_BASE_URL}/account/${handle}/solved`, {
2237
withCredentials: true,
@@ -33,9 +48,16 @@ export default function ProblemRatingBar({ handle }: { handle: string }) {
3348
.sort((a, b) => a.rating - b.rating);
3449

3550
setData(result);
51+
})
52+
.finally(() => {
53+
setIsLoading(false);
3654
});
3755
}, [handle]);
3856

57+
if (isLoading) {
58+
return <ProblemRatingBarSkeleton />;
59+
}
60+
3961
return data.length ? (
4062
<div>
4163
<h2 className="text-white text-xl mb-4 mt-4">
File renamed without changes.

frontend/src/components/ProfileInfo.tsx renamed to frontend/src/components/Profile/ProfileInfo.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { useEffect, useState } from "react";
22
import { Pencil } from "lucide-react";
33
import ProfileAvatar from "./ProfileAvatar";
4-
import { getRatingColor } from "../utils";
5-
import { useAuth } from "../context/AuthContext";
6-
import EditProfileModal from "./EditProfileModal";
7-
import type { User } from "../types/User";
4+
import { getRatingColor } from "../../utils";
5+
import { useAuth } from "../../context/AuthContext";
6+
import EditProfileModal from "../EditProfileModal";
7+
import type { User } from "../../types/User";
88

99
const PLATFORM_LOGOS: Record<string, string> = {
1010
"codeforces.com": "/logos/codeforces.svg",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import axios from "axios";
2+
import { useEffect, useState } from "react";
3+
import LoadingIndicator from "../../components/LoadingIndicator";
4+
import StreakHeatmap from "../../components/StreakHeatMap";
5+
import type { User } from "../../types/User";
6+
import { toTitleCase } from "../../utils";
7+
import ProblemRatingBar from "./ProblemRatingsBar";
8+
import ProfileHeader from "../ProfileHeader";
9+
import ProfileInfo from "./ProfileInfo";
10+
import ProgressLevel from "./ProgressLevel";
11+
import RatingGraph from "./RatingGraph";
12+
import TagPieChart from "./TagPieChart";
13+
14+
export default function ProfilePage({ slug }: { slug?: string }) {
15+
const [profile, setProfile] = useState<User | null>(null);
16+
const [loading, setLoading] = useState(true);
17+
const [error, setError] = useState<string | null>(null);
18+
19+
useEffect(() => {
20+
axios
21+
.get(`${import.meta.env.VITE_API_BASE_URL}/profile/${slug ?? ""}`, {
22+
withCredentials: true,
23+
})
24+
.then((res) => {
25+
setProfile(res.data);
26+
setLoading(false);
27+
})
28+
.catch((err) => {
29+
console.error(err);
30+
setError("Failed to load profile.");
31+
setLoading(false);
32+
});
33+
}, [slug]);
34+
35+
if (loading) return <LoadingIndicator />;
36+
37+
if (error)
38+
return (
39+
<div className="min-h-screen text-white flex items-center justify-center">
40+
<div className="text-red-400 animate-pulse">{error}</div>
41+
</div>
42+
);
43+
44+
return (
45+
<div className="min-h-screen text-white">
46+
<div className="max-w-7xl mx-auto">
47+
{/* Header Section */}
48+
<div className="mb-8 border-b border-[#25293E]">
49+
<div className="flex items-center justify-between mb-6">
50+
<div>
51+
<h1 className="text-3xl font-bold ">
52+
{slug ? `${toTitleCase(profile?.name || undefined)}'s` : "Your"}{" "}
53+
<span className="text-highlight-lighter">Profile</span>
54+
</h1>
55+
</div>
56+
<ProfileHeader
57+
cfRating={profile?.cfRating}
58+
className="hidden md:flex"
59+
/>
60+
</div>
61+
</div>
62+
63+
<ProfileInfo profile={profile} />
64+
{/* Progress Section */}
65+
<div>
66+
{profile?.cfRating && profile?.cfHandle && (
67+
<ProgressLevel cfRating={profile?.cfRating} />
68+
)}
69+
{profile?.cfHandle && <StreakHeatmap handle={profile?.cfHandle} />}
70+
{profile?.cfRating && profile?.cfHandle && (
71+
<RatingGraph handle={profile?.cfHandle} />
72+
)}
73+
{profile?.cfHandle && <ProblemRatingBar handle={profile?.cfHandle} />}
74+
{profile?.cfHandle && <TagPieChart handle={profile?.cfHandle} />}
75+
</div>
76+
</div>
77+
</div>
78+
);
79+
}

frontend/src/components/ProgressLevel.tsx renamed to frontend/src/components/Profile/ProgressLevel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { getRatingLevel, getRatingBand } from "../utils";
2+
import { getRatingLevel, getRatingBand } from "../../utils";
33

44
interface ProgressLevelProps {
55
cfRating: number | null;

frontend/src/components/RatingGraph.tsx renamed to frontend/src/components/Profile/RatingGraph.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,34 @@ interface RatingPoint {
1515
newRating: number;
1616
}
1717

18+
const RatingGraphSkeleton = () => {
19+
return (
20+
<div>
21+
<h2 className="text-white text-xl mb-4 mt-4">
22+
Rating <span className="text-gray-400">Graph</span>
23+
</h2>
24+
<div className="bg-highlight-dark p-4 md:p-6 rounded-lg">
25+
<div className="w-full h-[300px] bg-gray-600 rounded animate-pulse"></div>
26+
</div>
27+
</div>
28+
);
29+
};
30+
1831
export default function RatingGraph({ handle }: { handle: string }) {
1932
const [data, setData] = useState<RatingPoint[]>([]);
33+
const [isLoading, setIsLoading] = useState(true);
2034

2135
useEffect(() => {
36+
setIsLoading(true);
2237
axios
2338
.get(`${import.meta.env.VITE_API_BASE_URL}/account/${handle}/ratings`, {
2439
withCredentials: true,
2540
})
2641
.then((res) => {
2742
setData(res.data);
43+
})
44+
.finally(() => {
45+
setIsLoading(false);
2846
});
2947
}, [handle]);
3048

@@ -33,6 +51,10 @@ export default function RatingGraph({ handle }: { handle: string }) {
3351
rating: d.newRating,
3452
}));
3553

54+
if (isLoading) {
55+
return <RatingGraphSkeleton />;
56+
}
57+
3658
return (
3759
<div>
3860
<h2 className="text-white text-xl mb-4 mt-4">

frontend/src/components/TagPieChart.tsx renamed to frontend/src/components/Profile/TagPieChart.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,33 @@ import { useEffect, useState } from "react";
22
import axios from "axios";
33
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
44

5+
const TagPieChartSkeleton = () => {
6+
return (
7+
<div>
8+
<h2 className="text-white text-xl mb-4 mt-4">
9+
Tags <span className="text-gray-400">Solved</span>
10+
</h2>
11+
<div className="bg-highlight-dark p-4 md:p-6 rounded-lg">
12+
<div className="w-full h-[400px] bg-gray-600 rounded animate-pulse"></div>
13+
</div>
14+
</div>
15+
);
16+
};
17+
518
export default function TagPieChart({ handle }: { handle: string }) {
619
const [data, setData] = useState<{ name: string; value: number }[]>([]);
20+
const [isLoading, setIsLoading] = useState(true);
721

822
useEffect(() => {
23+
setIsLoading(true);
924
axios
1025
.get(`${import.meta.env.VITE_API_BASE_URL}/account/${handle}/solved`, {
1126
withCredentials: true,
1227
})
1328
.then((res) => {
1429
const tagCount: Record<string, number> = {};
15-
res.data.forEach((p: any) => {
16-
p.tags.forEach((tag: string) => {
30+
res.data.forEach((p: { tags: string[] }) => {
31+
p.tags.forEach((tag) => {
1732
tagCount[tag] = (tagCount[tag] || 0) + 1;
1833
});
1934
});
@@ -23,6 +38,9 @@ export default function TagPieChart({ handle }: { handle: string }) {
2338
.sort((a, b) => b.value - a.value); // sort descending
2439

2540
setData(result);
41+
})
42+
.finally(() => {
43+
setIsLoading(false);
2644
});
2745
}, [handle]);
2846

@@ -53,6 +71,10 @@ export default function TagPieChart({ handle }: { handle: string }) {
5371
"#fda4af",
5472
];
5573

74+
if (isLoading) {
75+
return <TagPieChartSkeleton />;
76+
}
77+
5678
return data.length ? (
5779
<div>
5880
<h2 className="text-white text-xl mb-4 mt-4">

frontend/src/components/StreakHeatMap.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,73 @@ const getColor = (count: number) => {
2323
return "bg-gray-700";
2424
};
2525

26+
// Loading component for the heatmap
27+
const HeatmapSkeleton = () => {
28+
const weeks = Array.from({ length: 53 });
29+
const days = Array.from({ length: 7 });
30+
31+
return (
32+
<div className="mt-10">
33+
<h2 className="text-white text-xl mb-4">
34+
Codeforces <span className="text-gray-400">Submissions</span>
35+
</h2>
36+
37+
<div className="bg-highlight-dark p-4 rounded-lg">
38+
{/* Month labels skeleton */}
39+
<div className="grid grid-flow-col text-xs text-gray-300 mb-1 w-full md:ml-7 md:w-[calc(100%-1.75rem)] animate-pulse">
40+
{Array.from({ length: 12 }).map((_, i) => (
41+
<div key={i} className="text-center">
42+
<div className="bg-gray-600 h-3 w-6 rounded mx-auto"></div>
43+
</div>
44+
))}
45+
</div>
46+
47+
<div className="flex">
48+
{/* Day labels skeleton */}
49+
<div className="hidden md:flex flex-col justify-between text-xs text-gray-400 mr-2 w-5">
50+
{["", "Mon", "", "Wed", "", "Fri", ""].map((d, i) => (
51+
<div key={i} className="h-4">
52+
{d && (
53+
<div className="bg-gray-600 h-3 w-5 rounded animate-pulse"></div>
54+
)}
55+
</div>
56+
))}
57+
</div>
58+
59+
{/* Heatmap skeleton */}
60+
<div
61+
className="grid gap-[2px] w-full aspect-53/7 animate-pulse"
62+
style={{
63+
gridTemplateRows: "repeat(7, 1fr)",
64+
gridTemplateColumns: `repeat(53, 1fr)`,
65+
}}
66+
>
67+
{weeks.map((_, wi) =>
68+
days.map((_, di) => (
69+
<div
70+
key={`${wi}-${di}`}
71+
style={{ gridColumn: wi + 1, gridRow: di + 1 }}
72+
className="w-full h-full bg-gray-600 md:rounded-sm"
73+
/>
74+
))
75+
)}
76+
</div>
77+
</div>
78+
79+
{/* Stats skeleton */}
80+
<div className="grid grid-cols-3 grid-rows-2 gap-4 text-center text-white mt-6 animate-pulse">
81+
{Array.from({ length: 6 }).map((_, i) => (
82+
<div key={i}>
83+
<div className="bg-gray-600 h-6 w-8 rounded mx-auto mb-2"></div>
84+
<div className="bg-gray-600 h-3 w-20 rounded mx-auto"></div>
85+
</div>
86+
))}
87+
</div>
88+
</div>
89+
</div>
90+
);
91+
};
92+
2693
const computeStats = (days: string[], heatmap: Record<string, number>) => {
2794
const totalAll = Object.values(heatmap).reduce((sum, c) => sum + c, 0);
2895
let totalYear = 0,
@@ -79,8 +146,10 @@ export default function StreakHeatmap({ handle }: { handle: string }) {
79146
const [stats, setStats] = useState<ReturnType<typeof computeStats> | null>(
80147
null
81148
);
149+
const [isLoading, setIsLoading] = useState(true);
82150

83151
useEffect(() => {
152+
setIsLoading(true);
84153
axios
85154
.get(
86155
`${import.meta.env.VITE_API_BASE_URL}/account/${handle}/submissions`,
@@ -98,6 +167,9 @@ export default function StreakHeatmap({ handle }: { handle: string }) {
98167
const days = getPastYearDates();
99168
setHeatmap(map);
100169
setStats(computeStats(days, map));
170+
})
171+
.finally(async () => {
172+
setIsLoading(false);
101173
});
102174
}, [handle]);
103175

@@ -125,6 +197,11 @@ export default function StreakHeatmap({ handle }: { handle: string }) {
125197
i === 0 || m !== monthNums[i - 1] ? dayjs(weeks[i][0]).format("MMM") : ""
126198
);
127199

200+
// Show loading state while fetching data
201+
if (isLoading) {
202+
return <HeatmapSkeleton />;
203+
}
204+
128205
return stats?.totalAll ? (
129206
<div className="mt-10">
130207
<h2 className="text-white text-xl mb-4">

0 commit comments

Comments
 (0)