-
Notifications
You must be signed in to change notification settings - Fork 2
Add leaderboards to admin dashboard pages #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { db } from '$lib/server/db/index.js'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { project, user, devlog } from '$lib/server/db/schema.js'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { project, user, devlog, legionReview } from '$lib/server/db/schema.js'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { error } from '@sveltejs/kit'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { eq, and, sql, ne, inArray } from 'drizzle-orm'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { eq, and, sql, ne, inArray, desc, gt } from 'drizzle-orm'; | ||||||||||||||||||||||||||||||||||||||||||||
| import type { Actions } from './$types'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { getCurrentlyPrinting } from './utils.server'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,13 +31,34 @@ export async function load({ locals }) { | |||||||||||||||||||||||||||||||||||||||||||
| .from(user) | ||||||||||||||||||||||||||||||||||||||||||||
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'))); // hide banned users | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const legionAgg = db | ||||||||||||||||||||||||||||||||||||||||||||
| .$with('legionAgg') | ||||||||||||||||||||||||||||||||||||||||||||
| .as( | ||||||||||||||||||||||||||||||||||||||||||||
| db | ||||||||||||||||||||||||||||||||||||||||||||
| .select({ userId: legionReview.userId, legionCnt: sql<number>`COUNT(*)`.as('legionCnt') }) | ||||||||||||||||||||||||||||||||||||||||||||
| .from(legionReview) | ||||||||||||||||||||||||||||||||||||||||||||
| .groupBy(legionReview.userId) | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const totalExpr = sql<number>`COALESCE(${legionAgg.legionCnt}, 0)`; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const leaderboard = await db | ||||||||||||||||||||||||||||||||||||||||||||
| .with(legionAgg) | ||||||||||||||||||||||||||||||||||||||||||||
| .select({ id: user.id, name: user.name, review_count: totalExpr }) | ||||||||||||||||||||||||||||||||||||||||||||
| .from(user) | ||||||||||||||||||||||||||||||||||||||||||||
| .leftJoin(legionAgg, eq(legionAgg.userId, user.id)) | ||||||||||||||||||||||||||||||||||||||||||||
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'), gt(totalExpr, 0))) | ||||||||||||||||||||||||||||||||||||||||||||
| .orderBy(desc(totalExpr)) | ||||||||||||||||||||||||||||||||||||||||||||
| .limit(10); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+53
|
||||||||||||||||||||||||||||||||||||||||||||
| const leaderboard = await db | |
| .with(legionAgg) | |
| .select({ id: user.id, name: user.name, review_count: totalExpr }) | |
| .from(user) | |
| .leftJoin(legionAgg, eq(legionAgg.userId, user.id)) | |
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'), gt(totalExpr, 0))) | |
| .orderBy(desc(totalExpr)) | |
| .limit(10); | |
| const leaderboardRaw = await db | |
| .with(legionAgg) | |
| .select({ id: user.id, name: user.name, print_count: totalExpr }) | |
| .from(user) | |
| .leftJoin(legionAgg, eq(legionAgg.userId, user.id)) | |
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'), gt(totalExpr, 0))) | |
| .orderBy(desc(totalExpr)) | |
| .limit(10); | |
| const leaderboard = leaderboardRaw.map((entry) => ({ | |
| ...entry, | |
| review_count: entry.print_count | |
| })); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -123,22 +123,29 @@ | |||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| <div class="themed-box grow p-3 lg:min-w-[30%]"> | ||||||||||||||||||||||||||
| <h2 class="text-xl font-bold">Leaderboard</h2> | ||||||||||||||||||||||||||
| <div class="w-full overflow-scroll"> | ||||||||||||||||||||||||||
| Coming soon! | ||||||||||||||||||||||||||
| <!-- <table class="w-full"> | ||||||||||||||||||||||||||
| <thead> | ||||||||||||||||||||||||||
| <tr> | ||||||||||||||||||||||||||
| <th align="left">a</th> | ||||||||||||||||||||||||||
| <th align="right">a</th> | ||||||||||||||||||||||||||
| </tr> | ||||||||||||||||||||||||||
| </thead> | ||||||||||||||||||||||||||
| <tbody> | ||||||||||||||||||||||||||
| <tr> | ||||||||||||||||||||||||||
| <td align="left">a</td> | ||||||||||||||||||||||||||
| <td align="right">a</td> | ||||||||||||||||||||||||||
| </tr> | ||||||||||||||||||||||||||
| </tbody> | ||||||||||||||||||||||||||
| </table> --> | ||||||||||||||||||||||||||
| <div class="w-full overflow-x-auto"> | ||||||||||||||||||||||||||
| {#if data.leaderboard?.length > 0} | ||||||||||||||||||||||||||
| <table class="w-full text-sm"> | ||||||||||||||||||||||||||
| <thead> | ||||||||||||||||||||||||||
| <tr class="text-primary-300"> | ||||||||||||||||||||||||||
| <th class="py-1" align="left">Printer</th> | ||||||||||||||||||||||||||
| <th class="py-1" align="right">Number of prints</th> | ||||||||||||||||||||||||||
| </tr> | ||||||||||||||||||||||||||
| </thead> | ||||||||||||||||||||||||||
| <tbody> | ||||||||||||||||||||||||||
| {#each data.leaderboard as row} | ||||||||||||||||||||||||||
| <tr> | ||||||||||||||||||||||||||
| <td class="py-1" align="left"> | ||||||||||||||||||||||||||
| <a class="underline" href={`/dashboard/users/${row.id}`}>{row.name}</a> | ||||||||||||||||||||||||||
| </td> | ||||||||||||||||||||||||||
| <td class="py-1" align="right">{row.review_count}</td> | ||||||||||||||||||||||||||
|
Comment on lines
+136
to
+141
|
||||||||||||||||||||||||||
| {#each data.leaderboard as row} | |
| <tr> | |
| <td class="py-1" align="left"> | |
| <a class="underline" href={`/dashboard/users/${row.id}`}>{row.name}</a> | |
| </td> | |
| <td class="py-1" align="right">{row.review_count}</td> | |
| {#each data.leaderboard as { id, name, review_count: print_count }} | |
| <tr> | |
| <td class="py-1" align="left"> | |
| <a class="underline" href={`/dashboard/users/${id}`}>{name}</a> | |
| </td> | |
| <td class="py-1" align="right">{print_count}</td> |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The leaderboard table markup is duplicated across all three admin pages (ysws-review, review, and print). Consider extracting this into a reusable Svelte component that accepts the leaderboard data, column headers, and user type label as props to reduce duplication and improve maintainability.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import { db } from '$lib/server/db/index.js'; | ||
| import { project, user, devlog } from '$lib/server/db/schema.js'; | ||
| import { project, user, devlog, t1Review, legionReview, t2Review } from '$lib/server/db/schema.js'; | ||
| import { error } from '@sveltejs/kit'; | ||
| import { eq, and, sql, ne, inArray } from 'drizzle-orm'; | ||
| import { eq, and, sql, ne, inArray, desc, gt } from 'drizzle-orm'; | ||
| import type { Actions } from './$types'; | ||
|
|
||
| export async function load({ locals }) { | ||
|
|
@@ -30,10 +30,51 @@ export async function load({ locals }) { | |
| .from(user) | ||
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'))); // hide banned users | ||
|
|
||
| const t1Agg = db | ||
| .$with('t1Agg') | ||
| .as( | ||
| db | ||
| .select({ userId: t1Review.userId, t1Cnt: sql<number>`COUNT(*)`.as('t1Cnt') }) | ||
| .from(t1Review) | ||
| .groupBy(t1Review.userId) | ||
| ); | ||
|
|
||
| const legionAgg = db | ||
| .$with('legionAgg') | ||
| .as( | ||
| db | ||
| .select({ userId: legionReview.userId, legionCnt: sql<number>`COUNT(*)`.as('legionCnt') }) | ||
| .from(legionReview) | ||
| .groupBy(legionReview.userId) | ||
| ); | ||
|
|
||
| const t2Agg = db | ||
| .$with('t2Agg') | ||
| .as( | ||
| db | ||
| .select({ userId: t2Review.userId, t2Cnt: sql<number>`COUNT(*)`.as('t2Cnt') }) | ||
| .from(t2Review) | ||
| .groupBy(t2Review.userId) | ||
| ); | ||
|
|
||
| const totalExpr = sql<number>`COALESCE(${t1Agg.t1Cnt}, 0) + COALESCE(${legionAgg.legionCnt}, 0) + COALESCE(${t2Agg.t2Cnt}, 0)`; | ||
|
|
||
| const leaderboard = await db | ||
| .with(t1Agg, legionAgg, t2Agg) | ||
| .select({ id: user.id, name: user.name, review_count: totalExpr }) | ||
| .from(user) | ||
| .leftJoin(t1Agg, eq(t1Agg.userId, user.id)) | ||
| .leftJoin(legionAgg, eq(legionAgg.userId, user.id)) | ||
| .leftJoin(t2Agg, eq(t2Agg.userId, user.id)) | ||
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'), gt(totalExpr, 0))) | ||
| .orderBy(desc(totalExpr)) | ||
| .limit(10); | ||
|
Comment on lines
+33
to
+71
|
||
|
|
||
| return { | ||
| allProjects, | ||
| projects, | ||
| users | ||
| users, | ||
| leaderboard | ||
| }; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,27 +111,34 @@ | |
| </div> | ||
| <div class="themed-box grow p-3 lg:min-w-[30%]"> | ||
| <h2 class="text-xl font-bold">Leaderboard</h2> | ||
| <div class="w-full overflow-scroll"> | ||
| Coming soon! | ||
| <!-- <table class="w-full"> | ||
| <thead> | ||
| <tr> | ||
| <th align="left">a</th> | ||
| <th align="right">a</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| <tr> | ||
| <td align="left">a</td> | ||
| <td align="right">a</td> | ||
| </tr> | ||
| </tbody> | ||
| </table> --> | ||
| <div class="w-full overflow-x-auto"> | ||
| {#if data.leaderboard?.length > 0} | ||
| <table class="w-full text-sm"> | ||
| <thead> | ||
| <tr class="text-primary-300"> | ||
| <th class="py-1" align="left">Reviewer</th> | ||
| <th class="py-1" align="right">Reviews</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {#each data.leaderboard as row} | ||
| <tr> | ||
| <td class="py-1" align="left"> | ||
| <a class="underline" href={`/dashboard/users/${row.id}`}>{row.name}</a> | ||
| </td> | ||
| <td class="py-1" align="right">{row.review_count}</td> | ||
| </tr> | ||
| {/each} | ||
| </tbody> | ||
| </table> | ||
| {:else} | ||
| <p class="text-sm text-primary-300">No reviews yet.</p> | ||
| {/if} | ||
|
Comment on lines
+114
to
+136
|
||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <h2 class="mt-4 mb-2 text-2xl font-bold">Projects</h2> | ||
| <h2 class="mt-4 mb-2 text-2xl font-bold">Projects <span class="ml-2 align-middle text-sm font-normal">({projects.length})</span></h2> | ||
|
|
||
| {#if projects.length == 0} | ||
| <div class="flex grow items-center justify-center"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import { db } from '$lib/server/db/index.js'; | ||
| import { project, user, devlog } from '$lib/server/db/schema.js'; | ||
| import { project, user, devlog, t2Review } from '$lib/server/db/schema.js'; | ||
| import { error } from '@sveltejs/kit'; | ||
| import { eq, and, sql, ne, inArray } from 'drizzle-orm'; | ||
| import { eq, and, sql, ne, inArray, desc, gt } from 'drizzle-orm'; | ||
| import type { Actions } from './$types'; | ||
|
|
||
| export async function load({ locals }) { | ||
|
|
@@ -30,10 +30,31 @@ export async function load({ locals }) { | |
| .from(user) | ||
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'))); // hide banned users | ||
|
|
||
| const t2Agg = db | ||
| .$with('t2Agg') | ||
| .as( | ||
| db | ||
| .select({ userId: t2Review.userId, t2Cnt: sql<number>`COUNT(*)`.as('t2Cnt') }) | ||
| .from(t2Review) | ||
| .groupBy(t2Review.userId) | ||
| ); | ||
|
|
||
| const totalExpr = sql<number>`COALESCE(${t2Agg.t2Cnt}, 0)`; | ||
|
|
||
| const leaderboard = await db | ||
| .with(t2Agg) | ||
| .select({ id: user.id, name: user.name, review_count: totalExpr }) | ||
| .from(user) | ||
| .leftJoin(t2Agg, eq(t2Agg.userId, user.id)) | ||
| .where(and(ne(user.trust, 'red'), ne(user.hackatimeTrust, 'red'), gt(totalExpr, 0))) | ||
| .orderBy(desc(totalExpr)) | ||
| .limit(10); | ||
|
Comment on lines
+33
to
+51
|
||
|
|
||
| return { | ||
| allProjects, | ||
| projects, | ||
| users | ||
| users, | ||
| leaderboard | ||
| }; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -111,22 +111,29 @@ | |||||
| </div> | ||||||
| <div class="themed-box grow p-3 lg:min-w-[30%]"> | ||||||
| <h2 class="text-xl font-bold">Leaderboard</h2> | ||||||
| <div class="w-full overflow-scroll"> | ||||||
| Coming soon! | ||||||
| <!-- <table class="w-full"> | ||||||
| <thead> | ||||||
| <tr> | ||||||
| <th align="left">a</th> | ||||||
| <th align="right">a</th> | ||||||
| </tr> | ||||||
| </thead> | ||||||
| <tbody> | ||||||
| <tr> | ||||||
| <td align="left">a</td> | ||||||
| <td align="right">a</td> | ||||||
| </tr> | ||||||
| </tbody> | ||||||
| </table> --> | ||||||
| <div class="w-full overflow-x-auto"> | ||||||
| {#if data.leaderboard?.length > 0} | ||||||
| <table class="w-full text-sm"> | ||||||
| <thead> | ||||||
| <tr class="text-primary-300"> | ||||||
| <th class="py-1" align="left">Reviewer</th> | ||||||
| <th class="py-1" align="right">Reviews</th> | ||||||
| </tr> | ||||||
| </thead> | ||||||
| <tbody> | ||||||
| {#each data.leaderboard as row} | ||||||
| <tr> | ||||||
| <td class="py-1" align="left"> | ||||||
| <a class="underline" href={`../users/${row.id}`}>{row.name}</a> | ||||||
|
||||||
| <a class="underline" href={`../users/${row.id}`}>{row.name}</a> | |
| <a class="underline" href={`/dashboard/users/${row.id}`}>{row.name}</a> |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The leaderboard table markup is duplicated across all three admin pages (ysws-review, review, and print). Consider extracting this into a reusable Svelte component that accepts the leaderboard data, column headers, and user type label as props to reduce duplication and improve maintainability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The leaderboard query pattern is duplicated across three files (ysws-review, review, and print +page.server.ts). Consider extracting this into a shared utility function that accepts the review tables and optional filters as parameters to improve maintainability and reduce code duplication.