Skip to content

Commit d061a9a

Browse files
committed
close #187
1 parent f9007f1 commit d061a9a

File tree

7 files changed

+291
-34
lines changed

7 files changed

+291
-34
lines changed

client/geoword/compare.html

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<title>QB Reader</title>
6+
<meta charset="utf-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<meta name="description" content="Select a geoword division.">
9+
10+
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
11+
<link href="/apple-touch-icon-precomposed.png" rel="apple-touch-icon-precomposed">
12+
<link type="image/x-icon" href="/favicon.ico" rel="icon">
13+
14+
<link href="/bootstrap/light.css" rel="stylesheet">
15+
<link href="/bootstrap/dark.css" rel="stylesheet" id="custom-css">
16+
<script src="/apply-theme.js"></script>
17+
</head>
18+
19+
<body>
20+
<nav class="navbar navbar-light navbar-expand-lg bg-custom" id="navbar" style="z-index: 10">
21+
<div class="container-fluid">
22+
<a class="navbar-brand ms-1 py-0" id="logo" href="/">
23+
<span class="logo-prefix">QB</span><span class="logo-suffix">Reader</span>
24+
</a>
25+
<button class="navbar-toggler" data-bs-target="#navbarSupportedContent" data-bs-toggle="collapse" type="button"
26+
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
27+
<span class="navbar-toggler-icon"></span>
28+
</button>
29+
<div class="collapse navbar-collapse" id="navbarSupportedContent">
30+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
31+
<li class="nav-item">
32+
<a class="nav-link" href="/tossups">Tossups</a>
33+
</li>
34+
<li class="nav-item">
35+
<a class="nav-link" href="/bonuses">Bonuses</a>
36+
</li>
37+
<li class="nav-item">
38+
<a class="nav-link" href="/multiplayer">Multiplayer</a>
39+
</li>
40+
<li class="nav-item">
41+
<a class="nav-link" href="/db">Database</a>
42+
</li>
43+
<li class="nav-item">
44+
<a class="nav-link active" href="/geoword">Geoword</a>
45+
</li>
46+
<li class="nav-item">
47+
<a class="nav-link" href="/api-docs">API</a>
48+
</li>
49+
<li class="nav-item">
50+
<a class="nav-link" href="/about">About</a>
51+
</li>
52+
</ul>
53+
<div class="d-flex">
54+
<ul class="navbar-nav mb-2 mb-lg-0">
55+
<li class="nav-item">
56+
<a class="nav-link" href="/user/login" id="login-link">Log in</a>
57+
</li>
58+
</ul>
59+
</div>
60+
</div>
61+
</div>
62+
</nav>
63+
64+
<div class="container-xl mt-3 mb-5 pb-5">
65+
<p>
66+
Compare your stats on <b id="packet-name"></b> (<span id="division"></span>) against another player.
67+
</p>
68+
<form class="mb-3" id="form">
69+
<div class="mb-3">
70+
<label for="opponent" class="form-label">Enter a username here:</label>
71+
<input type="text" class="form-control" id="opponent">
72+
</div>
73+
<button type="submit" class="btn btn-primary">Submit</button>
74+
</form>
75+
<div id="root"></div>
76+
</div>
77+
78+
<script src="/bootstrap/bootstrap.bundle.min.js"></script>
79+
<script src="/script.js"></script>
80+
81+
<script src="/geoword/script.js"></script>
82+
<script src="/geoword/compare.js"></script>
83+
</body>
84+
85+
</html>

client/geoword/compare.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const packetName = window.location.pathname.split('/').pop();
2+
const packetTitle = titleCase(packetName);
3+
document.getElementById('packet-name').textContent = packetTitle;
4+
5+
let division;
6+
7+
fetch('/api/geoword/division-choice?' + new URLSearchParams({ packetName }))
8+
.then(response => response.json())
9+
.then(data => {
10+
division = data.division;
11+
document.getElementById('division').textContent = division;
12+
});
13+
14+
document.getElementById('form').addEventListener('submit', event => {
15+
event.preventDefault();
16+
17+
const opponent = document.getElementById('opponent').value;
18+
19+
fetch('/api/geoword/compare?' + new URLSearchParams({ packetName, division, opponent }))
20+
.then(response => response.json())
21+
.then(data => {
22+
const { myBuzzes, opponentBuzzes } = data;
23+
24+
if (myBuzzes.length === 0) {
25+
document.getElementById('root').innerHTML = `
26+
<div class="alert alert-danger" role="alert">
27+
No stats found for you.
28+
</div>`;
29+
return;
30+
}
31+
32+
if (opponentBuzzes.length === 0) {
33+
document.getElementById('root').innerHTML = `
34+
<div class="alert alert-danger" role="alert">
35+
No stats found for ${escapeHTML(opponent)}.
36+
</div>`;
37+
return;
38+
}
39+
40+
let myPoints = 0;
41+
let myTossupCount = 0;
42+
let opponentPoints = 0;
43+
let opponentTossupCount = 0;
44+
45+
let innerHTML = '';
46+
47+
for (let i = 0; i < Math.min(myBuzzes.length, opponentBuzzes.length); i++) {
48+
const myBuzz = myBuzzes[i];
49+
const opponentBuzz = opponentBuzzes[i];
50+
51+
if (myBuzz.points > 0 && opponentBuzz.points === 0) {
52+
myPoints += myBuzz.points;
53+
myTossupCount++;
54+
} else if (myBuzz.points === 0 && opponentBuzz.points > 0) {
55+
opponentPoints += opponentBuzz.points;
56+
opponentTossupCount++;
57+
} else if (myBuzz.points > 0 && opponentBuzz.points > 0) {
58+
if (myBuzz.celerity > opponentBuzz.celerity) {
59+
myPoints += myBuzz.points;
60+
myTossupCount++;
61+
} else if (myBuzz.celerity < opponentBuzz.celerity) {
62+
opponentPoints += opponentBuzz.points;
63+
opponentTossupCount++;
64+
} else {
65+
myPoints += myBuzz.points / 2;
66+
myTossupCount++;
67+
opponentPoints += opponentBuzz.points / 2;
68+
opponentTossupCount++;
69+
}
70+
}
71+
72+
innerHTML += `
73+
<hr>
74+
<div class="row mb-3">
75+
<div class="col-6">
76+
<div><b>#${myBuzz.questionNumber}</b></div>
77+
<div><b>Celerity:</b> ${(myBuzz.celerity ?? 0.0).toFixed(3)}</div>
78+
<div><b>Points:</b> ${myBuzz.points}</div>
79+
<div><b>Given answer:</b> ${escapeHTML(myBuzz.givenAnswer)}</div>
80+
</div>
81+
<div class="col-6">
82+
<div><b>Answer:</b> ${removeParentheses(myBuzz.formatted_answer ?? myBuzz.answer)}</div>
83+
<div><b>Celerity:</b> ${(opponentBuzz.celerity ?? 0.0).toFixed(3)}</div>
84+
<div><b>Points:</b> ${opponentBuzz.points}</div>
85+
<div><b>Given answer:</b> ${escapeHTML(opponentBuzz.givenAnswer)}</div>
86+
</div>
87+
</div>`;
88+
}
89+
90+
innerHTML = `
91+
<div class="row mb-3">
92+
<div class="text-center col-6">
93+
<div class="lead">Your stats:</div>
94+
${myPoints} points (${myTossupCount} tossups)
95+
</div>
96+
<div class="text-center col-6">
97+
<div class="lead">${escapeHTML(opponent)}'s stats:</div>
98+
${opponentPoints} points (${opponentTossupCount} tossups)
99+
</div>
100+
</div>
101+
` + innerHTML;
102+
103+
104+
105+
document.getElementById('root').innerHTML = innerHTML;
106+
});
107+
});
108+
109+
function removeParentheses(answer) {
110+
return answer.replace(/[([].*/g, '');
111+
}

client/geoword/stats.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
<p>
7474
View the tossups from this packet: <span id="packet-links"></span>
7575
</p>
76+
<p>
77+
Compare your stats against another player <a id="compare-link">by clicking here</a>.
78+
</p>
7679
<div id="stats"></div>
7780
</div>
7881

client/geoword/stats.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const packetName = window.location.pathname.split('/').pop();
22
const packetTitle = titleCase(packetName);
33

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

67
fetch('/api/geoword/stats?' + new URLSearchParams({ packetName }))

database/geoword.js

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,50 @@ async function getAnswer(packetName, division, questionNumber) {
9898
}
9999
}
100100

101+
/**
102+
*
103+
* @param {String} packetName
104+
* @param {String} division
105+
* @param {ObjectId} user_id
106+
* @param {Boolean} protests - whether to include protests (default: false)
107+
*/
108+
async function getBuzzes(packetName, division, user_id, protests=false) {
109+
const projection = {
110+
_id: 0,
111+
celerity: 1,
112+
points: 1,
113+
questionNumber: 1,
114+
answer: '$tossup.answer',
115+
formatted_answer: '$tossup.formatted_answer',
116+
givenAnswer: 1,
117+
};
118+
119+
if (protests) {
120+
projection.pendingProtest = 1;
121+
projection.decision = 1;
122+
projection.reason = 1;
123+
}
124+
125+
return await buzzes.aggregate([
126+
{ $match: { packetName, division, user_id } },
127+
{ $sort: { questionNumber: 1 } },
128+
{ $lookup: {
129+
from: 'tossups',
130+
let: { questionNumber: '$questionNumber', packetName, division },
131+
pipeline: [
132+
{ $match: { $expr: { $and: [
133+
{ $eq: ['$questionNumber', '$$questionNumber'] },
134+
{ $eq: ['$packetName', '$$packetName'] },
135+
{ $eq: ['$division', '$$division'] },
136+
] } } },
137+
],
138+
as: 'tossup',
139+
} },
140+
{ $unwind: '$tossup' },
141+
{ $project: projection },
142+
]).toArray();
143+
}
144+
101145
async function getBuzzCount(packetName, username) {
102146
const user_id = await getUserId(username);
103147
return await buzzes.countDocuments({ packetName, user_id });
@@ -236,42 +280,12 @@ async function getQuestionCount(packetName, division) {
236280
}
237281

238282
/**
239-
* @param {Object} params
240-
* @param {String} params.packetName
283+
* @param {String} packetName
241284
* @param {ObjectId} user_id
242285
*/
243-
async function getUserStats({ packetName, user_id }) {
286+
async function getUserStats(packetName, user_id) {
244287
const division = await getDivisionChoiceById(packetName, user_id);
245-
246-
const buzzArray = await buzzes.aggregate([
247-
{ $match: { packetName, user_id } },
248-
{ $sort: { questionNumber: 1 } },
249-
{ $lookup: {
250-
from: 'tossups',
251-
let: { questionNumber: '$questionNumber', packetName, division },
252-
pipeline: [
253-
{ $match: { $expr: { $and: [
254-
{ $eq: ['$questionNumber', '$$questionNumber'] },
255-
{ $eq: ['$packetName', '$$packetName'] },
256-
{ $eq: ['$division', '$$division'] },
257-
] } } },
258-
],
259-
as: 'tossup',
260-
} },
261-
{ $unwind: '$tossup' },
262-
{ $project: {
263-
_id: 0,
264-
celerity: 1,
265-
pendingProtest: 1,
266-
points: 1,
267-
questionNumber: 1,
268-
answer: '$tossup.answer',
269-
formatted_answer: '$tossup.formatted_answer',
270-
givenAnswer: 1,
271-
decision: 1,
272-
reason: 1,
273-
} },
274-
]).toArray();
288+
const buzzArray = await getBuzzes(packetName, division, user_id, true);
275289

276290
const leaderboard = await buzzes.aggregate([
277291
{ $match: { packetName, division, active: true } },
@@ -391,6 +405,7 @@ export {
391405
getAdminStats,
392406
getAnswer,
393407
getBuzzCount,
408+
getBuzzes,
394409
getCost,
395410
getDivisionChoice,
396411
getDivisions,

routes/api/geoword.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ import stripeClass from 'stripe';
99
const router = Router();
1010
const stripe = new stripeClass(process.env.STRIPE_SECRET_KEY);
1111

12+
router.get('/compare', async (req, res) => {
13+
const { username, token } = req.session;
14+
if (!checkToken(username, token)) {
15+
delete req.session;
16+
res.redirect('/geoword/login');
17+
return;
18+
}
19+
20+
const { packetName, division, opponent } = req.query;
21+
const myBuzzes = await geoword.getBuzzes(packetName, division, await getUserId(username));
22+
const opponentBuzzes = (await geoword.getBuzzes(packetName, division, await getUserId(opponent))).slice(0, myBuzzes.length);
23+
24+
return res.json({ myBuzzes, opponentBuzzes });
25+
});
26+
1227
router.post('/create-payment-intent', async (req, res) => {
1328
const { username, token } = req.session;
1429
if (!checkToken(username, token)) {
@@ -42,6 +57,20 @@ router.get('/check-answer', async (req, res) => {
4257
res.json({ actualAnswer: answer, directive, directedPrompt });
4358
});
4459

60+
router.get('/division-choice', async (req, res) => {
61+
const { username, token } = req.session;
62+
if (!checkToken(username, token)) {
63+
delete req.session;
64+
res.redirect('/geoword/login');
65+
return;
66+
}
67+
68+
const { packetName } = req.query;
69+
const division = await geoword.getDivisionChoice(packetName, username);
70+
71+
res.json({ division });
72+
});
73+
4574
router.get('/get-progress', async (req, res) => {
4675
const { username, token } = req.session;
4776
if (!checkToken(username, token)) {
@@ -173,7 +202,7 @@ router.get('/stats', async (req, res) => {
173202

174203
const user_id = await getUserId(username);
175204
const { packetName } = req.query;
176-
const { buzzArray, division, leaderboard } = await geoword.getUserStats({ packetName, user_id });
205+
const { buzzArray, division, leaderboard } = await geoword.getUserStats(packetName, user_id);
177206
res.json({ buzzArray, division, leaderboard });
178207
});
179208

routes/geoword.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ router.use('/*/:packetName', async (req, res, next) => {
6262
next();
6363
});
6464

65+
router.get('/compare/:packetName', async (req, res) => {
66+
const { username } = req.session;
67+
const packetName = req.params.packetName;
68+
const paid = await geoword.checkPayment({ packetName, username });
69+
70+
if (!paid) {
71+
res.redirect('/geoword/payment/' + packetName);
72+
return;
73+
}
74+
75+
res.sendFile('compare.html', { root: './client/geoword' });
76+
});
77+
6578
router.get('/division/:packetName', async (req, res) => {
6679
const { username } = req.session;
6780
const packetName = req.params.packetName;

0 commit comments

Comments
 (0)