Skip to content

Commit 3e33e16

Browse files
Merge pull request #4 from Advik-Gupta/main
Changed UI + Connected Timer + Analytics Section
2 parents d538973 + 78175ed commit 3e33e16

36 files changed

+4699
-1103
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
"react-hot-toast": "^2.4.1",
4242
"react-icons": "^5.3.0",
4343
"react-markdown": "^9.0.1",
44+
"recharts": "^3.2.1",
45+
"shadcn": "3.3.1",
4446
"tailwind-merge": "^2.5.2",
4547
"tailwindcss-animate": "^1.0.7",
4648
"zod": "^3.23.8"

pnpm-lock.yaml

Lines changed: 2448 additions & 55 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/analytics.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { handleAPIError } from "@/lib/error";
2+
import api from ".";
3+
4+
export interface AnalyticsResponse {
5+
status: string;
6+
total_users: number;
7+
total_submissions: number;
8+
round_wise: Record<
9+
string,
10+
Array<{
11+
question_id: string;
12+
submissions_made: number;
13+
}>
14+
>;
15+
language_wise: Record<string, number>;
16+
}
17+
18+
export async function getAnalytics() {
19+
try {
20+
const response = await api.get<AnalyticsResponse>("/admin/analytics");
21+
return response.data;
22+
} catch (error) {
23+
throw handleAPIError(error);
24+
}
25+
}

src/api/timer.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,26 @@ export interface GetTimeResponse {
88
}
99

1010
export interface SetTimeParams {
11-
round: string;
11+
round_id: string;
1212
time: string;
1313
}
1414

1515
export interface UpdateTimeParams {
16-
duration: number;
16+
round_id: string;
17+
duration: string;
1718
}
1819

1920
export interface StartRoundResponse {
2021
success: boolean;
21-
round: number;
22+
round_id: number;
2223
}
2324

2425
/**
2526
* Fetch the current round and server times.
2627
*/
2728
export async function getTime(): Promise<GetTimeResponse | null> {
2829
try {
29-
const response = await api.get<GetTimeResponse>("/GetTime");
30+
const response = await api.get<GetTimeResponse>("/getTime");
3031
return response.data;
3132
} catch (e) {
3233
console.error(e);
@@ -41,7 +42,10 @@ export async function setTime(
4142
data: SetTimeParams,
4243
): Promise<{ success: boolean }> {
4344
try {
44-
const response = await api.post<{ success: boolean }>("/SetTime", data);
45+
const response = await api.post<{ success: boolean }>(
46+
"/admin/setTime",
47+
data,
48+
);
4549
return response.data;
4650
} catch (e) {
4751
throw handleAPIError(e);
@@ -53,7 +57,7 @@ export async function setTime(
5357
*/
5458
export async function updateTime(data: UpdateTimeParams): Promise<void> {
5559
try {
56-
await api.post("/UpdateTime", data);
60+
await api.post("/admin/updateTime", data);
5761
} catch (e) {
5862
throw handleAPIError(e);
5963
}
@@ -64,7 +68,7 @@ export async function updateTime(data: UpdateTimeParams): Promise<void> {
6468
*/
6569
export async function startRound(): Promise<StartRoundResponse> {
6670
try {
67-
const response = await api.post<StartRoundResponse>("/StartRound");
71+
const response = await api.get<StartRoundResponse>("/admin/startRound");
6872
return response.data;
6973
} catch (e) {
7074
throw handleAPIError(e);

src/app/dashboard/page.tsx

Lines changed: 6 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22
import { getLeaderboard, type LeaderboardUser } from "@/api/users";
3+
import AnalyticsCard from "@/components/Analytics";
34
import NotificationsSender from "@/components/NotificationsSender";
4-
import Round from "@/components/round";
55
import { useQuery } from "@tanstack/react-query";
66

77
function Dashboard() {
@@ -15,97 +15,18 @@ function Dashboard() {
1515

1616
return (
1717
<div className="min-h-screen text-white">
18-
<div className="relative w-full rounded-md border border-gray-300 p-10 shadow-md">
19-
<span className="absolute -top-3 left-4 bg-black px-2 text-lg font-semibold text-white">
20-
Round Select
21-
</span>
22-
<Round />
23-
</div>
24-
2518
<div className="s-sling m-3 mt-10 text-center text-xl font-semibold">
26-
Notifications
19+
Analytics
2720
</div>
2821
<div className="flex w-full justify-center text-black">
29-
<NotificationsSender />
22+
<AnalyticsCard />
3023
</div>
3124

3225
<div className="s-sling m-3 mt-10 text-center text-xl font-semibold">
33-
Leaderboard
26+
Notifications
3427
</div>
35-
36-
{isLoading ? (
37-
<div className="mt-5 text-center">
38-
<p>Loading leaderboard...</p>
39-
</div>
40-
) : error ? (
41-
<div className="mt-5 text-center text-red-500">
42-
<p>Error loading leaderboard: {error?.message}</p>
43-
</div>
44-
) : data?.length ? (
45-
<div className="m-5 flex flex-col items-center gap-5">
46-
<p
47-
className="w-fit cursor-pointer rounded-sm border bg-yellow-700 p-2 text-lg hover:bg-yellow-600"
48-
onClick={() => navigator.clipboard.writeText(data?.[0]?.ID ?? "")}
49-
>
50-
#1 {data?.[0]?.Name ?? "Unknown"}{data?.[0]?.Score ?? 0}
51-
</p>
52-
53-
{data?.length > 1 && (
54-
<div className="flex justify-around gap-5">
55-
<p
56-
className="w-fit cursor-pointer rounded-sm border bg-green-700 p-2 text-lg hover:bg-green-600"
57-
onClick={() =>
58-
navigator.clipboard.writeText(data?.[1]?.ID ?? "")
59-
}
60-
>
61-
#2 {data?.[1]?.Name ?? "Unknown"}{data?.[1]?.Score ?? 0}
62-
</p>
63-
64-
{data?.length > 2 && (
65-
<p
66-
className="w-fit cursor-pointer rounded-sm border bg-red-700 p-2 text-lg hover:bg-red-600"
67-
onClick={() =>
68-
navigator.clipboard.writeText(data?.[2]?.ID ?? "")
69-
}
70-
>
71-
#3 {data?.[2]?.Name ?? "Unknown"}{data?.[2]?.Score ?? 0}
72-
</p>
73-
)}
74-
</div>
75-
)}
76-
</div>
77-
) : (
78-
<p>No data available</p>
79-
)}
80-
81-
<div className="flex justify-center gap-10">
82-
<table className="border-collapse">
83-
{data?.slice(3, 6)?.map((user, index) => (
84-
<tr
85-
key={user?.ID}
86-
className="cursor-pointer rounded-md text-white"
87-
onClick={() => navigator.clipboard.writeText(user?.ID ?? "")}
88-
>
89-
<td className="m-10 bg-black px-4 py-2 text-left hover:bg-slate-700">
90-
{index + 4}. {user?.Name ?? "Unknown"}{user?.Score ?? 0}
91-
</td>
92-
</tr>
93-
))}
94-
</table>
95-
96-
<table className="border-collapse">
97-
{data?.slice(6, 10)?.map((user, index) => (
98-
<tr
99-
key={user?.ID}
100-
className="cursor-pointer"
101-
onClick={() => navigator.clipboard.writeText(user?.ID ?? "")}
102-
>
103-
<td className="m-10 bg-black px-4 py-2 text-left hover:bg-slate-700">
104-
{index + 7}. {user?.Name ?? "Unknown"}{user?.Score ?? 0}
105-
</td>
106-
</tr>
107-
))}
108-
</table>
28+
<div className="flex w-full justify-center text-black">
29+
<NotificationsSender />
10930
</div>
11031
</div>
11132
);

src/app/leaderboard/page.tsx

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"use client";
2+
import { getLeaderboard, type LeaderboardUser } from "@/api/users";
3+
import { useQuery } from "@tanstack/react-query";
4+
import { Copy, Trophy } from "lucide-react";
5+
import toast from "react-hot-toast";
6+
7+
const ACCENT_GREEN = "#1ba94c";
8+
const ACCENT_COLOR = "text-[#1ba94c]";
9+
const CARD_BG = "bg-[#182319]";
10+
11+
function Leaderboard() {
12+
const { data, error, isLoading } = useQuery<LeaderboardUser[], Error>({
13+
queryKey: ["leaderboard"],
14+
queryFn: async () => {
15+
const res = await getLeaderboard();
16+
return res;
17+
},
18+
});
19+
20+
const handleCopy = (id: string) => {
21+
navigator.clipboard
22+
.writeText(id ?? "")
23+
.then(() => {
24+
toast.success("User ID copied to clipboard!");
25+
})
26+
.catch(() => {
27+
toast.error("Failed to copy User ID.");
28+
});
29+
};
30+
31+
const topThree = data?.slice(0, 3) ?? [];
32+
const otherUsers = data?.slice(3) ?? [];
33+
34+
const getRankClasses = (rank: number) => {
35+
switch (rank) {
36+
case 1:
37+
return `${ACCENT_COLOR} border-4 border-yellow-500 shadow-[0_0_15px_rgba(253,224,71,0.5)]`;
38+
case 2:
39+
return "border-4 border-gray-400 text-gray-300";
40+
case 3:
41+
return "border-4 border-yellow-700 text-yellow-600";
42+
default:
43+
return "border border-gray-700 text-white";
44+
}
45+
};
46+
47+
const orderedTopThree = [];
48+
if (topThree[1]) orderedTopThree.push({ user: topThree[1], rank: 2 });
49+
if (topThree[0]) orderedTopThree.push({ user: topThree[0], rank: 1 });
50+
if (topThree[2]) orderedTopThree.push({ user: topThree[2], rank: 3 });
51+
52+
return (
53+
<div className={`min-h-screen p-5 text-white`}>
54+
<h1
55+
className={`mb-8 pb-2 text-center text-3xl font-extrabold uppercase tracking-widest sm:text-4xl ${ACCENT_COLOR} border-b border-[${ACCENT_GREEN}]/50`}
56+
>
57+
Leaderboard
58+
</h1>
59+
60+
{isLoading && (
61+
<div className="mt-10 text-center">
62+
<p className={ACCENT_COLOR}>Loading leaderboard...</p>
63+
</div>
64+
)}
65+
{error && (
66+
<div className="mt-10 text-center text-red-500">
67+
<p>Error loading leaderboard: {error?.message}</p>
68+
</div>
69+
)}
70+
71+
{!isLoading && !error && (
72+
<div className="mx-auto max-w-4xl">
73+
<div className="mb-12 flex items-end justify-center gap-2 sm:gap-4">
74+
{orderedTopThree.map(({ user, rank }) => (
75+
<div
76+
key={user?.ID}
77+
className={`flex transform cursor-pointer flex-col justify-between rounded-lg p-2 transition-transform duration-300 hover:scale-[1.03] bg-[${ACCENT_GREEN}]/10 shadow-lg backdrop-blur-sm border-[${ACCENT_GREEN}]/50 w-full max-w-[100px] sm:max-w-[150px] ${getRankClasses(rank)} `}
78+
style={{
79+
minHeight: "180px",
80+
flexGrow: rank === 1 ? 1.2 : rank === 2 ? 1.1 : 1,
81+
}}
82+
onClick={() => handleCopy(user?.ID ?? "")}
83+
>
84+
<div className="flex flex-col items-center p-1">
85+
<Trophy
86+
className={`mb-1 h-5 w-5 sm:h-8 sm:w-8 ${
87+
rank === 1
88+
? "fill-yellow-500"
89+
: rank === 2
90+
? "fill-gray-400"
91+
: "fill-yellow-700"
92+
}`}
93+
/>
94+
<p className="mb-0 text-xl font-black sm:text-3xl">{`#${rank}`}</p>
95+
<p className="w-full truncate text-center text-xs font-semibold sm:text-sm">
96+
{user?.Name ?? "Unknown"}
97+
</p>
98+
<p
99+
className={`mt-1 text-lg font-bold sm:text-xl ${
100+
rank === 1 ? ACCENT_COLOR : ""
101+
}`}
102+
>
103+
{user?.Score ?? 0}
104+
</p>
105+
<Copy className="mt-1 h-3 w-3 opacity-30 transition-opacity hover:opacity-100" />
106+
</div>
107+
108+
<div
109+
className={`mt-2 rounded-b-lg p-1 text-center text-xs font-bold sm:text-sm bg-[${ACCENT_GREEN}]/30 border-t border-[${ACCENT_GREEN}]/50 `}
110+
>
111+
RANK {rank}
112+
</div>
113+
</div>
114+
))}
115+
</div>
116+
117+
<div
118+
className={`rounded-xl border border-[${ACCENT_GREEN}]/30 ${CARD_BG} p-4 shadow-inner sm:p-6`}
119+
>
120+
<h2
121+
className={`mb-4 text-xl font-bold uppercase tracking-wider ${ACCENT_COLOR}`}
122+
>
123+
The Rest of the Field
124+
</h2>
125+
<div className="flex flex-col gap-1">
126+
{otherUsers.length > 0 ? (
127+
otherUsers.map((user, index) => {
128+
const rank = index + 4;
129+
return (
130+
<div
131+
key={user?.ID}
132+
className={`flex cursor-pointer items-center justify-between rounded-md p-3 transition-colors duration-200 hover:bg-[${ACCENT_GREEN}]/10`}
133+
onClick={() => handleCopy(user?.ID ?? "")}
134+
>
135+
<div className="flex items-center space-x-4">
136+
<span
137+
className={`w-6 text-right text-lg font-bold ${ACCENT_COLOR}`}
138+
>{`${rank}.`}</span>
139+
140+
<span className="text-sm font-medium text-white sm:text-base">
141+
{user?.Name ?? "Unknown"}
142+
</span>
143+
</div>
144+
145+
<div className="flex items-center space-x-4">
146+
<span
147+
className={`text-lg font-extrabold sm:text-xl ${ACCENT_COLOR}`}
148+
>
149+
{user?.Score ?? 0}
150+
</span>
151+
<Copy className="h-4 w-4 text-gray-500 opacity-50 transition-opacity hover:opacity-100" />
152+
</div>
153+
</div>
154+
);
155+
})
156+
) : (
157+
<p className="py-4 text-center text-gray-500">
158+
No other participants yet.
159+
</p>
160+
)}
161+
</div>
162+
</div>
163+
</div>
164+
)}
165+
</div>
166+
);
167+
}
168+
169+
export default Leaderboard;

0 commit comments

Comments
 (0)