-
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?
Conversation
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.
Pull request overview
This PR implements leaderboard functionality across three admin dashboard pages (ysws-review, review, and print) to display top contributors based on their review or printing activity.
Key changes:
- Added server-side queries to aggregate review counts across different review types (t1Review, legionReview, t2Review)
- Replaced "Coming soon!" placeholders with functional leaderboard tables showing top 10 contributors
- Added project count display to the review page header
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/dashboard/admin/ysws-review/+page.svelte | Implemented leaderboard table for YSWS reviewers with user links and review counts |
| src/routes/dashboard/admin/ysws-review/+page.server.ts | Added database query to aggregate t2Review counts and generate top 10 leaderboard |
| src/routes/dashboard/admin/review/+page.svelte | Implemented leaderboard table for all reviewers and added project count to header |
| src/routes/dashboard/admin/review/+page.server.ts | Added database queries to aggregate t1Review, legionReview, and t2Review counts for comprehensive leaderboard |
| src/routes/dashboard/admin/print/+page.svelte | Implemented leaderboard table for printers showing top contributors |
| src/routes/dashboard/admin/print/+page.server.ts | Added database query to aggregate legionReview counts for printing leaderboard |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {#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> |
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 variable name 'review_count' is misleading in this context. Since this leaderboard tracks printing actions (as indicated by the column header "Number of prints" and the empty state message "No printing actions yet."), it should be renamed to something like 'print_count' or 'action_count' for clarity.
| {#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> |
| 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); |
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 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.
| 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); |
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 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.
| <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> | ||
| </tr> | ||
| {/each} | ||
| </tbody> | ||
| </table> | ||
| {:else} | ||
| <p class="text-sm text-primary-300">No printing actions yet.</p> | ||
| {/if} |
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.
| {#each data.leaderboard as row} | ||
| <tr> | ||
| <td class="py-1" align="left"> | ||
| <a class="underline" href={`../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 URL path uses a relative path '../users/' which may cause navigation issues depending on the current route. Consider using an absolute path '/dashboard/users/' for consistency with the review page implementation (line 127 in review/+page.svelte).
| <a class="underline" href={`../users/${row.id}`}>{row.name}</a> | |
| <a class="underline" href={`/dashboard/users/${row.id}`}>{row.name}</a> |
| 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); | ||
|
|
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 variable name 'review_count' is misleading in this context. Since this leaderboard tracks printing actions (as indicated by the query using legionReview table), it should be renamed to something like 'print_count' or 'action_count' to accurately reflect what's being counted.
| 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 | |
| })); |
| 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); |
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 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.
| <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> | ||
| </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} |
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.
| <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} |
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.
No description provided.