Skip to content

Commit

Permalink
add admin leaderboard
Browse files Browse the repository at this point in the history
  • Loading branch information
geoffrey-wu committed Sep 11, 2024
1 parent 1276a62 commit 811da33
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 0 deletions.
1 change: 1 addition & 0 deletions client/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<ul>
<li><a href="/admin/category-reports">Fix question categories</a></li>
<li><a href="/admin/geoword">Geoword</a></li>
<li><a href="/admin/leaderboard">Leaderboard</a></li>
</ul>
</div>

Expand Down
90 changes: 90 additions & 0 deletions client/admin/leaderboard/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!doctype html>
<html lang="en">

<head>
<title>QB Reader</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">

<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/apple-touch-icon-precomposed.png" rel="apple-touch-icon-precomposed">
<link type="image/x-icon" href="/favicon.ico" rel="icon">

<link href="/bootstrap/light.css" rel="stylesheet">
<link href="/bootstrap/dark.css" rel="stylesheet" id="custom-css">
<script type="module" src="/scripts/apply-theme.js"></script>
</head>

<body>
<nav class="navbar navbar-light navbar-expand-lg bg-custom" id="navbar" style="z-index: 10">
<div class="container-fluid">
<a class="navbar-brand ms-1 py-0" id="logo" href="/">
<span class="logo-prefix">QB</span><span class="logo-suffix">Reader</span>
</a>
<button class="navbar-toggler" data-bs-target="#navbarSupportedContent" data-bs-toggle="collapse" type="button"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/singleplayer">Singleplayer</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/multiplayer">Multiplayer</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/db">Database</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/frequency-list">Frequency List</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/geoword">Geoword</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/api-docs">API</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">Settings</a>
</li>
</ul>
<div class="d-flex">
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/user/login" id="login-link">Log in</a>
</li>
</ul>
</div>
</div>
</div>
</nav>

<div class="container-xl mt-3 mb-5 pb-5">
<p class="lead">Most active QB Reader players:</p>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col">Tossups Read</th>
<th scope="col">Bonuses Read</th>
<th scope="col">Total</th>
</tr>
</thead>
<tbody class="table-group-divider" id="leaderboard"></tbody>
</table>
<div class="d-block mx-auto mt-3 spinner-border" id="spinner" role="status"><span class='d-none'>Loading...</span></div>
</div>

<script type="module" src="/bootstrap/bootstrap.bundle.min.js"></script>
<script type="module" src="/script.js"></script>

<script type="module" src="/admin/leaderboard/index.js"></script>
</body>

</html>
23 changes: 23 additions & 0 deletions client/admin/leaderboard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import sortTable from '../../scripts/utilities/tables.js';

const limit = 20;
fetch('/api/admin/leaderboard?' + new URLSearchParams({ limit }))
.then(res => res.json())
.then(data => data.data)
.then(data => {
document.getElementById('spinner').classList.add('d-none');
const table = document.getElementById('leaderboard');
data.forEach((document, index) => {
const row = table.insertRow(-1);
row.insertCell(-1).textContent = index + 1;
row.insertCell(-1).textContent = document.username;
row.insertCell(-1).textContent = document.tossupCount;
row.insertCell(-1).textContent = document.bonusCount;
row.insertCell(-1).textContent = document.total;
});
});

document.querySelectorAll('th').forEach((th, index) => {
const numeric = [true, false, true, true, true];
th.addEventListener('click', () => sortTable(index, numeric[index], 'leaderboard', 0, 0));
});
78 changes: 78 additions & 0 deletions client/scripts/utilities/tables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Sorts a table by the values in a specified column.
* @param {number} n - a zero-indexed column number to sort
* @param {boolean} numeric - whether the column values represent numeric values
* @param {string} tableId - the id of the table to sort
* @param {number} headers - the number of headers of the table to skip (default 1)
* @param {number} footers - the number of footers of the table to skip (default 0)
*/
export default function sortTable (n, numeric = false, tableId = 'table', headers = 1, footers = 0) {
let rows; let switching; let i; let x; let y; let shouldSwitch; let dir; let switchcount = 0;
const table = document.getElementById(tableId);
switching = true;
// Set the sorting direction to ascending for text;
// Numerals are always sorted in the opposite order from text
dir = 'asc';
/* Make a loop that will continue until
no switching has been done: */
while (switching) {
// Start by saying: no switching is done:
switching = false;
rows = table.rows;
// Loop through all table rows (except headers and footers)
for (i = headers; i < rows.length - 1 - footers; i++) {
// Start by saying there should be no switching:
shouldSwitch = false;
/* Get the two elements you want to compare,
one from current row and one from the next: */
x = rows[i].children[n];
y = rows[i + 1].children[n];
/* Check if the two rows should switch place,
based on the direction, asc or desc: */
if (dir === 'asc') {
if (numeric) {
if (parseFloat(x.innerHTML) < parseFloat(y.innerHTML)) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
} else {
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
} else if (dir === 'desc') {
if (numeric) {
if (parseFloat(x.innerHTML) > parseFloat(y.innerHTML)) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
} else {
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
}
}
if (shouldSwitch) {
/* If a switch has been marked, make the switch
and mark that a switch has been done: */
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
// Each time a switch is done, increase this count by 1:
switchcount++;
} else {
/* If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again. */
if (switchcount === 0 && dir === 'asc') {
dir = 'desc';
switching = true;
}
}
}
}
59 changes: 59 additions & 0 deletions database/account-info/leaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { tossupData, bonusData } from '../../database/qbreader/collections.js';
import mergeTwoSortedArrays from '../../merge-two-sorted-arrays.js';

export default async function leaderboard (limit) {
const tossupLeaderboard = await helper('tossup');
const bonusLeaderboard = await helper('bonus');
const overall = mergeTwoSortedArrays(
tossupLeaderboard,
bonusLeaderboard,
(document) => document.username,
(document1, document2) => ({ _id: document1._id, username: document1.username, tossupCount: document1.tossupCount, bonusCount: document2.bonusCount, total: document1.total + document2.total })
);
// sort from most to least
overall.sort((a, b) => b.total - a.total);
return overall.slice(0, limit);
}

/**
*
* @param {'tossup' | 'bonus'} type - the type of questions to filter by
* @returns
*/
async function helper (type = 'tossup') {
const aggregation = [
{
$group: {
_id: '$user_id',
count: { $sum: 1 }
}
},
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'user'
}
},
{
$project: {
username: { $arrayElemAt: ['$user.username', 0] },
_id: 1,
total: '$count'
}
},
{ $sort: { username: 1 } }
];

switch (type) {
case 'tossup': {
const results = await tossupData.aggregate(aggregation).toArray();
return results.map((result) => ({ ...result, tossupCount: result.total, bonusCount: 0 }));
}
case 'bonus': {
const results = await bonusData.aggregate(aggregation).toArray();
return results.map((result) => ({ ...result, tossupCount: 0, bonusCount: result.total }));
}
}
}
2 changes: 2 additions & 0 deletions routes/admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { checkToken } from '../../server/authentication.js';

import categoryReportsRouter from './category-reports.js';
import geowordRouter from './geoword.js';
import leaderboardRouter from './leaderboard.js';

import { Router } from 'express';

Expand All @@ -29,6 +30,7 @@ router.use(async (req, res, next) => {

router.use('/category-reports', categoryReportsRouter);
router.use('/geoword', geowordRouter);
router.use('/leaderboard', leaderboardRouter);

router.get('/', (req, res) => {
res.sendFile('index.html', { root: './client/admin' });
Expand Down
9 changes: 9 additions & 0 deletions routes/admin/leaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router } from 'express';

const router = Router();

router.get('/', async (req, res) => {
res.sendFile('index.html', { root: './client/admin/leaderboard' });
});

export default router;
2 changes: 2 additions & 0 deletions routes/api/admin/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import geowordRouter from './geoword.js';
import listReportsRouter from './list-reports.js';
import leaderboardRouter from './leaderboard.js';
import updateSubcategoryRouter from './update-subcategory.js';

import isAdmin from '../../../database/account-info/is-admin.js';
Expand Down Expand Up @@ -28,6 +29,7 @@ router.use(async (req, res, next) => {

router.use('/geoword', geowordRouter);
router.use('/list-reports', listReportsRouter);
router.use('/leaderboard', leaderboardRouter);
router.use('/update-subcategory', updateSubcategoryRouter);

export default router;
13 changes: 13 additions & 0 deletions routes/api/admin/leaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import leaderboard from '../../../database/account-info/leaderboard.js';

import { Router } from 'express';

const router = Router();

router.get('/', async (req, res) => {
const limit = req.query.limit ? parseInt(req.query.limit) : null;
const data = await leaderboard(limit);
res.json({ data });
});

export default router;

0 comments on commit 811da33

Please sign in to comment.