Skip to content

Commit

Permalink
close #187
Browse files Browse the repository at this point in the history
  • Loading branch information
geoffrey-wu committed Jul 9, 2023
1 parent f9007f1 commit d061a9a
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 34 deletions.
85 changes: 85 additions & 0 deletions client/geoword/compare.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!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="Select a geoword division.">

<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 src="/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="/tossups">Tossups</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/bonuses">Bonuses</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 active" 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>
</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>
Compare your stats on <b id="packet-name"></b> (<span id="division"></span>) against another player.
</p>
<form class="mb-3" id="form">
<div class="mb-3">
<label for="opponent" class="form-label">Enter a username here:</label>
<input type="text" class="form-control" id="opponent">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div id="root"></div>
</div>

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

<script src="/geoword/script.js"></script>
<script src="/geoword/compare.js"></script>
</body>

</html>
111 changes: 111 additions & 0 deletions client/geoword/compare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const packetName = window.location.pathname.split('/').pop();
const packetTitle = titleCase(packetName);
document.getElementById('packet-name').textContent = packetTitle;

let division;

fetch('/api/geoword/division-choice?' + new URLSearchParams({ packetName }))
.then(response => response.json())
.then(data => {
division = data.division;
document.getElementById('division').textContent = division;
});

document.getElementById('form').addEventListener('submit', event => {
event.preventDefault();

const opponent = document.getElementById('opponent').value;

fetch('/api/geoword/compare?' + new URLSearchParams({ packetName, division, opponent }))
.then(response => response.json())
.then(data => {
const { myBuzzes, opponentBuzzes } = data;

if (myBuzzes.length === 0) {
document.getElementById('root').innerHTML = `
<div class="alert alert-danger" role="alert">
No stats found for you.
</div>`;
return;
}

if (opponentBuzzes.length === 0) {
document.getElementById('root').innerHTML = `
<div class="alert alert-danger" role="alert">
No stats found for ${escapeHTML(opponent)}.
</div>`;
return;
}

let myPoints = 0;
let myTossupCount = 0;
let opponentPoints = 0;
let opponentTossupCount = 0;

let innerHTML = '';

for (let i = 0; i < Math.min(myBuzzes.length, opponentBuzzes.length); i++) {
const myBuzz = myBuzzes[i];
const opponentBuzz = opponentBuzzes[i];

if (myBuzz.points > 0 && opponentBuzz.points === 0) {
myPoints += myBuzz.points;
myTossupCount++;
} else if (myBuzz.points === 0 && opponentBuzz.points > 0) {
opponentPoints += opponentBuzz.points;
opponentTossupCount++;
} else if (myBuzz.points > 0 && opponentBuzz.points > 0) {
if (myBuzz.celerity > opponentBuzz.celerity) {
myPoints += myBuzz.points;
myTossupCount++;
} else if (myBuzz.celerity < opponentBuzz.celerity) {
opponentPoints += opponentBuzz.points;
opponentTossupCount++;
} else {
myPoints += myBuzz.points / 2;
myTossupCount++;
opponentPoints += opponentBuzz.points / 2;
opponentTossupCount++;
}
}

innerHTML += `
<hr>
<div class="row mb-3">
<div class="col-6">
<div><b>#${myBuzz.questionNumber}</b></div>
<div><b>Celerity:</b> ${(myBuzz.celerity ?? 0.0).toFixed(3)}</div>
<div><b>Points:</b> ${myBuzz.points}</div>
<div><b>Given answer:</b> ${escapeHTML(myBuzz.givenAnswer)}</div>
</div>
<div class="col-6">
<div><b>Answer:</b> ${removeParentheses(myBuzz.formatted_answer ?? myBuzz.answer)}</div>
<div><b>Celerity:</b> ${(opponentBuzz.celerity ?? 0.0).toFixed(3)}</div>
<div><b>Points:</b> ${opponentBuzz.points}</div>
<div><b>Given answer:</b> ${escapeHTML(opponentBuzz.givenAnswer)}</div>
</div>
</div>`;
}

innerHTML = `
<div class="row mb-3">
<div class="text-center col-6">
<div class="lead">Your stats:</div>
${myPoints} points (${myTossupCount} tossups)
</div>
<div class="text-center col-6">
<div class="lead">${escapeHTML(opponent)}'s stats:</div>
${opponentPoints} points (${opponentTossupCount} tossups)
</div>
</div>
` + innerHTML;



document.getElementById('root').innerHTML = innerHTML;
});
});

function removeParentheses(answer) {
return answer.replace(/[([].*/g, '');
}
3 changes: 3 additions & 0 deletions client/geoword/stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
<p>
View the tossups from this packet: <span id="packet-links"></span>
</p>
<p>
Compare your stats against another player <a id="compare-link">by clicking here</a>.
</p>
<div id="stats"></div>
</div>

Expand Down
1 change: 1 addition & 0 deletions client/geoword/stats.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const packetName = window.location.pathname.split('/').pop();
const packetTitle = titleCase(packetName);

document.getElementById('compare-link').href = `/geoword/compare/${packetName}`;
document.getElementById('packet-name').textContent = packetTitle;

fetch('/api/geoword/stats?' + new URLSearchParams({ packetName }))
Expand Down
81 changes: 48 additions & 33 deletions database/geoword.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,50 @@ async function getAnswer(packetName, division, questionNumber) {
}
}

/**
*
* @param {String} packetName
* @param {String} division
* @param {ObjectId} user_id
* @param {Boolean} protests - whether to include protests (default: false)
*/
async function getBuzzes(packetName, division, user_id, protests=false) {
const projection = {
_id: 0,
celerity: 1,
points: 1,
questionNumber: 1,
answer: '$tossup.answer',
formatted_answer: '$tossup.formatted_answer',
givenAnswer: 1,
};

if (protests) {
projection.pendingProtest = 1;
projection.decision = 1;
projection.reason = 1;
}

return await buzzes.aggregate([
{ $match: { packetName, division, user_id } },
{ $sort: { questionNumber: 1 } },
{ $lookup: {
from: 'tossups',
let: { questionNumber: '$questionNumber', packetName, division },
pipeline: [
{ $match: { $expr: { $and: [
{ $eq: ['$questionNumber', '$$questionNumber'] },
{ $eq: ['$packetName', '$$packetName'] },
{ $eq: ['$division', '$$division'] },
] } } },
],
as: 'tossup',
} },
{ $unwind: '$tossup' },
{ $project: projection },
]).toArray();
}

async function getBuzzCount(packetName, username) {
const user_id = await getUserId(username);
return await buzzes.countDocuments({ packetName, user_id });
Expand Down Expand Up @@ -236,42 +280,12 @@ async function getQuestionCount(packetName, division) {
}

/**
* @param {Object} params
* @param {String} params.packetName
* @param {String} packetName
* @param {ObjectId} user_id
*/
async function getUserStats({ packetName, user_id }) {
async function getUserStats(packetName, user_id) {
const division = await getDivisionChoiceById(packetName, user_id);

const buzzArray = await buzzes.aggregate([
{ $match: { packetName, user_id } },
{ $sort: { questionNumber: 1 } },
{ $lookup: {
from: 'tossups',
let: { questionNumber: '$questionNumber', packetName, division },
pipeline: [
{ $match: { $expr: { $and: [
{ $eq: ['$questionNumber', '$$questionNumber'] },
{ $eq: ['$packetName', '$$packetName'] },
{ $eq: ['$division', '$$division'] },
] } } },
],
as: 'tossup',
} },
{ $unwind: '$tossup' },
{ $project: {
_id: 0,
celerity: 1,
pendingProtest: 1,
points: 1,
questionNumber: 1,
answer: '$tossup.answer',
formatted_answer: '$tossup.formatted_answer',
givenAnswer: 1,
decision: 1,
reason: 1,
} },
]).toArray();
const buzzArray = await getBuzzes(packetName, division, user_id, true);

const leaderboard = await buzzes.aggregate([
{ $match: { packetName, division, active: true } },
Expand Down Expand Up @@ -391,6 +405,7 @@ export {
getAdminStats,
getAnswer,
getBuzzCount,
getBuzzes,
getCost,
getDivisionChoice,
getDivisions,
Expand Down
31 changes: 30 additions & 1 deletion routes/api/geoword.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ import stripeClass from 'stripe';
const router = Router();
const stripe = new stripeClass(process.env.STRIPE_SECRET_KEY);

router.get('/compare', async (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
delete req.session;
res.redirect('/geoword/login');
return;
}

const { packetName, division, opponent } = req.query;
const myBuzzes = await geoword.getBuzzes(packetName, division, await getUserId(username));
const opponentBuzzes = (await geoword.getBuzzes(packetName, division, await getUserId(opponent))).slice(0, myBuzzes.length);

return res.json({ myBuzzes, opponentBuzzes });
});

router.post('/create-payment-intent', async (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
Expand Down Expand Up @@ -42,6 +57,20 @@ router.get('/check-answer', async (req, res) => {
res.json({ actualAnswer: answer, directive, directedPrompt });
});

router.get('/division-choice', async (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
delete req.session;
res.redirect('/geoword/login');
return;
}

const { packetName } = req.query;
const division = await geoword.getDivisionChoice(packetName, username);

res.json({ division });
});

router.get('/get-progress', async (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
Expand Down Expand Up @@ -173,7 +202,7 @@ router.get('/stats', async (req, res) => {

const user_id = await getUserId(username);
const { packetName } = req.query;
const { buzzArray, division, leaderboard } = await geoword.getUserStats({ packetName, user_id });
const { buzzArray, division, leaderboard } = await geoword.getUserStats(packetName, user_id);
res.json({ buzzArray, division, leaderboard });
});

Expand Down
13 changes: 13 additions & 0 deletions routes/geoword.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ router.use('/*/:packetName', async (req, res, next) => {
next();
});

router.get('/compare/:packetName', async (req, res) => {
const { username } = req.session;
const packetName = req.params.packetName;
const paid = await geoword.checkPayment({ packetName, username });

if (!paid) {
res.redirect('/geoword/payment/' + packetName);
return;
}

res.sendFile('compare.html', { root: './client/geoword' });
});

router.get('/division/:packetName', async (req, res) => {
const { username } = req.session;
const packetName = req.params.packetName;
Expand Down

0 comments on commit d061a9a

Please sign in to comment.