diff --git a/.gitignore b/.gitignore index 863580d68..0541648f6 100644 --- a/.gitignore +++ b/.gitignore @@ -73,7 +73,10 @@ public/css/style.css public/css/style.css.map functions/serviceAccountKey.json functions/serviceAccountKey_live.json +functions/serviceAccountKey_copy.json +functions/serviceAccountKey_live_copy.json .firebaserc .firebaserc_copy functions/serviceAccountKey_copy.json functions/serviceAccountKey_live_copy.json + diff --git a/firebase.json b/firebase.json index 64d0fa9b9..a148775e0 100644 --- a/firebase.json +++ b/firebase.json @@ -6,25 +6,22 @@ "**/.*", "**/node_modules/**" ], - "redirects": [ - { - "source": "/soon", - "destination": "/", - "type": 301 - } - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ], + "redirects": [{ + "source": "/soon", + "destination": "/", + "type": 301 + }], + "rewrites": [{ + "source": "**", + "destination": "/index.html" + }], "cleanUrls": true, "trailingSlash": false - }, - "functions": { - "predeploy": [ - "npm --prefix \"$RESOURCE_DIR\" run lint" - ] } -} + // }, + // "functions": { + // "predeploy": [ + // "npm --prefix \"$RESOURCE_DIR\" run lint" + // ] + // } +} \ No newline at end of file diff --git a/functions/index.js b/functions/index.js index caa96bf34..ca7cc43ea 100644 --- a/functions/index.js +++ b/functions/index.js @@ -60,7 +60,7 @@ function getAllUsers() { function isUsernameValid(name) { if (name === null || name === undefined || name === "") return false; - if (/miodec/.test(name)) return false; + if (/miodec/.test(name.toLowerCase())) return false; if (name.length > 12) return false; return /^[0-9a-zA-Z_.-]+$/.test(name); } @@ -141,63 +141,92 @@ exports.changeName = functions.https.onCall((request, response) => { exports.checkIfNeedsToChangeName = functions.https.onCall( (request, response) => { try { - return admin - .auth() - .getUser(request.uid) - .then((requestUser) => { - if (!isUsernameValid(requestUser.displayName)) { - //invalid name, needs to change - console.log( - `user ${requestUser.uid} ${requestUser.displayName} needs to change name` - ); - return 1; - } else { - //valid name, but need to change if not duplicate - - return getAllUsers().then((users) => { - let sameName = []; - - //look for name names - users.forEach((user) => { - if (user.uid !== requestUser.uid) { - try { - if ( - user.displayName.toLowerCase() === - requestUser.displayName.toLowerCase() - ) { - sameName.push(user); - } - } catch (e) { - // - } - } - }); - - if (sameName.length === 0) { - return 0; - } else { - //check when the request user made the account compared to others - let earliestTimestamp = 999999999999999; - sameName.forEach((sn) => { - let ts = new Date(sn.metadata.creationTime).getTime() / 1000; - if (ts <= earliestTimestamp) { - earliestTimestamp = ts; - } - }); - - if ( - new Date(requestUser.metadata.creationTime).getTime() / 1000 > - earliestTimestamp - ) { + return db + .collection("users") + .doc(request.uid) + .get() + .then((doc) => { + if (doc.data().name === undefined) { + return admin + .auth() + .getUser(request.uid) + .then((requestUser) => { + if (!isUsernameValid(requestUser.displayName)) { + //invalid name, needs to change console.log( `user ${requestUser.uid} ${requestUser.displayName} needs to change name` ); - return 2; + return 1; } else { - return 0; + //valid name, but need to change if not duplicate + + return getAllUsers() + .then((users) => { + let sameName = []; + + //look for name names + users.forEach((user) => { + if (user.uid !== requestUser.uid) { + try { + if ( + user.displayName.toLowerCase() === + requestUser.displayName.toLowerCase() + ) { + sameName.push(user); + } + } catch (e) { + // + } + } + }); + + if (sameName.length === 0) { + db.collection("users") + .doc(request.uid) + .update({ name: requestUser.displayName }) + .then(() => { + return 0; + }); + } else { + //check when the request user made the account compared to others + let earliestTimestamp = 999999999999999; + sameName.forEach((sn) => { + let ts = + new Date(sn.metadata.creationTime).getTime() / 1000; + if (ts <= earliestTimestamp) { + earliestTimestamp = ts; + } + }); + + if ( + new Date( + requestUser.metadata.creationTime + ).getTime() / + 1000 > + earliestTimestamp + ) { + console.log( + `user ${requestUser.uid} ${requestUser.displayName} needs to change name` + ); + return 2; + } else { + db.collection("users") + .doc(request.uid) + .update({ name: requestUser.displayName }) + .then(() => { + return 0; + }); + } + } + }) + .catch((e) => { + console.error(`error getting all users - ${e}`); + }); } - } - }); + }); + } else { + console.log("name is good"); + return 0; } }); } catch (e) { @@ -326,11 +355,19 @@ function checkIfPB(uid, obj) { }); } +function stdDev(array) { + const n = array.length; + const mean = array.reduce((a, b) => a + b) / n; + return Math.sqrt( + array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n + ); +} + exports.testCompleted = functions.https.onCall((request, response) => { try { if (request.uid === undefined || request.obj === undefined) { console.error(`error saving result for ${request.uid} - missing input`); - return -1; + return { resultCode: -999 }; } let obj = request.obj; @@ -359,18 +396,75 @@ exports.testCompleted = functions.https.onCall((request, response) => { return -1; } + let keySpacing = { + average: + obj.keySpacing.reduce((previous, current) => (current += previous)) / + obj.keySpacing.length, + sd: stdDev(obj.keySpacing), + }; + + let keyDuration = { + average: + obj.keyDuration.reduce((previous, current) => (current += previous)) / + obj.keyDuration.length, + sd: stdDev(obj.keyDuration), + }; + return db .collection("users") .doc(request.uid) .get() .then((ret) => { let userdata = ret.data(); + let name = userdata.name === undefined ? false : userdata.name; + let banned = userdata.banned === undefined ? false : userdata.banned; + let verified = + userdata.verified === undefined ? false : userdata.verified; + + request.obj.name = name; + + //check keyspacing and duration here + if (!verified) { + if ( + keySpacing.sd < 15 || + keyDuration.sd < 15 || + keyDuration.average < 15 + ) { + console.error( + `possible bot detected by user ${ + request.uid + } ${name} - spacing ${JSON.stringify( + keySpacing + )} duration ${JSON.stringify(keyDuration)}` + ); + return { resultCode: -2 }; + } + } + return db .collection(`users/${request.uid}/results`) .add(obj) .then((e) => { - return checkIfPB(request.uid, request.obj).then((e) => { - if (e) { + return Promise.all([ + checkLeaderboards(request.obj, "global", banned, name), + checkLeaderboards(request.obj, "daily", banned, name), + checkIfPB(request.uid, request.obj), + ]).then((values) => { + let globallb = values[0].insertedAt; + let dailylb = values[1].insertedAt; + let ispb = values[2]; + // console.log(values); + + let returnobj = { + resultCode: null, + globalLeaderboard: globallb, + dailyLeaderboard: dailylb, + lbBanned: banned, + name: name, + }; + request.obj.keySpacing = "removed"; + request.obj.keyDuration = "removed"; + if (ispb) { console.log( `saved result for ${request.uid} (new PB) - ${JSON.stringify( request.obj @@ -388,33 +482,35 @@ exports.testCompleted = functions.https.onCall((request, response) => { updateDiscordRole(userdata.discordId, Math.round(obj.wpm)); return; } - return 2; + returnobj.resultCode = 2; } else { console.log( `saved result for ${request.uid} - ${JSON.stringify( request.obj )}` ); - return 1; + returnobj.resultCode = 1; } + // console.log(returnobj); + return returnobj; }); }) .catch((e) => { console.error( - `error saving result when checking for PB for ${request.uid} - ${e.message}` + `error saving result when checking for PB / checking leaderboards for ${request.uid} - ${e.message}` ); - return -1; + return { resultCode: -999 }; }); }) .catch((e) => { console.error( - `error saving result when getting user info ${request.uid} - ${e.message}` + `error saving result when getting user data for ${request.uid} - ${e.message}` ); - return -1; + return { resultCode: -999 }; }); } catch (e) { console.error(`error saving result for ${request.uid} - ${e}`); - return -1; + return { resultCode: -999 }; } }); @@ -634,6 +730,176 @@ function generate(n) { return ("" + number).substring(add); } +class Leaderboard { + constructor(size, mode, mode2, type, starting) { + this.size = size; + this.board = []; + this.mode = mode; + this.mode2 = mode2; + this.type = type; + if (starting !== undefined && starting !== null) { + starting.forEach((entry) => { + if (entry.mode === this.mode && entry.mode2 === this.mode2) { + this.board.push({ + uid: entry.uid, + wpm: parseFloat(entry.wpm), + raw: parseFloat(entry.raw), + acc: parseFloat(entry.acc), + mode: entry.mode, + mode2: entry.mode2, + timestamp: entry.timestamp, + }); + } + }); + } + this.sortBoard(); + this.clipBoard(); + } + sortBoard() { + this.board.sort((a, b) => { + if (a.wpm === b.wpm) { + if (a.acc === b.acc) { + return a.timestamp - b.timestamp; + } else { + return b.acc - a.acc; + } + } else { + return b.wpm - a.wpm; + } + }); + } + clipBoard() { + let boardLength = this.board.length; + if (boardLength > this.size) { + while (this.board.length !== this.size) { + this.board.pop(); + } + } + } + logBoard() { + console.log(this.board); + } + removeDuplicates(insertedAt, uid) { + //return true if a better result is found + let found = false; + // let ret; + let foundAt = null; + if (this.board !== undefined) { + this.board.forEach((entry, index) => { + if (entry.uid === uid) { + if (found) { + this.board.splice(index, 1); + // if (index > insertedAt) { + // //removed old result + // ret = false; + // } else { + // ret = true; + // } + } else { + found = true; + foundAt = index; + } + } + }); + } + // console.log(ret); + // return ret; + return foundAt; + } + insert(a) { + let insertedAt = -1; + if (a.mode === this.mode && a.mode2 === this.mode2) { + this.board.forEach((b, index) => { + if (insertedAt !== -1) return; + if (a.wpm === b.wpm) { + if (a.acc === b.acc) { + if (a.timestamp < b.timestamp) { + this.board.splice(index, 0, { + uid: a.uid, + name: a.name, + wpm: parseFloat(a.wpm), + raw: parseFloat(a.rawWpm), + acc: parseFloat(a.acc), + mode: a.mode, + mode2: a.mode2, + timestamp: a.timestamp, + }); + insertedAt = index; + } + } else { + if (a.acc > b.acc) { + this.board.splice(index, 0, { + uid: a.uid, + name: a.name, + wpm: parseFloat(a.wpm), + raw: parseFloat(a.rawWpm), + acc: parseFloat(a.acc), + mode: a.mode, + mode2: a.mode2, + timestamp: a.timestamp, + }); + insertedAt = index; + } + } + } else { + if (a.wpm > b.wpm) { + this.board.splice(index, 0, { + uid: a.uid, + name: a.name, + wpm: parseFloat(a.wpm), + raw: parseFloat(a.rawWpm), + acc: parseFloat(a.acc), + mode: a.mode, + mode2: a.mode2, + timestamp: a.timestamp, + }); + insertedAt = index; + } + } + }); + if (this.board.length < this.size && insertedAt === -1) { + this.board.push({ + uid: a.uid, + name: a.name, + wpm: parseFloat(a.wpm), + raw: parseFloat(a.rawWpm), + acc: parseFloat(a.acc), + mode: a.mode, + mode2: a.mode2, + timestamp: a.timestamp, + }); + insertedAt = this.board.length - 1; + } + // console.log("before duplicate remove"); + // console.log(this.board); + let newBest = false; + let foundAt = null; + if (insertedAt >= 0) { + // if (this.removeDuplicates(insertedAt, a.uid)) { + // insertedAt = -2; + // } + foundAt = this.removeDuplicates(insertedAt, a.uid); + + if (foundAt >= insertedAt) { + //new better result + newBest = true; + } + } + // console.log(this.board); + this.clipBoard(); + return { + insertedAt: insertedAt, + newBest: newBest, + foundAt: foundAt, + }; + } else { + return { + insertedAt: -999, + }; + } + } +} + exports.generatePairingCode = functions.https.onCall((request, response) => { try { if (request === null) { @@ -720,21 +986,224 @@ exports.generatePairingCode = functions.https.onCall((request, response) => { } }); -// exports.getConfig = functions.https.onCall((request,response) => { -// try{ -// if(request.uid === undefined){ -// console.error(`error getting config for ${request.uid} - missing input`); -// return -1; -// } +async function checkLeaderboards(resultObj, type, banned, name) { + try { + if (!name) + return { + insertedAt: null, + noName: true, + }; + if (banned) + return { + insertedAt: null, + banned: true, + }; + if ( + resultObj.mode === "time" && + ["15", "60"].includes(String(resultObj.mode2)) && + resultObj.language === "english" + ) { + return db + .collection("leaderboards") + .where("mode", "==", String(resultObj.mode)) + .where("mode2", "==", String(resultObj.mode2)) + .where("type", "==", type) + .get() + .then((ret) => { + if (ret.docs.length === 0) { + //no lb found, create + console.log( + `no ${resultObj.mode} ${resultObj.mode2} ${type} leaderboard found - creating` + ); + let toAdd = { + size: 20, + mode: String(resultObj.mode), + mode2: String(resultObj.mode2), + type: type, + }; + return db + .collection("leaderboards") + .doc( + `${String(resultObj.mode)}_${String(resultObj.mode2)}_${type}` + ) + .set(toAdd) + .then((ret) => { + return cont( + `${String(resultObj.mode)}_${String( + resultObj.mode2 + )}_${type}`, + toAdd + ); + }); + } else { + //continue + return cont( + `${String(resultObj.mode)}_${String(resultObj.mode2)}_${type}`, + ret.docs[0].data() + ); + } + }); -// return admin.firestore().collection(`users`).doc(request.uid).get().then(e => { -// return e.data().config; -// }).catch(e => { -// console.error(`error getting config from DB for ${request.uid} - ${e.message}`); -// return -1; -// }); -// }catch(e){ -// console.error(`error getting config for ${request.uid} - ${e}`); -// return {resultCode:-999}; -// } -// }) + function cont(docid, documentData) { + let boardInfo = documentData; + let boardData = boardInfo.board; + + // console.log(`info ${JSON.stringify(boardInfo)}`); + // console.log(`data ${JSON.stringify(boardData)}`); + + let lb = new Leaderboard( + boardInfo.size, + resultObj.mode, + resultObj.mode2, + boardInfo.type, + boardData + ); + + // console.log("board created"); + // lb.logBoard(); + + let insertResult = lb.insert(resultObj); + + // console.log("board after inseft"); + // lb.logBoard(); + + if (insertResult.insertedAt >= 0) { + //update the database here + console.log( + `leaderboard changed ${resultObj.mode} ${ + resultObj.mode2 + } ${type} - ${JSON.stringify(lb.board)}` + ); + db.collection("leaderboards").doc(docid).set( + { + size: lb.size, + type: lb.type, + board: lb.board, + }, + { merge: true } + ); + } else { + // console.log("board is the same"); + } + + return { + insertedAt: insertResult, + }; + } + } else { + return { + insertedAt: null, + }; + } + } catch (e) { + console.error( + `error while checking leaderboards - ${e} - ${type} ${resultObj}` + ); + return null; + } +} + +exports.getLeaderboard = functions.https.onCall((request, response) => { + return db + .collection("leaderboards") + .where("mode", "==", String(request.mode)) + .where("mode2", "==", String(request.mode2)) + .where("type", "==", String(request.type)) + .get() + .then(async (data) => { + // console.log("got data"); + if (data.docs.length === 0) return null; + let lbdata = data.docs[0].data(); + if (lbdata.board !== undefined) { + // console.log("replacing users"); + + // for (let i = 0; i < lbdata.board.length; i++) { + // await db + // .collection("users") + // .doc(lbdata.board[i].uid) + // .get() + // .then((doc) => { + // if ( + // lbdata.board[i].uid !== null && + // lbdata.board[i].uid === request.uid + // ) { + // lbdata.board[i].currentUser = true; + // } + // lbdata.board[i].name = doc.data().name; + // lbdata.board[i].uid = null; + // }); + // } + + lbdata.board.forEach((boardentry) => { + if (boardentry.uid !== null && boardentry.uid === request.uid) { + boardentry.currentUser = true; + } + boardentry.uid = null; + }); + + // console.log(lbdata); + if (request.type === "daily") { + let resetTime = new Date(Date.now()); + resetTime.setHours(0, 0, 0, 0); + resetTime.setDate(resetTime.getUTCDate() + 1); + resetTime = resetTime.valueOf(); + lbdata.resetTime = resetTime; + } + + return lbdata; + } else { + if ( + lbdata.board === undefined || + lbdata.board === [] || + lbdata.board.length === 0 + ) { + return lbdata; + } else { + return []; + } + } + }); +}); + +exports.scheduledFunctionCrontab = functions.pubsub + .schedule("00 00 * * *") + .timeZone("Africa/Abidjan") + .onRun((context) => { + try { + console.log("moving daily leaderboards to history"); + db.collection("leaderboards") + .where("type", "==", "daily") + .get() + .then((res) => { + res.docs.forEach((doc) => { + let lbdata = doc.data(); + t = new Date(); + db.collection("leaderboards_history") + .doc( + `${t.getUTCDate()}_${t.getUTCMonth()}_${t.getUTCFullYear()}_${ + lbdata.mode + }_${lbdata.mode2}` + ) + .set(lbdata); + db.collection("leaderboards").doc(doc.id).set( + { + board: [], + }, + { merge: true } + ); + }); + }); + return null; + } catch (e) { + console.error(`error while moving daily leaderboards to history - ${e}`); + } + }); + +async function announceLbUpdate(discordId, pos, lb, wpm) { + db.collection("bot-commands").add({ + command: "updateRole", + arguments: [discordId, pos, lb, wpm], + executed: false, + requestTimestamp: Date.now(), + }); +} diff --git a/public/css/style.scss b/public/css/style.scss index 76ee8517b..af6f5f488 100644 --- a/public/css/style.scss +++ b/public/css/style.scss @@ -10,6 +10,15 @@ /* Firefox */ } +html { + @extend .ffscroll; +} + +.ffscroll { + scrollbar-width: thin; + scrollbar-color: var(--sub-color) transparent; +} + input { outline: none; border: none; @@ -85,7 +94,7 @@ html { /* width */ ::-webkit-scrollbar { - width: 10px; + width: 7px; } /* Track */ @@ -135,6 +144,7 @@ a:hover { animation-iteration-count: infinite; animation-duration: 2s; animation-timing-function: cubic-bezier(0.38, 0.16, 0.57, 0.82); + z-index: 9999; } @keyframes loader { @@ -154,6 +164,127 @@ a:hover { } } +#leaderboardsWrapper { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + position: fixed; + left: 0; + top: 0; + z-index: 1000; + display: grid; + justify-content: center; + align-items: center; + padding: 5rem 0; + + #leaderboards { + width: 85vw; + height: calc(100vh - 10rem); + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 2rem; + grid-template-rows: 3rem auto; + grid-template-areas: "title buttons" + "tables tables"; + grid-template-columns: 1fr 1fr; + + .mainTitle { + font-size: 3rem; + line-height: 3rem; + grid-area: title; + } + + .title { + font-size: 2rem; + line-height: 2rem; + margin-bottom: .5rem; + } + + .tables { + grid-area: tables; + display: grid; + gap: 1rem; + grid-template-columns: 1fr 1fr; + margin-bottom: 2rem; + font-size: .8rem; + + .titleAndTable { + display: grid; + + .title { + grid-area: 1/1; + } + + .subtitle { + grid-area: 1/1; + align-self: center; + justify-self: right; + color: var(--sub-color); + } + } + + .globalTableWrapper, + .dailyTableWrapper { + height: calc(100vh - 22rem); + @extend .ffscroll; + overflow-y: scroll; + overflow-x: hidden; + } + + table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + + tr td:first-child { + text-align: center; + } + + td { + padding: .25rem .5rem; + + &.me { + color: var(--main-color); + font-weight: 900; + } + } + + thead { + color: var(--sub-color); + font-size: .75rem; + } + + tbody { + color: var(--text-color); + + tr:nth-child(odd) td { + background: rgba(0, 0, 0, .1); + } + } + } + } + + .buttons { + grid-area: buttons; + display: -ms-grid; + display: grid; + gap: 1rem; + grid-template-columns: 1fr 1fr; + align-self: center; + + .buttonGroup { + display: grid; + grid-auto-flow: column; + gap: 1rem; + grid-area: 1/2; + } + } + } +} + + #tagsWrapper { width: 100%; height: 100%; @@ -230,6 +361,7 @@ a:hover { padding: 2rem; display: grid; gap: 1rem; + @extend .ffscroll; overflow-y: scroll; .tip { @@ -346,6 +478,7 @@ a:hover { .suggestions { display: block; + @extend .ffscroll; overflow-y: scroll; max-height: calc(100vh - 10rem - 3rem); display: grid; @@ -791,7 +924,7 @@ key { .stats { display: grid; - gap: .5rem; + column-gap: .5rem; justify-content: center; align-items: center; grid-template-areas: @@ -799,9 +932,13 @@ key { "wpm key" "raw time" "source source" + "leaderboards leaderboards" "testType infoAndTags"; + .group { + margin-bottom: .5rem; + .top { color: var(--sub-color); font-size: 1rem; @@ -831,6 +968,22 @@ key { } } + .leaderboards { + align-self: baseline; + grid-area: leaderboards; + color: var(--sub-color); + + .top { + font-size: 1rem; + line-height: 1.25rem; + } + + .bottom { + font-size: 1rem; + line-height: 1rem; + } + } + .source { align-self: baseline; grid-area: source; diff --git a/public/index.html b/public/index.html index 8b0552822..215065fa9 100644 --- a/public/index.html +++ b/public/index.html @@ -52,6 +52,428 @@
+ +
+
+ +
+
@@ -228,6 +655,10 @@
-
+
+
leaderboards
+
-
+
@@ -852,14 +1283,14 @@ - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/public/js/account.js b/public/js/account.js index 52e639729..5edf0c86f 100644 --- a/public/js/account.js +++ b/public/js/account.js @@ -135,6 +135,11 @@ function signUp() { }) .then(function () { // Update successful. + firebase + .firestore() + .collection("users") + .doc(usr.uid) + .set({ name: nname }, { merge: true }); showNotification("Account created", 2000); $("#menu .icon-button.account .text").text(nname); try { @@ -147,11 +152,12 @@ function signUp() { }) .catch(function (error) { // An error happened. + console.error(error); usr .delete() .then(function () { // User deleted. - showNotification("Name invalid", 2000); + showNotification("An error occured", 2000); $(".pageLogin .preloader").addClass("hidden"); }) .catch(function (error) { @@ -210,7 +216,7 @@ firebase.auth().onAuthStateChanged(function (user) { // showNotification('Applying db config',3000); updateSettingsPage(); saveConfigToCookie(); - } else { + } else if (dbSnapshot.config !== undefined) { let configsDifferent = false; Object.keys(config).forEach((key) => { if (!configsDifferent) { diff --git a/public/js/leaderboards.js b/public/js/leaderboards.js new file mode 100644 index 000000000..f8e54af89 --- /dev/null +++ b/public/js/leaderboards.js @@ -0,0 +1,225 @@ +let currentLeaderboard = "time_15"; + +function showLeaderboards() { + if ($("#leaderboardsWrapper").hasClass("hidden")) { + $("#leaderboardsWrapper") + .stop(true, true) + .css("opacity", 0) + .removeClass("hidden") + .animate( + { + opacity: 1, + }, + 125, + () => { + updateLeaderboards(); + } + ); + } +} + +function hideLeaderboards() { + $("#leaderboardsWrapper") + .stop(true, true) + .css("opacity", 1) + .animate( + { + opacity: 0, + }, + 100, + () => { + $("#leaderboardsWrapper").addClass("hidden"); + } + ); + focusWords(); +} + +function updateLeaderboards() { + $("#leaderboardsWrapper .buttons .button").removeClass("active"); + $( + `#leaderboardsWrapper .buttons .button[board=${currentLeaderboard}]` + ).addClass("active"); + + // $( + // `#leaderboardsWrapper .leaderboardMode .button[mode=${currentLeaderboard.mode}]` + // ).addClass("active"); + + // $("#leaderboardsWrapper .leaderboardWords .button").removeClass("active"); + // $( + // `#leaderboardsWrapper .leaderboardWords .button[words=${currentLeaderboard.words}]` + // ).addClass("active"); + + // $("#leaderboardsWrapper .leaderboardTime .button").removeClass("active"); + // $( + // `#leaderboardsWrapper .leaderboardTime .button[time=${currentLeaderboard.time}]` + // ).addClass("active"); + + let boardinfo = currentLeaderboard.split("_"); + + // if (boardinfo[0] === "time") { + // $("#leaderboardsWrapper .leaderboardWords").addClass("hidden"); + // $("#leaderboardsWrapper .leaderboardTime").removeClass("hidden"); + // } else if (currentLeaderboard.mode === "words") { + // $("#leaderboardsWrapper .leaderboardWords").removeClass("hidden"); + // $("#leaderboardsWrapper .leaderboardTime").addClass("hidden"); + // } + + // let mode2; + // if (currentLeaderboard.mode === "words") { + // mode2 = currentLeaderboard.words; + // } else if (currentLeaderboard.mode === "time") { + // mode2 = currentLeaderboard.time; + // } + + let uid = null; + if (firebase.auth().currentUser !== null) { + uid = firebase.auth().currentUser.uid; + } + + showBackgroundLoader(); + Promise.all([ + firebase.functions().httpsCallable("getLeaderboard")({ + mode: boardinfo[0], + mode2: boardinfo[1], + type: "daily", + uid: uid, + }), + firebase.functions().httpsCallable("getLeaderboard")({ + mode: boardinfo[0], + mode2: boardinfo[1], + type: "global", + uid: uid, + }), + ]) + .then((lbdata) => { + hideBackgroundLoader(); + let dailyData = lbdata[0].data; + let globalData = lbdata[1].data; + + //daily + let diffAsDate = new Date(dailyData.resetTime - Date.now()); + + let diffHours = diffAsDate.getUTCHours(); + let diffMinutes = diffAsDate.getUTCMinutes(); + let diffSeconds = diffAsDate.getUTCSeconds(); + + let resetString = ""; + if (diffHours > 0) { + resetString = `resets in ${diffHours} ${ + diffHours == 1 ? "hour" : "hours" + } ${diffMinutes} ${diffMinutes == 1 ? "minute" : "minutes"} + `; + } else if (diffMinutes > 0) { + resetString = `resets in ${diffMinutes} ${ + diffMinutes == 1 ? "minute" : "minutes" + } ${diffSeconds} ${diffSeconds == 1 ? "second" : "seconds"}`; + } else if (diffSeconds > 0) { + resetString = `resets in ${diffSeconds} ${ + diffSeconds == 1 ? "second" : "seconds" + }`; + } + + $("#leaderboardsWrapper .subtitle").text(resetString); + + $("#leaderboardsWrapper table.daily tbody").empty(); + if (dailyData.board !== undefined) { + dailyData.board.forEach((entry, index) => { + let meClassString = ""; + if (entry.currentUser) meClassString = ' class="me"'; + $("#leaderboardsWrapper table.daily tbody").append(` + + ${ + index === 0 ? '' : index + 1 + } + ${entry.name} + ${entry.wpm} + ${entry.raw} + ${entry.acc}% + ${entry.mode} ${entry.mode2} + ${moment(entry.timestamp).format("DD MMM YYYY
HH:mm")} + + `); + }); + } + let lenDaily = 0; + if (dailyData.board !== undefined) lenDaily = dailyData.board.length; + if (dailyData.length === 0 || lenDaily !== dailyData.size) { + for (let i = lenDaily; i < dailyData.size; i++) { + $("#leaderboardsWrapper table.daily tbody").append(` + + ${i + 1} + - + - + - + - + - + -
- + + `); + } + } + + //global + $("#leaderboardsWrapper table.global tbody").empty(); + if (globalData.board !== undefined) { + globalData.board.forEach((entry, index) => { + let meClassString = ""; + if (entry.currentUser) meClassString = ' class="me"'; + $("#leaderboardsWrapper table.global tbody").append(` + + ${ + index === 0 ? '' : index + 1 + } + ${entry.name} + ${entry.wpm} + ${entry.raw} + ${entry.acc}% + ${entry.mode} ${entry.mode2} + ${moment(entry.timestamp).format("DD MMM YYYY
HH:mm")} + + `); + }); + } + let lenGlobal = 0; + if (globalData.board !== undefined) lenGlobal = globalData.board.length; + if (globalData.length === 0 || lenGlobal !== globalData.size) { + for (let i = lenGlobal; i < globalData.size; i++) { + $("#leaderboardsWrapper table.global tbody").append(` + + ${i + 1} + - + - + - + - + - + -
- + + `); + } + } + }) + .catch((e) => { + showNotification("Something went wrong", 3000); + }); +} + +$("#leaderboardsWrapper").click((e) => { + if ($(e.target).attr("id") === "leaderboardsWrapper") { + hideLeaderboards(); + } +}); + +$("#leaderboardsWrapper .buttons .button").click((e) => { + currentLeaderboard = $(e.target).attr("board"); + updateLeaderboards(); +}); + +// $("#leaderboardsWrapper .leaderboardWords .button").click((e) => { +// currentLeaderboard.words = $(e.target).attr("words"); +// updateLeaderboards(); +// }); + +// $("#leaderboardsWrapper .leaderboardTime .button").click((e) => { +// currentLeaderboard.time = $(e.target).attr("time"); +// updateLeaderboards(); +// }); diff --git a/public/js/script.js b/public/js/script.js index d614d187f..1378c8321 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -29,6 +29,17 @@ let accuracyStats = { incorrect: 0, }; +let keypressStats = { + spacing: { + current: -1, + array: [], + }, + duration: { + current: -1, + array: [], + }, +}; + let customText = "The quick brown fox jumps over the lazy dog".split(" "); let randomQuote = null; @@ -908,6 +919,9 @@ function showResult(difficultyFailed = false) { // 'margin-bottom': 0 }); + $("#result .stats .leaderboards .bottom").text(""); + $("#result .stats .leaderboards").addClass("hidden"); + let mode2 = ""; if (config.mode === "time") { mode2 = config.time; @@ -972,6 +986,29 @@ function showResult(difficultyFailed = false) { }); } + function stdDev(array) { + const n = array.length; + const mean = array.reduce((a, b) => a + b) / n; + return Math.sqrt( + array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n + ); + } + + console.log( + `avg time between keys ${ + keypressStats.spacing.array.reduce( + (previous, current) => (current += previous) + ) / keypressStats.spacing.array.length + } std(${stdDev(keypressStats.spacing.array)})` + ); + console.log( + `avg key down time ${ + keypressStats.duration.array.reduce( + (previous, current) => (current += previous) + ) / keypressStats.duration.array.length + } std(${stdDev(keypressStats.duration.array)})` + ); + wpmOverTimeChart.data.datasets[2].data = errorsNoZero; if (difficultyFailed) { @@ -1008,6 +1045,8 @@ function showResult(difficultyFailed = false) { blindMode: config.blindMode, theme: config.theme, tags: activeTags, + keySpacing: keypressStats.spacing.array, + keyDuration: keypressStats.duration.array, }; if ( config.difficulty == "normal" || @@ -1075,14 +1114,24 @@ function showResult(difficultyFailed = false) { ); wpmOverTimeChart.update({ duration: 0 }); } + $("#result .stats .leaderboards").removeClass("hidden"); + $("#result .stats .leaderboards .bottom").html("checking..."); testCompleted({ uid: firebase.auth().currentUser.uid, obj: completedEvent, }).then((e) => { accountIconLoading(false); - if (e.data === -1) { + console.log(e.data); + if (e.data.resultCode === -1) { showNotification("Could not save result", 3000); - } else if (e.data === 1 || e.data === 2) { + } else if (e.data.resultCode === -2) { + showNotification( + "Possible bot detected. Result not saved.", + 4000 + ); + } else if (e.data.resultCode === -999) { + showNotification("Internal error. Result not saved.", 4000); + } else if (e.data.resultCode === 1 || e.data.resultCode === 2) { dbSnapshot.results.unshift(completedEvent); try { firebase @@ -1091,7 +1140,93 @@ function showResult(difficultyFailed = false) { } catch (e) { console.log("Analytics unavailable"); } - if (e.data === 2) { + + //global + let globalLbString = ""; + if (e.data.globalLeaderboard === null) { + globalLbString = "global: not found"; + } else if (e.data.globalLeaderboard.insertedAt === -1) { + globalLbString = "global: not qualified"; + } else if (e.data.globalLeaderboard.insertedAt >= 0) { + if (e.data.globalLeaderboard.newBest) { + let pos = e.data.globalLeaderboard.insertedAt + 1; + let numend = "th"; + if (pos === 1) { + numend = "st"; + } else if (pos === 2) { + numend = "nd"; + } else if (pos === 3) { + numend = "rd"; + } + globalLbString = `global: ${pos}${numend} place`; + } else { + let pos = e.data.globalLeaderboard.foundAt + 1; + let numend = "th"; + if (pos === 1) { + numend = "st"; + } else if (pos === 2) { + numend = "nd"; + } else if (pos === 3) { + numend = "rd"; + } + globalLbString = `global: already ${pos}${numend}`; + } + } + + //daily + let dailyLbString = ""; + if (e.data.dailyLeaderboard === null) { + dailyLbString = "daily: not found"; + } else if (e.data.dailyLeaderboard.insertedAt === -1) { + dailyLbString = "daily: not qualified"; + } else if (e.data.dailyLeaderboard.insertedAt >= 0) { + if (e.data.dailyLeaderboard.newBest) { + let pos = e.data.dailyLeaderboard.insertedAt + 1; + let numend = "th"; + if (pos === 1) { + numend = "st"; + } else if (pos === 2) { + numend = "nd"; + } else if (pos === 3) { + numend = "rd"; + } + dailyLbString = `daily: ${pos}${numend} place`; + } else { + let pos = e.data.dailyLeaderboard.foundAt + 1; + let numend = "th"; + if (pos === 1) { + numend = "st"; + } else if (pos === 2) { + numend = "nd"; + } else if (pos === 3) { + numend = "rd"; + } + dailyLbString = `daily: already ${pos}${numend}`; + } + } + if ( + e.data.dailyLeaderboard === null && + e.data.globalLeaderboard === null && + e.data.lbBanned === false && + e.data.name !== false + ) { + $("#result .stats .leaderboards").addClass("hidden"); + } else { + $("#result .stats .leaderboards").removeClass("hidden"); + if (e.data.lbBanned) { + $("#result .stats .leaderboards .bottom").html("banned"); + } else if (e.data.name === false) { + $("#result .stats .leaderboards .bottom").html( + "update your name to access leaderboards" + ); + } else { + $("#result .stats .leaderboards .bottom").html( + globalLbString + "
" + dailyLbString + ); + } + } + + if (e.data.resultCode === 2) { //new pb if (!localPb) { showNotification( @@ -1142,6 +1277,7 @@ function showResult(difficultyFailed = false) { if (firebase.auth().currentUser != null) { $("#result .loginTip").addClass("hidden"); } else { + $("#result .stats .leaderboards").addClass("hidden"); $("#result .loginTip").removeClass("hidden"); } @@ -1260,6 +1396,16 @@ function restartTest(withSameWordset = false) { currentErrorCount = 0; currentTestLine = 0; activeWordJumped = false; + keypressStats = { + spacing: { + current: -1, + array: [], + }, + duration: { + current: -1, + array: [], + }, + }; hideTimer(); // restartTimer(); let el = null; @@ -1981,8 +2127,12 @@ $(document).on("click", "#top .config .mode .text-button", (e) => { $(document).on("click", "#top #menu .icon-button", (e) => { if ($(e.currentTarget).hasClass("discord")) return; - href = $(e.currentTarget).attr("href"); - changePage(href.replace("/", "")); + if ($(e.currentTarget).hasClass("leaderboards")) { + showLeaderboards(); + } else { + href = $(e.currentTarget).attr("href"); + changePage(href.replace("/", "")); + } }); $(window).on("popstate", (e) => { @@ -2110,6 +2260,16 @@ $(document).keypress(function (event) { updateActiveElement(); updateTimer(); clearIntervals(); + keypressStats = { + spacing: { + current: -1, + array: [], + }, + duration: { + current: -1, + array: [], + }, + }; timers.push( setInterval(function () { time++; @@ -2161,6 +2321,7 @@ $(document).keypress(function (event) { } else { accuracyStats.correct++; } + currentKeypressCount++; currentInput += event["key"]; $("#words .word.active").attr("input", currentInput); @@ -2175,10 +2336,29 @@ $(document).keypress(function (event) { updateCaretPosition(); }); +$(document).keydown((event) => { + keypressStats.duration.current = performance.now(); +}); + +$(document).keyup((event) => { + let now = performance.now(); + let diff = Math.abs(keypressStats.duration.current - now); + if (keypressStats.duration.current !== -1) { + keypressStats.duration.array.push(diff); + } + keypressStats.duration.current = now; +}); + //handle keyboard events $(document).keydown((event) => { - //tab + let now = performance.now(); + let diff = Math.abs(keypressStats.spacing.current - now); + if (keypressStats.spacing.current !== -1) { + keypressStats.spacing.array.push(diff); + } + keypressStats.spacing.current = now; + //tab if (event["keyCode"] == 9) { if (config.quickTab) { event.preventDefault();