Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,35 +1,27 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "cockroachdb"
// NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
// Further reading:
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
url = env("DATABASE_URL")
}

// Necessary for Next auth
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? // @db.Text
access_token String? // @db.Text
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String? // @db.Text
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
refresh_token_expires_in Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([provider, providerAccountId])
}
Expand Down Expand Up @@ -76,7 +68,6 @@ model Problem {
id String @id @default(cuid())
name String
level String
status String
leetcodeUrl String
weekId String
week Week @relation(fields: [weekId], references: [id], onDelete: Cascade)
Expand Down
92 changes: 86 additions & 6 deletions src/app/(pages)/leaderboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,91 @@

"use client";
import { useState } from "react";
import { api } from "~/trpc/react";

const Leaderboard = () => {
return (
<div>
Hello
const [selected, setSelected] = useState("all");

// Fetch weeks for the slider
const { data: weeksData, isLoading: weeksLoading } = api.week.getWeeks.useQuery();

// Fetch leaderboard data
const {
data: leaderboardData,
isLoading: leaderboardLoading,
error: leaderboardError,
} = selected === "all"
? api.leaderboard.getAll.useQuery()
: api.leaderboard.getByWeek.useQuery(selected);

if (weeksLoading || leaderboardLoading) return <div>Loading...</div>;
if (leaderboardError) return <div>Error loading leaderboard</div>;

// Prepare week options
const weekOptions = [
{ id: "all", label: "Global" },
...(weeksData
? weeksData.map((w: any) => ({ id: w.id?.toString() ?? w.number?.toString(), label: w.title || `Week ${w.number}` }))
: []),
];

// Sort leaderboard
const sorted = [...(leaderboardData ?? [])].sort((a, b) => b.total - a.total);

return (
<div className="max-w-3xl mx-auto mt-12">
<h1 className="text-3xl font-extrabold mb-8 text-center tracking-tight text-white drop-shadow">
Leaderboard
</h1>
{/* Slider/toggle */}
<div className="flex justify-center mb-6">
<div className="inline-flex bg-neutral-800 rounded-full p-1">
{weekOptions.map((w) => (
<button
key={w.id}
onClick={() => setSelected(w.id)}
className={`px-5 py-2 rounded-full font-semibold transition-all ${
selected === w.id
? "bg-white text-neutral-900 shadow"
: "text-white hover:bg-neutral-700"
}`}
>
{w.label}
</button>
))}
</div>
)
}
</div>
{/* Table header */}
<div className="w-full flex flex-row justify-between bg-neutral-800 text-white rounded-t-2xl px-8 py-3 text-lg font-semibold mb-2">
<span className="w-1/4 text-left">User</span>
<span className="w-1/8 text-center">Warmup</span>
<span className="w-1/8 text-center">Medium</span>
<span className="w-1/8 text-center">Harder</span>
<span className="w-1/8 text-center">Insane</span>
<span className="w-1/8 text-center">Total</span>
</div>
<ol className="flex flex-col gap-4">
{sorted.map((row, i) => (
<li
key={i}
className={`w-full bg-neutral-700 text-white rounded-2xl shadow-lg px-8 py-4 flex flex-row items-center justify-between transition-all
${i === 0 ? "bg-gray-700 font-bold scale-[1.03]" : ""}
${row.username === "You" ? "bg-green-600 font-bold" : ""}
`}
>
<div className="w-1/4 flex items-center gap-3">
<span className="text-xl font-semibold w-8 text-gray-400 text-center">{i + 1}</span>
<span className="text-base font-medium break-words leading-tight">{row.username}</span>
</div>
<span className="w-1/8 text-center text-lg font-bold">{row.completedWarmup}</span>
<span className="w-1/8 text-center text-lg font-bold">{row.completedMedium}</span>
<span className="w-1/8 text-center text-lg font-bold">{row.completedHarder}</span>
<span className="w-1/8 text-center text-lg font-bold">{row.completedInsane}</span>
<span className="w-1/8 text-center text-lg font-bold">{row.total}</span>
</li>
))}
</ol>
</div>
);
};

export default Leaderboard;
9 changes: 8 additions & 1 deletion src/app/_components/week/weekInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { api } from "~/trpc/server";
import Title from "../title";
import Subtitle from "../subtitle";
import { auth } from "~/server/auth";

const WeekInfo = async ({ id }: { id: number }) => {
const week = await api.week.getWeek(id);
const session = await auth();
const userId = session?.user?.id;

return (
<div>
Expand Down Expand Up @@ -71,7 +74,11 @@ const WeekInfo = async ({ id }: { id: number }) => {
</td>
<td>{problem.level}</td>
<td>{problem.solvedBy?.length ?? 0}</td>
<td>{problem.status}</td>
<td>
{userId && problem.solvedBy?.some((u) => u.id === userId)
? "Solved"
: "Unsolved"}
</td>
</tr>
))}
</tbody>
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { leetcodeRouter } from "~/server/api/routers/leetcode";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
import { weekRouter } from "./routers/week";
import { problemRouter } from "./routers/problem";
import { leaderboardRouter } from "./routers/leaderboard";

/**
* This is the primary router for your server.
Expand All @@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
leetcode: leetcodeRouter,
week: weekRouter,
problem : problemRouter,
leaderboard: leaderboardRouter,
});

// export type definition of API
Expand Down
95 changes: 95 additions & 0 deletions src/server/api/routers/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc";

// Map problem levels to leaderboard columns
const levelMap = {
warmup: "warmup",
medium: "medium",
harder: "harder",
insane: "insane",
} as const;
export const leaderboardRouter = createTRPCRouter({
// Full leaderboard
getAll: publicProcedure.query(async ({ ctx }) => {
// Fetch users -> solved problems
const users = await ctx.db.user.findMany({
select: {
id: true,
name: true,
solvedProblems: {
select: {
level: true,
weekId: true,
},
},
},
});
// Aggregate leaderboard data from database
return users.map((user) => {
const counts = {
warmup: 0,
medium: 0,
harder: 0,
insane: 0,
};
user.solvedProblems.forEach((p) => {
// key for level - if not found, default to warmup
const key =
levelMap[p.level.toLowerCase() as keyof typeof levelMap] || "warmup";
// Increment the count for the corresponding level
if (counts[key] !== undefined) counts[key]++;
});
return {
username: user.name,
completedWarmup: counts.warmup,
completedMedium: counts.medium,
completedHarder: counts.harder,
completedInsane: counts.insane,
total:
counts.warmup +
counts.medium +
counts.harder +
counts.insane,
};
});
}),
// Get leaderboard for a specific week
getByWeek: publicProcedure.input(z.string()).query(async ({ ctx, input }) => {
const users = await ctx.db.user.findMany({
select: {
id: true,
name: true,
solvedProblems: {
where: { weekId: input },
select: { level: true },
},
},
});

return users.map((user) => {
const counts = {
warmup: 0,
medium: 0,
harder: 0,
insane: 0,
};
user.solvedProblems.forEach((p) => {
const key =
levelMap[p.level.toLowerCase() as keyof typeof levelMap] || "warmup";
if (counts[key] !== undefined) counts[key]++;
});
return {
username: user.name,
completedWarmup: counts.warmup,
completedMedium: counts.medium,
completedHarder: counts.harder,
completedInsane: counts.insane,
total:
counts.warmup +
counts.medium +
counts.harder +
counts.insane,
};
});
}),
});
2 changes: 0 additions & 2 deletions src/server/api/routers/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const problemRouter = createTRPCRouter({
.input(z.object({
name: z.string(),
level: z.string(),
status: z.string(),
leetcodeUrl: z.string().url(),
weekId: z.string(),
}))
Expand All @@ -25,7 +24,6 @@ export const problemRouter = createTRPCRouter({
id: z.string(),
name: z.string().optional(),
level: z.string().optional(),
status: z.string().optional(),
leetcodeUrl: z.string().url().optional(),
weekId: z.string().optional(),
}))
Expand Down
8 changes: 5 additions & 3 deletions src/server/api/routers/week.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ export const weekRouter = createTRPCRouter({
getWeek: protectedProcedure
.input(z.number())
.query(async({ctx, input}) => {
return ctx.db.week.findFirst({
return ctx.db.week.findMany({
where: {
number: input
},
include: { problems: {include: { solvedBy: true }}},
})
include: { problems: {include: { solvedBy: true }}},
// for cockroachdb
take: 1,
}).then(weeks => weeks[0]);
})
})