==> ./monkeytype/README.md <== [![](https://github.com/Miodec/monkeytype/blob/master/static/images/githubbanner2.png?raw=true)](https://monkeytype.com/)
JavaScript HTML5 CSS3 CSS3
# About Monkeytype is a minimalistic and customizable typing test. It features many test modes, an account system to save your typing speed history, and user-configurable features like themes, sounds, a smooth caret, and more. # Features - minimalistic design with no ads - look at what you are typing - focus mode - different test modes - punctuation mode - themes - quotes - live wpm - smooth caret - account system - command line - and much more # Discord bot On the [Monkeytype Discord server](https://www.discord.gg/monkeytype), we added a Discord bot to auto-assign roles on our server. You can find its code over at https://github.com/Miodec/monkey-bot # Bug report or Feature request If you encounter a bug or have a feature request, [send me a message on Reddit](https://reddit.com/user/miodec), [create an issue](https://github.com/Miodec/monkeytype/issues), [create a discussion thread](https://github.com/Miodec/monkeytype/discussions), or [join the Discord server](https://www.discord.gg/monkeytype). # Want to Contribute? Refer to [CONTRIBUTING.md.](https://github.com/Miodec/monkeytype/blob/master/CONTRIBUTING.md) # Code of Conduct Before contributing to this repository, please read the [code of conduct.](https://github.com/Miodec/monkeytype/blob/master/CODE_OF_CONDUCT.md) # Credits [Montydrei](https://www.reddit.com/user/montydrei) for the name suggestion. Everyone who provided valuable feedback on the [original Reddit post](https://www.reddit.com/r/MechanicalKeyboards/comments/gc6wx3/experimenting_with_a_completely_new_type_of/) for the prototype of this website. All of the [contributors](https://github.com/Miodec/monkeytype/graphs/contributors) that have helped with implementing various features, adding themes, fixing bugs, and more. # Support If you wish to support further development and feel extra awesome, you can [donate](https://ko-fi.com/monkeytype), [become a Patron](https://www.patreon.com/monkeytype) or [buy a t-shirt](https://www.monkeytype.store/). ==> ./monkeytype/.npmrc <== engine-strict=true ==> ./monkeytype/backend/example.env <== DB_NAME=monkeytype DB_URI=mongodb://localhost:27017 MODE=dev # You can also use the format mongodb://username:password@host:port or # uncomment the following lines if you want to define them separately # DB_USERNAME= # DB_PASSWORD= # DB_AUTH_MECHANISM="SCRAM-SHA-256" # DB_AUTH_SOURCE=admin ==> ./monkeytype/backend/init/mongodb.js <== const { MongoClient } = require("mongodb"); let mongoClient; module.exports = { async connectDB() { let options = { useNewUrlParser: true, useUnifiedTopology: true, connectTimeoutMS: 2000, serverSelectionTimeoutMS: 2000, }; if (process.env.DB_USERNAME && process.env.DB_PASSWORD) { options.auth = { username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, }; } if (process.env.DB_AUTH_MECHANISM) { options.authMechanism = process.env.DB_AUTH_MECHANISM; } if (process.env.DB_AUTH_SOURCE) { options.authSource = process.env.DB_AUTH_SOURCE; } return MongoClient.connect(process.env.DB_URI, options) .then((client) => { mongoClient = client; }) .catch((e) => { console.error(e.message); console.error("FAILED TO CONNECT TO DATABASE. EXITING..."); process.exit(1); }); }, mongoDB() { return mongoClient.db(process.env.DB_NAME); }, }; ==> ./monkeytype/backend/server.js <== const express = require("express"); const { config } = require("dotenv"); const path = require("path"); const MonkeyError = require("./handlers/error"); config({ path: path.join(__dirname, ".env") }); const cors = require("cors"); const admin = require("firebase-admin"); const Logger = require("./handlers/logger.js"); const serviceAccount = require("./credentials/serviceAccountKey.json"); const { connectDB, mongoDB } = require("./init/mongodb"); const jobs = require("./jobs"); const addApiRoutes = require("./api/routes"); const PORT = process.env.PORT || 5005; // MIDDLEWARE & SETUP const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(cors()); app.set("trust proxy", 1); app.use((req, res, next) => { if (process.env.MAINTENANCE === "true") { res.status(503).json({ message: "Server is down for maintenance" }); } else { next(); } }); addApiRoutes(app); //DO NOT REMOVE NEXT, EVERYTHING WILL EXPLODE app.use(function (e, req, res, next) { if (/ECONNREFUSED.*27017/i.test(e.message)) { e.message = "Could not connect to the database. It may have crashed."; delete e.stack; } let monkeyError; if (e.errorID) { //its a monkey error monkeyError = e; } else { //its a server error monkeyError = new MonkeyError(e.status, e.message, e.stack); } if (!monkeyError.uid && req.decodedToken) { monkeyError.uid = req.decodedToken.uid; } if (process.env.MODE !== "dev" && monkeyError.status > 400) { Logger.log( "system_error", `${monkeyError.status} ${monkeyError.message}`, monkeyError.uid ); mongoDB().collection("errors").insertOne({ _id: monkeyError.errorID, timestamp: Date.now(), status: monkeyError.status, uid: monkeyError.uid, message: monkeyError.message, stack: monkeyError.stack, }); monkeyError.stack = undefined; } else { console.error(monkeyError.message); } return res.status(monkeyError.status || 500).json(monkeyError); }); console.log("Starting server..."); app.listen(PORT, async () => { console.log(`Listening on port ${PORT}`); console.log("Connecting to database..."); await connectDB(); console.log("Database connected"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); console.log("Starting cron jobs..."); jobs.forEach((job) => job.start()); }); ==> ./monkeytype/backend/constants/quoteLanguages.js <== const SUPPORTED_QUOTE_LANGUAGES = [ "albanian", "arabic", "code_c++", "code_c", "code_java", "code_javascript", "code_python", "code_rust", "czech", "danish", "dutch", "english", "filipino", "french", "german", "hindi", "icelandic", "indonesian", "irish", "italian", "lithuanian", "malagasy", "polish", "portuguese", "russian", "serbian", "slovak", "spanish", "swedish", "thai", "toki_pona", "turkish", "vietnamese", ]; module.exports = SUPPORTED_QUOTE_LANGUAGES; ==> ./monkeytype/backend/dao/leaderboards.js <== const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); const { ObjectID } = require("mongodb"); const Logger = require("../handlers/logger"); const { performance } = require("perf_hooks"); class LeaderboardsDAO { static async get(mode, mode2, language, skip, limit = 50) { if (limit > 50 || limit <= 0) limit = 50; if (skip < 0) skip = 0; const preset = await mongoDB() .collection(`leaderboards.${language}.${mode}.${mode2}`) .find() .sort({ rank: 1 }) .skip(parseInt(skip)) .limit(parseInt(limit)) .toArray(); return preset; } static async getRank(mode, mode2, language, uid) { const res = await mongoDB() .collection(`leaderboards.${language}.${mode}.${mode2}`) .findOne({ uid }); if (res) res.count = await mongoDB() .collection(`leaderboards.${language}.${mode}.${mode2}`) .estimatedDocumentCount(); return res; } static async update(mode, mode2, language, uid = undefined) { let str = `lbPersonalBests.${mode}.${mode2}.${language}`; let start1 = performance.now(); let lb = await mongoDB() .collection("users") .aggregate( [ { $match: { [str + ".wpm"]: { $exists: true, }, [str + ".acc"]: { $exists: true, }, [str + ".timestamp"]: { $exists: true, }, banned: { $exists: false }, }, }, { $set: { [str + ".uid"]: "$uid", [str + ".name"]: "$name", [str + ".discordId"]: "$discordId", }, }, { $replaceRoot: { newRoot: "$" + str, }, }, { $sort: { wpm: -1, acc: -1, timestamp: -1, }, }, ], { allowDiskUse: true } ) .toArray(); let end1 = performance.now(); let start2 = performance.now(); let retval = undefined; lb.forEach((lbEntry, index) => { lbEntry.rank = index + 1; if (uid && lbEntry.uid === uid) { retval = index + 1; } }); let end2 = performance.now(); let start3 = performance.now(); try { await mongoDB() .collection(`leaderboards.${language}.${mode}.${mode2}`) .drop(); } catch (e) {} if (lb && lb.length !== 0) await mongoDB() .collection(`leaderboards.${language}.${mode}.${mode2}`) .insertMany(lb); let end3 = performance.now(); let start4 = performance.now(); await mongoDB() .collection(`leaderboards.${language}.${mode}.${mode2}`) .createIndex({ uid: -1, }); await mongoDB() .collection(`leaderboards.${language}.${mode}.${mode2}`) .createIndex({ rank: 1, }); let end4 = performance.now(); let timeToRunAggregate = (end1 - start1) / 1000; let timeToRunLoop = (end2 - start2) / 1000; let timeToRunInsert = (end3 - start3) / 1000; let timeToRunIndex = (end4 - start4) / 1000; Logger.log( `system_lb_update_${language}_${mode}_${mode2}`, `Aggregate ${timeToRunAggregate}s, loop ${timeToRunLoop}s, insert ${timeToRunInsert}s, index ${timeToRunIndex}s`, uid ); if (retval) { return { message: "Successfully updated leaderboard", rank: retval, }; } else { return { message: "Successfully updated leaderboard", }; } } } module.exports = LeaderboardsDAO; ==> ./monkeytype/backend/dao/preset.js <== const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); const { ObjectID } = require("mongodb"); class PresetDAO { static async getPresets(uid) { const preset = await mongoDB() .collection("presets") .find({ uid }) .sort({ timestamp: -1 }) .toArray(); // this needs to be changed to later take patreon into consideration return preset; } static async addPreset(uid, name, config) { const count = await mongoDB().collection("presets").find({ uid }).count(); if (count >= 10) throw new MonkeyError(409, "Too many presets"); let preset = await mongoDB() .collection("presets") .insertOne({ uid, name, config }); return { insertedId: preset.insertedId, }; } static async editPreset(uid, _id, name, config) { console.log(_id); const preset = await mongoDB() .collection("presets") .findOne({ uid, _id: ObjectID(_id) }); if (!preset) throw new MonkeyError(404, "Preset not found"); if (config) { return await mongoDB() .collection("presets") .updateOne({ uid, _id: ObjectID(_id) }, { $set: { name, config } }); } else { return await mongoDB() .collection("presets") .updateOne({ uid, _id: ObjectID(_id) }, { $set: { name } }); } } static async removePreset(uid, _id) { const preset = await mongoDB() .collection("presets") .findOne({ uid, _id: ObjectID(_id) }); if (!preset) throw new MonkeyError(404, "Preset not found"); return await mongoDB() .collection("presets") .deleteOne({ uid, _id: ObjectID(_id) }); } } module.exports = PresetDAO; ==> ./monkeytype/backend/dao/quote-ratings.js <== const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); class QuoteRatingsDAO { static async submit(quoteId, language, rating, update) { if (update) { await mongoDB() .collection("quote-rating") .updateOne( { quoteId, language }, { $inc: { totalRating: rating } }, { upsert: true } ); } else { await mongoDB() .collection("quote-rating") .updateOne( { quoteId, language }, { $inc: { ratings: 1, totalRating: rating } }, { upsert: true } ); } let quoteRating = await this.get(quoteId, language); let average = parseFloat( ( Math.round((quoteRating.totalRating / quoteRating.ratings) * 10) / 10 ).toFixed(1) ); return await mongoDB() .collection("quote-rating") .updateOne({ quoteId, language }, { $set: { average } }); } static async get(quoteId, language) { return await mongoDB() .collection("quote-rating") .findOne({ quoteId, language }); } } module.exports = QuoteRatingsDAO; ==> ./monkeytype/backend/dao/user.js <== const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); const { ObjectID } = require("mongodb"); const { checkAndUpdatePb } = require("../handlers/pb"); const { updateAuthEmail } = require("../handlers/auth"); const { isUsernameValid } = require("../handlers/validation"); class UsersDAO { static async addUser(name, email, uid) { const user = await mongoDB().collection("users").findOne({ uid }); if (user) throw new MonkeyError(400, "User document already exists", "addUser"); return await mongoDB() .collection("users") .insertOne({ name, email, uid, addedAt: Date.now() }); } static async deleteUser(uid) { return await mongoDB().collection("users").deleteOne({ uid }); } static async updateName(uid, name) { const nameDoc = await mongoDB() .collection("users") .findOne({ name: { $regex: new RegExp(`^${name}$`, "i") } }); if (nameDoc) throw new MonkeyError(409, "Username already taken"); let user = await mongoDB().collection("users").findOne({ uid }); if ( Date.now() - user.lastNameChange < 2592000000 && isUsernameValid(user.name) ) { throw new MonkeyError(409, "You can change your name once every 30 days"); } return await mongoDB() .collection("users") .updateOne({ uid }, { $set: { name, lastNameChange: Date.now() } }); } static async clearPb(uid) { return await mongoDB() .collection("users") .updateOne({ uid }, { $set: { personalBests: {}, lbPersonalBests: {} } }); } static async isNameAvailable(name) { const nameDoc = await mongoDB().collection("users").findOne({ name }); if (nameDoc) { return false; } else { return true; } } static async updateQuoteRatings(uid, quoteRatings) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "updateQuoteRatings"); await mongoDB() .collection("users") .updateOne({ uid }, { $set: { quoteRatings } }); return true; } static async updateEmail(uid, email) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "update email"); await updateAuthEmail(uid, email); await mongoDB().collection("users").updateOne({ uid }, { $set: { email } }); return true; } static async getUser(uid) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "get user"); return user; } static async getUserByDiscordId(discordId) { const user = await mongoDB().collection("users").findOne({ discordId }); if (!user) throw new MonkeyError(404, "User not found", "get user by discord id"); return user; } static async addTag(uid, name) { let _id = ObjectID(); await mongoDB() .collection("users") .updateOne({ uid }, { $push: { tags: { _id, name } } }); return { _id, name, }; } static async getTags(uid) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "get tags"); return user.tags; } static async editTag(uid, _id, name) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "edit tag"); if ( user.tags === undefined || user.tags.filter((t) => t._id == _id).length === 0 ) throw new MonkeyError(404, "Tag not found"); return await mongoDB() .collection("users") .updateOne( { uid: uid, "tags._id": ObjectID(_id), }, { $set: { "tags.$.name": name } } ); } static async removeTag(uid, _id) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "remove tag"); if ( user.tags === undefined || user.tags.filter((t) => t._id == _id).length === 0 ) throw new MonkeyError(404, "Tag not found"); return await mongoDB() .collection("users") .updateOne( { uid: uid, "tags._id": ObjectID(_id), }, { $pull: { tags: { _id: ObjectID(_id) } } } ); } static async removeTagPb(uid, _id) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "remove tag pb"); if ( user.tags === undefined || user.tags.filter((t) => t._id == _id).length === 0 ) throw new MonkeyError(404, "Tag not found"); return await mongoDB() .collection("users") .updateOne( { uid: uid, "tags._id": ObjectID(_id), }, { $set: { "tags.$.personalBests": {} } } ); } static async updateLbMemory(uid, mode, mode2, language, rank) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "update lb memory"); if (user.lbMemory === undefined) user.lbMemory = {}; if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {}; if (user.lbMemory[mode][mode2] === undefined) user.lbMemory[mode][mode2] = {}; user.lbMemory[mode][mode2][language] = rank; return await mongoDB() .collection("users") .updateOne( { uid }, { $set: { lbMemory: user.lbMemory }, } ); } static async checkIfPb(uid, result) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "check if pb"); const { mode, mode2, acc, consistency, difficulty, lazyMode, language, punctuation, rawWpm, wpm, funbox, } = result; if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { return false; } if (mode === "quote") { return false; } let lbpb = user.lbPersonalBests; if (!lbpb) lbpb = {}; let pb = checkAndUpdatePb( user.personalBests, lbpb, mode, mode2, acc, consistency, difficulty, lazyMode, language, punctuation, rawWpm, wpm ); if (pb.isPb) { await mongoDB() .collection("users") .updateOne({ uid }, { $set: { personalBests: pb.obj } }); if (pb.lbObj) { await mongoDB() .collection("users") .updateOne({ uid }, { $set: { lbPersonalBests: pb.lbObj } }); } return true; } else { return false; } } static async checkIfTagPb(uid, result) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "check if tag pb"); if (user.tags === undefined || user.tags.length === 0) { return []; } const { mode, mode2, acc, consistency, difficulty, lazyMode, language, punctuation, rawWpm, wpm, tags, funbox, } = result; if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { return []; } if (mode === "quote") { return []; } let tagsToCheck = []; user.tags.forEach((tag) => { tags.forEach((resultTag) => { if (resultTag == tag._id) { tagsToCheck.push(tag); } }); }); let ret = []; tagsToCheck.forEach(async (tag) => { let tagpb = checkAndUpdatePb( tag.personalBests, undefined, mode, mode2, acc, consistency, difficulty, lazyMode, language, punctuation, rawWpm, wpm ); if (tagpb.isPb) { ret.push(tag._id); await mongoDB() .collection("users") .updateOne( { uid, "tags._id": ObjectID(tag._id) }, { $set: { "tags.$.personalBests": tagpb.obj } } ); } }); return ret; } static async resetPb(uid) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "reset pb"); return await mongoDB() .collection("users") .updateOne({ uid }, { $set: { personalBests: {} } }); } static async updateTypingStats(uid, restartCount, timeTyping) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "update typing stats"); return await mongoDB() .collection("users") .updateOne( { uid }, { $inc: { startedTests: restartCount + 1, completedTests: 1, timeTyping, }, } ); } static async linkDiscord(uid, discordId) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "link discord"); return await mongoDB() .collection("users") .updateOne({ uid }, { $set: { discordId } }); } static async unlinkDiscord(uid) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "unlink discord"); return await mongoDB() .collection("users") .updateOne({ uid }, { $set: { discordId: null } }); } static async incrementBananas(uid, wpm) { const user = await mongoDB().collection("users").findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", "increment bananas"); let best60; try { best60 = Math.max(...user.personalBests.time[60].map((best) => best.wpm)); } catch (e) { best60 = undefined; } if (best60 === undefined || wpm >= best60 - best60 * 0.25) { //increment when no record found or wpm is within 25% of the record return await mongoDB() .collection("users") .updateOne({ uid }, { $inc: { bananas: 1 } }); } else { return null; } } } module.exports = UsersDAO; ==> ./monkeytype/backend/dao/config.js <== const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); class ConfigDAO { static async saveConfig(uid, config) { return await mongoDB() .collection("configs") .updateOne({ uid }, { $set: { config } }, { upsert: true }); } static async getConfig(uid) { let config = await mongoDB().collection("configs").findOne({ uid }); // if (!config) throw new MonkeyError(404, "Config not found"); return config; } } module.exports = ConfigDAO; ==> ./monkeytype/backend/dao/new-quotes.js <== const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); const fs = require("fs"); const simpleGit = require("simple-git"); const path = require("path"); let git; try { git = simpleGit(path.join(__dirname, "../../../monkeytype-new-quotes")); } catch (e) { git = undefined; } const stringSimilarity = require("string-similarity"); const { ObjectID } = require("mongodb"); class NewQuotesDAO { static async add(text, source, language, uid) { if (!git) throw new MonkeyError(500, "Git not available."); let quote = { text: text, source: source, language: language.toLowerCase(), submittedBy: uid, timestamp: Date.now(), approved: false, }; //check for duplicate first const fileDir = path.join( __dirname, `../../../monkeytype-new-quotes/static/quotes/${language}.json` ); let duplicateId = -1; let similarityScore = -1; if (fs.existsSync(fileDir)) { // let quoteFile = fs.readFileSync(fileDir); // quoteFile = JSON.parse(quoteFile.toString()); // quoteFile.quotes.every((old) => { // if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.9) { // duplicateId = old.id; // similarityScore = stringSimilarity.compareTwoStrings( // old.text, // quote.text // ); // return false; // } // return true; // }); } else { return { languageError: 1 }; } if (duplicateId != -1) { return { duplicateId, similarityScore }; } return await mongoDB().collection("new-quotes").insertOne(quote); } static async get() { if (!git) throw new MonkeyError(500, "Git not available."); return await mongoDB() .collection("new-quotes") .find({ approved: false }) .sort({ timestamp: 1 }) .limit(10) .toArray(); } static async approve(quoteId, editQuote, editSource) { if (!git) throw new MonkeyError(500, "Git not available."); //check mod status let quote = await mongoDB() .collection("new-quotes") .findOne({ _id: ObjectID(quoteId) }); if (!quote) { throw new MonkeyError(404, "Quote not found"); } let language = quote.language; quote = { text: editQuote ? editQuote : quote.text, source: editSource ? editSource : quote.source, length: quote.text.length, }; let message = ""; const fileDir = path.join( __dirname, `../../../monkeytype-new-quotes/static/quotes/${language}.json` ); await git.pull("upstream", "master"); if (fs.existsSync(fileDir)) { let quoteFile = fs.readFileSync(fileDir); quoteFile = JSON.parse(quoteFile.toString()); quoteFile.quotes.every((old) => { if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.8) { throw new MonkeyError(409, "Duplicate quote"); } }); let maxid = 0; quoteFile.quotes.map(function (q) { if (q.id > maxid) { maxid = q.id; } }); quote.id = maxid + 1; quoteFile.quotes.push(quote); fs.writeFileSync(fileDir, JSON.stringify(quoteFile, null, 2)); message = `Added quote to ${language}.json.`; } else { //file doesnt exist, create it quote.id = 1; fs.writeFileSync( fileDir, JSON.stringify({ language: language, groups: [ [0, 100], [101, 300], [301, 600], [601, 9999], ], quotes: [quote], }) ); message = `Created file ${language}.json and added quote.`; } await git.add([`static/quotes/${language}.json`]); await git.commit(`Added quote to ${language}.json`); await git.push("origin", "master"); await mongoDB() .collection("new-quotes") .deleteOne({ _id: ObjectID(quoteId) }); return { quote, message }; } static async refuse(quoteId) { if (!git) throw new MonkeyError(500, "Git not available."); return await mongoDB() .collection("new-quotes") .deleteOne({ _id: ObjectID(quoteId) }); } } module.exports = NewQuotesDAO; ==> ./monkeytype/backend/dao/public-stats.js <== // const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); const { roundTo2 } = require("../handlers/misc"); class PublicStatsDAO { //needs to be rewritten, this is public stats not user stats static async updateStats(restartCount, time) { time = roundTo2(time); await mongoDB() .collection("public") .updateOne( { type: "stats" }, { $inc: { testsCompleted: 1, testsStarted: restartCount + 1, timeTyping: time, }, }, { upsert: true } ); return true; } } module.exports = PublicStatsDAO; ==> ./monkeytype/backend/dao/bot.js <== const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); async function addCommand(command, arguments) { return await mongoDB().collection("bot-commands").insertOne({ command, arguments, executed: false, requestTimestamp: Date.now(), }); } async function addCommands(commands, arguments) { if (commands.length === 0 || commands.length !== arguments.length) { return []; } const normalizedCommands = commands.map((command, index) => { return { command, arguments: arguments[index], executed: false, requestTimestamp: Date.now(), }; }); return await mongoDB() .collection("bot-commands") .insertMany(normalizedCommands); } class BotDAO { static async updateDiscordRole(discordId, wpm) { return await addCommand("updateRole", [discordId, wpm]); } static async linkDiscord(uid, discordId) { return await addCommand("linkDiscord", [discordId, uid]); } static async unlinkDiscord(uid, discordId) { return await addCommand("unlinkDiscord", [discordId, uid]); } static async awardChallenge(discordId, challengeName) { return await addCommand("awardChallenge", [discordId, challengeName]); } static async announceLbUpdate(newRecords, leaderboardId) { if (newRecords.length === 0) { return []; } const leaderboardCommands = Array(newRecords.length).fill("sayLbUpdate"); const leaderboardCommandsArguments = newRecords.map((newRecord) => { return [ newRecord.discordId ?? newRecord.name, newRecord.rank, leaderboardId, newRecord.wpm, newRecord.raw, newRecord.acc, newRecord.consistency, ]; }); return await addCommands(leaderboardCommands, leaderboardCommandsArguments); } } module.exports = BotDAO; ==> ./monkeytype/backend/dao/psa.js <== const { mongoDB } = require("../init/mongodb"); class PsaDAO { static async get(uid, config) { return await mongoDB().collection("psa").find().toArray(); } } module.exports = PsaDAO; ==> ./monkeytype/backend/dao/result.js <== const { ObjectID } = require("mongodb"); const MonkeyError = require("../handlers/error"); const { mongoDB } = require("../init/mongodb"); const UserDAO = require("./user"); class ResultDAO { static async addResult(uid, result) { let user; try { user = await UserDAO.getUser(uid); } catch (e) { user = null; } if (!user) throw new MonkeyError(404, "User not found", "add result"); if (result.uid === undefined) result.uid = uid; // result.ir = true; let res = await mongoDB().collection("results").insertOne(result); return { insertedId: res.insertedId, }; } static async deleteAll(uid) { return await mongoDB().collection("results").deleteMany({ uid }); } static async updateTags(uid, resultid, tags) { const result = await mongoDB() .collection("results") .findOne({ _id: ObjectID(resultid), uid }); if (!result) throw new MonkeyError(404, "Result not found"); const userTags = await UserDAO.getTags(uid); const userTagIds = userTags.map((tag) => tag._id.toString()); let validTags = true; tags.forEach((tagId) => { if (!userTagIds.includes(tagId)) validTags = false; }); if (!validTags) throw new MonkeyError(400, "One of the tag id's is not vaild"); return await mongoDB() .collection("results") .updateOne({ _id: ObjectID(resultid), uid }, { $set: { tags } }); } static async getResult(uid, id) { const result = await mongoDB() .collection("results") .findOne({ _id: ObjectID(id), uid }); if (!result) throw new MonkeyError(404, "Result not found"); return result; } static async getLastResult(uid) { let result = await mongoDB() .collection("results") .find({ uid }) .sort({ timestamp: -1 }) .limit(1) .toArray(); result = result[0]; if (!result) throw new MonkeyError(404, "No results found"); return result; } static async getResultByTimestamp(uid, timestamp) { return await mongoDB().collection("results").findOne({ uid, timestamp }); } static async getResults(uid, start, end) { start = start ?? 0; end = end ?? 1000; const result = await mongoDB() .collection("results") .find({ uid }) .sort({ timestamp: -1 }) .skip(start) .limit(end) .toArray(); // this needs to be changed to later take patreon into consideration if (!result) throw new MonkeyError(404, "Result not found"); return result; } } module.exports = ResultDAO; ==> ./monkeytype/backend/middlewares/auth.js <== const MonkeyError = require("../handlers/error"); const { verifyIdToken } = require("../handlers/auth"); module.exports = { async authenticateRequest(req, res, next) { try { if (process.env.MODE === "dev" && !req.headers.authorization) { if (req.body.uid) { req.decodedToken = { uid: req.body.uid, }; console.log("Running authorization in dev mode"); return next(); } else { throw new MonkeyError( 400, "Running authorization in dev mode but still no uid was provided" ); } } const { authorization } = req.headers; if (!authorization) throw new MonkeyError( 401, "Unauthorized", `endpoint: ${req.baseUrl} no authorization header found` ); const token = authorization.split(" "); if (token[0].trim() !== "Bearer") return next( new MonkeyError(400, "Invalid Token", "Incorrect token type") ); req.decodedToken = await verifyIdToken(token[1]); return next(); } catch (e) { return next(e); } }, }; ==> ./monkeytype/backend/middlewares/rate-limit.js <== const rateLimit = require("express-rate-limit"); const getAddress = (req) => req.headers["cf-connecting-ip"] || req.headers["x-forwarded-for"] || req.ip || "255.255.255.255"; const message = "Too many requests, please try again later"; const multiplier = process.env.MODE === "dev" ? 100 : 1; // Config Routing exports.configUpdate = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 500 * multiplier, message, keyGenerator: getAddress, }); exports.configGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 120 * multiplier, message, keyGenerator: getAddress, }); // Leaderboards Routing exports.leaderboardsGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); // New Quotes Routing exports.newQuotesGet = rateLimit({ windowMs: 60 * 60 * 1000, max: 500 * multiplier, message, keyGenerator: getAddress, }); exports.newQuotesAdd = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.newQuotesAction = rateLimit({ windowMs: 60 * 60 * 1000, max: 500 * multiplier, message, keyGenerator: getAddress, }); // Quote Ratings Routing exports.quoteRatingsGet = rateLimit({ windowMs: 60 * 60 * 1000, max: 500 * multiplier, message, keyGenerator: getAddress, }); exports.quoteRatingsSubmit = rateLimit({ windowMs: 60 * 60 * 1000, max: 500 * multiplier, message, keyGenerator: getAddress, }); // Quote reporting exports.quoteReportSubmit = rateLimit({ windowMs: 30 * 60 * 1000, // 30 min max: 50 * multiplier, message, keyGenerator: getAddress, }); // Presets Routing exports.presetsGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.presetsAdd = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.presetsRemove = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.presetsEdit = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); // PSA (Public Service Announcement) Routing exports.psaGet = rateLimit({ windowMs: 60 * 1000, max: 60 * multiplier, message, keyGenerator: getAddress, }); // Results Routing exports.resultsGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.resultsAdd = rateLimit({ windowMs: 60 * 60 * 1000, max: 500 * multiplier, message, keyGenerator: getAddress, }); exports.resultsTagsUpdate = rateLimit({ windowMs: 60 * 60 * 1000, max: 30 * multiplier, message, keyGenerator: getAddress, }); exports.resultsDeleteAll = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 10 * multiplier, message, keyGenerator: getAddress, }); exports.resultsLeaderboardGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.resultsLeaderboardQualificationGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); // Users Routing exports.userGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.userSignup = rateLimit({ windowMs: 24 * 60 * 60 * 1000, // 1 day max: 3 * multiplier, message, keyGenerator: getAddress, }); exports.userDelete = rateLimit({ windowMs: 24 * 60 * 60 * 1000, // 1 day max: 3 * multiplier, message, keyGenerator: getAddress, }); exports.userCheckName = rateLimit({ windowMs: 60 * 1000, max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.userUpdateName = rateLimit({ windowMs: 24 * 60 * 60 * 1000, // 1 day max: 3 * multiplier, message, keyGenerator: getAddress, }); exports.userUpdateLBMemory = rateLimit({ windowMs: 60 * 1000, max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.userUpdateEmail = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.userClearPB = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.userTagsGet = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.userTagsRemove = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 30 * multiplier, message, keyGenerator: getAddress, }); exports.userTagsClearPB = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 60 * multiplier, message, keyGenerator: getAddress, }); exports.userTagsEdit = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 30 * multiplier, message, keyGenerator: getAddress, }); exports.userTagsAdd = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 30 * multiplier, message, keyGenerator: getAddress, }); exports.userDiscordLink = exports.usersTagsEdit = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 15 * multiplier, message, keyGenerator: getAddress, }); exports.userDiscordUnlink = exports.usersTagsEdit = rateLimit({ windowMs: 60 * 60 * 1000, // 60 min max: 15 * multiplier, message, keyGenerator: getAddress, }); ==> ./monkeytype/backend/middlewares/apiUtils.js <== const joi = require("joi"); const MonkeyError = require("../handlers/error"); function requestValidation(validationSchema) { return (req, res, next) => { // In dev environments, as an alternative to token authentication, // you can pass the authentication middleware by having a user id in the body. // Inject the user id into the schema so that validation will not fail. if (process.env.MODE === "dev") { validationSchema.body = { uid: joi.any(), ...(validationSchema.body ?? {}), }; } Object.keys(validationSchema).forEach((key) => { const schema = validationSchema[key]; const joiSchema = joi.object().keys(schema); const { error } = joiSchema.validate(req[key] ?? {}); if (error) { const errorMessage = error.details[0].message; throw new MonkeyError(400, `Invalid request: ${errorMessage}`); } }); next(); }; } module.exports = { requestValidation, }; ==> ./monkeytype/backend/.gitignore <== lastId.txt log_success.txt log_failed.txt ==> ./monkeytype/backend/api/controllers/leaderboards.js <== const LeaderboardsDAO = require("../../dao/leaderboards"); const ResultDAO = require("../../dao/result"); const UserDAO = require("../../dao/user"); const admin = require("firebase-admin"); const { verifyIdToken } = require("../../handlers/auth"); class LeaderboardsController { static async get(req, res, next) { try { const { language, mode, mode2, skip, limit } = req.query; let uid; const { authorization } = req.headers; if (authorization) { const token = authorization.split(" "); if (token[0].trim() == "Bearer") req.decodedToken = await verifyIdToken(token[1]); uid = req.decodedToken.uid; } if (!language || !mode || !mode2 || !skip) { return res.status(400).json({ message: "Missing parameters", }); } let retval = await LeaderboardsDAO.get( mode, mode2, language, skip, limit ); retval.forEach((item) => { if (uid && item.uid == uid) { // } else { delete item.discordId; delete item.uid; delete item.difficulty; delete item.language; } }); return res.status(200).json(retval); } catch (e) { return next(e); } } static async getRank(req, res, next) { try { const { language, mode, mode2 } = req.query; const { uid } = req.decodedToken; if (!language || !mode || !mode2 || !uid) { return res.status(400).json({ message: "Missing parameters", }); } let retval = await LeaderboardsDAO.getRank(mode, mode2, language, uid); return res.status(200).json(retval); } catch (e) { return next(e); } } static async update(req, res, next) { try { return res.status(200).json({ message: "Leaderboards disabled", lbdisabled: true, }); if (process.env.LBDISABLED === true) { return res.status(200).json({ message: "Leaderboards disabled", lbdisabled: true, }); } const { rid } = req.body; const { uid } = req.decodedToken; if (!rid) { return res.status(400).json({ message: "Missing parameters", }); } //verify user first let user = await UserDAO.getUser(uid); if (!user) { return res.status(400).json({ message: "User not found", }); } if (user.banned === true) { return res.status(200).json({ message: "User banned", banned: true, }); } let userauth = await admin.auth().getUser(uid); if (!userauth.emailVerified) { return res.status(200).json({ message: "User needs to verify email address", needsToVerifyEmail: true, }); } let result = await ResultDAO.getResult(uid, rid); if (!result.language) result.language = "english"; if ( result.mode == "time" && result.isPb && (result.mode2 == 15 || result.mode2 == 60) && ["english"].includes(result.language) ) { //check if its better than their current lb pb let lbpb = user?.lbPersonalBests?.[result.mode]?.[result.mode2]?.[ result.language ]?.wpm; if (!lbpb) lbpb = 0; if (result.wpm >= lbpb) { //run update let retval = await LeaderboardsDAO.update( result.mode, result.mode2, result.language, uid ); if (retval.rank) { await UserDAO.updateLbMemory( uid, result.mode, result.mode2, result.language, retval.rank ); } return res.status(200).json(retval); } else { let rank = await LeaderboardsDAO.getRank( result.mode, result.mode2, result.language, uid ); rank = rank?.rank; if (!rank) { return res.status(400).json({ message: "User has a lbPb but was not found on the leaderboard", }); } await UserDAO.updateLbMemory( uid, result.mode, result.mode2, result.language, rank ); return res.status(200).json({ message: "Not a new leaderboard personal best", rank, }); } } else { return res.status(400).json({ message: "This result is not eligible for any leaderboard", }); } } catch (e) { return next(e); } } static async debugUpdate(req, res, next) { try { const { language, mode, mode2 } = req.body; if (!language || !mode || !mode2) { return res.status(400).json({ message: "Missing parameters", }); } let retval = await LeaderboardsDAO.update(mode, mode2, language); return res.status(200).json(retval); } catch (e) { return next(e); } } } module.exports = LeaderboardsController; ==> ./monkeytype/backend/api/controllers/preset.js <== const PresetDAO = require("../../dao/preset"); const { isTagPresetNameValid, validateConfig, } = require("../../handlers/validation"); const MonkeyError = require("../../handlers/error"); class PresetController { static async getPresets(req, res, next) { try { const { uid } = req.decodedToken; let presets = await PresetDAO.getPresets(uid); return res.status(200).json(presets); } catch (e) { return next(e); } } static async addPreset(req, res, next) { try { const { name, config } = req.body; const { uid } = req.decodedToken; if (!isTagPresetNameValid(name)) throw new MonkeyError(400, "Invalid preset name."); validateConfig(config); let preset = await PresetDAO.addPreset(uid, name, config); return res.status(200).json(preset); } catch (e) { return next(e); } } static async editPreset(req, res, next) { try { const { _id, name, config } = req.body; const { uid } = req.decodedToken; if (!isTagPresetNameValid(name)) throw new MonkeyError(400, "Invalid preset name."); if (config) validateConfig(config); await PresetDAO.editPreset(uid, _id, name, config); return res.sendStatus(200); } catch (e) { return next(e); } } static async removePreset(req, res, next) { try { const { _id } = req.body; const { uid } = req.decodedToken; await PresetDAO.removePreset(uid, _id); return res.sendStatus(200); } catch (e) { return next(e); } } } module.exports = PresetController; ==> ./monkeytype/backend/api/controllers/core.js <== class CoreController { static async handleTestResult() {} } ==> ./monkeytype/backend/api/controllers/quote-ratings.js <== const QuoteRatingsDAO = require("../../dao/quote-ratings"); const UserDAO = require("../../dao/user"); const MonkeyError = require("../../handlers/error"); class QuoteRatingsController { static async getRating(req, res, next) { try { const { quoteId, language } = req.query; let data = await QuoteRatingsDAO.get(parseInt(quoteId), language); return res.status(200).json(data); } catch (e) { return next(e); } } static async submitRating(req, res, next) { try { let { uid } = req.decodedToken; let { quoteId, rating, language } = req.body; quoteId = parseInt(quoteId); rating = parseInt(rating); if (isNaN(quoteId) || isNaN(rating)) { throw new MonkeyError( 400, "Bad request. Quote id or rating is not a number." ); } if (typeof language !== "string") { throw new MonkeyError(400, "Bad request. Language is not a string."); } if (rating < 1 || rating > 5) { throw new MonkeyError( 400, "Bad request. Rating must be between 1 and 5." ); } rating = Math.round(rating); //check if user already submitted a rating let user = await UserDAO.getUser(uid); if (!user) { throw new MonkeyError(401, "User not found."); } let quoteRatings = user.quoteRatings; if (quoteRatings === undefined) quoteRatings = {}; if (quoteRatings[language] === undefined) quoteRatings[language] = {}; if (quoteRatings[language][quoteId] == undefined) quoteRatings[language][quoteId] = undefined; let quoteRating = quoteRatings[language][quoteId]; let newRating; let update; if (quoteRating) { //user already voted for this newRating = rating - quoteRating; update = true; } else { //user has not voted for this newRating = rating; update = false; } await QuoteRatingsDAO.submit(quoteId, language, newRating, update); quoteRatings[language][quoteId] = rating; await UserDAO.updateQuoteRatings(uid, quoteRatings); return res.sendStatus(200); } catch (e) { return next(e); } } } module.exports = QuoteRatingsController; ==> ./monkeytype/backend/api/controllers/user.js <== const UsersDAO = require("../../dao/user"); const BotDAO = require("../../dao/bot"); const { isUsernameValid, isTagPresetNameValid, } = require("../../handlers/validation"); const MonkeyError = require("../../handlers/error"); const fetch = require("node-fetch"); const Logger = require("./../../handlers/logger.js"); const uaparser = require("ua-parser-js"); // import UsersDAO from "../../dao/user"; // import BotDAO from "../../dao/bot"; // import { isUsernameValid } from "../../handlers/validation"; class UserController { static async createNewUser(req, res, next) { try { const { name } = req.body; const { email, uid } = req.decodedToken; await UsersDAO.addUser(name, email, uid); Logger.log("user_created", `${name} ${email}`, uid); return res.sendStatus(200); } catch (e) { return next(e); } } static async deleteUser(req, res, next) { try { const { uid } = req.decodedToken; const userInfo = await UsersDAO.getUser(uid); await UsersDAO.deleteUser(uid); Logger.log("user_deleted", `${userInfo.email} ${userInfo.name}`, uid); return res.sendStatus(200); } catch (e) { return next(e); } } static async updateName(req, res, next) { try { const { uid } = req.decodedToken; const { name } = req.body; if (!isUsernameValid(name)) return res.status(400).json({ message: "Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -", }); let olduser = await UsersDAO.getUser(uid); await UsersDAO.updateName(uid, name); Logger.log( "user_name_updated", `changed name from ${olduser.name} to ${name}`, uid ); return res.sendStatus(200); } catch (e) { return next(e); } } static async clearPb(req, res, next) { try { const { uid } = req.decodedToken; await UsersDAO.clearPb(uid); Logger.log("user_cleared_pbs", "", uid); return res.sendStatus(200); } catch (e) { return next(e); } } static async checkName(req, res, next) { try { const { name } = req.body; if (!isUsernameValid(name)) return next({ status: 400, message: "Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -", }); const available = await UsersDAO.isNameAvailable(name); if (!available) return res.status(400).json({ message: "Username unavailable" }); return res.sendStatus(200); } catch (e) { return next(e); } } static async updateEmail(req, res, next) { try { const { uid } = req.decodedToken; const { newEmail } = req.body; try { await UsersDAO.updateEmail(uid, newEmail); } catch (e) { throw new MonkeyError(400, e.message, "update email", uid); } Logger.log("user_email_updated", `changed email to ${newEmail}`, uid); return res.sendStatus(200); } catch (e) { return next(e); } } static async getUser(req, res, next) { try { const { email, uid } = req.decodedToken; let userInfo; try { userInfo = await UsersDAO.getUser(uid); } catch (e) { if (email && uid) { userInfo = await UsersDAO.addUser(undefined, email, uid); } else { throw new MonkeyError( 400, "User not found. Could not recreate user document.", "Tried to recreate user document but either email or uid is nullish", uid ); } } let agent = uaparser(req.headers["user-agent"]); let logobj = { ip: req.headers["cf-connecting-ip"] || req.headers["x-forwarded-for"] || req.ip || "255.255.255.255", agent: agent.os.name + " " + agent.os.version + " " + agent.browser.name + " " + agent.browser.version, }; if (agent.device.vendor) { logobj.device = agent.device.vendor + " " + agent.device.model + " " + agent.device.type; } Logger.log("user_data_requested", logobj, uid); return res.status(200).json(userInfo); } catch (e) { return next(e); } } static async linkDiscord(req, res, next) { try { const { uid } = req.decodedToken; let requser; try { requser = await UsersDAO.getUser(uid); } catch (e) { requser = null; } if (requser?.banned === true) { throw new MonkeyError(403, "Banned accounts cannot link with Discord"); } let discordFetch = await fetch("https://discord.com/api/users/@me", { headers: { authorization: `${req.body.data.tokenType} ${req.body.data.accessToken}`, }, }); discordFetch = await discordFetch.json(); const did = discordFetch.id; if (!did) { throw new MonkeyError( 500, "Could not get Discord account info", "did is undefined" ); } let user; try { user = await UsersDAO.getUserByDiscordId(did); } catch (e) { user = null; } if (user !== null) { throw new MonkeyError( 400, "This Discord account is already linked to a different account" ); } await UsersDAO.linkDiscord(uid, did); await BotDAO.linkDiscord(uid, did); Logger.log("user_discord_link", `linked to ${did}`, uid); return res.status(200).json({ message: "Discord account linked", did, }); } catch (e) { return next(e); } } static async unlinkDiscord(req, res, next) { try { const { uid } = req.decodedToken; let userInfo; try { userInfo = await UsersDAO.getUser(uid); } catch (e) { throw new MonkeyError(400, "User not found."); } if (!userInfo.discordId) { throw new MonkeyError( 400, "User does not have a linked Discord account" ); } await BotDAO.unlinkDiscord(uid, userInfo.discordId); await UsersDAO.unlinkDiscord(uid); Logger.log("user_discord_unlinked", userInfo.discordId, uid); return res.status(200).send(); } catch (e) { return next(e); } } static async addTag(req, res, next) { try { const { uid } = req.decodedToken; const { tagName } = req.body; if (!isTagPresetNameValid(tagName)) return res.status(400).json({ message: "Tag name invalid. Name cannot contain special characters or more than 16 characters. Can include _ . and -", }); let tag = await UsersDAO.addTag(uid, tagName); return res.status(200).json(tag); } catch (e) { return next(e); } } static async clearTagPb(req, res, next) { try { const { uid } = req.decodedToken; const { tagid } = req.body; await UsersDAO.removeTagPb(uid, tagid); return res.sendStatus(200); } catch (e) { return next(e); } } static async editTag(req, res, next) { try { const { uid } = req.decodedToken; const { tagid, newname } = req.body; if (!isTagPresetNameValid(newname)) return res.status(400).json({ message: "Tag name invalid. Name cannot contain special characters or more than 16 characters. Can include _ . and -", }); await UsersDAO.editTag(uid, tagid, newname); return res.sendStatus(200); } catch (e) { return next(e); } } static async removeTag(req, res, next) { try { const { uid } = req.decodedToken; const { tagid } = req.body; await UsersDAO.removeTag(uid, tagid); return res.sendStatus(200); } catch (e) { return next(e); } } static async getTags(req, res, next) { try { const { uid } = req.decodedToken; let tags = await UsersDAO.getTags(uid); if (tags == undefined) tags = []; return res.status(200).json(tags); } catch (e) { return next(e); } } static async updateLbMemory(req, res, next) { try { const { uid } = req.decodedToken; const { mode, mode2, language, rank } = req.body; await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank); return res.sendStatus(200); } catch (e) { return next(e); } } } module.exports = UserController; ==> ./monkeytype/backend/api/controllers/config.js <== const ConfigDAO = require("../../dao/config"); const { validateConfig } = require("../../handlers/validation"); class ConfigController { static async getConfig(req, res, next) { try { const { uid } = req.decodedToken; let config = await ConfigDAO.getConfig(uid); return res.status(200).json(config); } catch (e) { return next(e); } } static async saveConfig(req, res, next) { try { const { config } = req.body; const { uid } = req.decodedToken; validateConfig(config); await ConfigDAO.saveConfig(uid, config); return res.sendStatus(200); } catch (e) { return next(e); } } } module.exports = ConfigController; ==> ./monkeytype/backend/api/controllers/new-quotes.js <== const NewQuotesDAO = require("../../dao/new-quotes"); const MonkeyError = require("../../handlers/error"); const UserDAO = require("../../dao/user"); const Logger = require("../../handlers/logger.js"); // const Captcha = require("../../handlers/captcha"); class NewQuotesController { static async getQuotes(req, res, next) { try { const { uid } = req.decodedToken; const userInfo = await UserDAO.getUser(uid); if (!userInfo.quoteMod) { throw new MonkeyError(403, "You don't have permission to do this"); } let data = await NewQuotesDAO.get(); return res.status(200).json(data); } catch (e) { return next(e); } } static async addQuote(req, res, next) { try { throw new MonkeyError( 500, "Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up." ); // let { uid } = req.decodedToken; // let { text, source, language, captcha } = req.body; // if (!text || !source || !language) { // throw new MonkeyError(400, "Please fill all the fields"); // } // if (!(await Captcha.verify(captcha))) { // throw new MonkeyError(400, "Captcha check failed"); // } // let data = await NewQuotesDAO.add(text, source, language, uid); // return res.status(200).json(data); } catch (e) { return next(e); } } static async approve(req, res, next) { try { let { uid } = req.decodedToken; let { quoteId, editText, editSource } = req.body; const userInfo = await UserDAO.getUser(uid); if (!userInfo.quoteMod) { throw new MonkeyError(403, "You don't have permission to do this"); } if (editText === "" || editSource === "") { throw new MonkeyError(400, "Please fill all the fields"); } let data = await NewQuotesDAO.approve(quoteId, editText, editSource); Logger.log("system_quote_approved", data, uid); return res.status(200).json(data); } catch (e) { return next(e); } } static async refuse(req, res, next) { try { let { uid } = req.decodedToken; let { quoteId } = req.body; await NewQuotesDAO.refuse(quoteId, uid); return res.sendStatus(200); } catch (e) { return next(e); } } } module.exports = NewQuotesController; ==> ./monkeytype/backend/api/controllers/psa.js <== const PsaDAO = require("../../dao/psa"); class PsaController { static async get(req, res, next) { try { let data = await PsaDAO.get(); return res.status(200).json(data); } catch (e) { return next(e); } } } module.exports = PsaController; ==> ./monkeytype/backend/api/controllers/result.js <== const ResultDAO = require("../../dao/result"); const UserDAO = require("../../dao/user"); const PublicStatsDAO = require("../../dao/public-stats"); const BotDAO = require("../../dao/bot"); const { validateObjectValues } = require("../../handlers/validation"); const { stdDev, roundTo2 } = require("../../handlers/misc"); const objecthash = require("object-hash"); const Logger = require("../../handlers/logger"); const path = require("path"); const { config } = require("dotenv"); config({ path: path.join(__dirname, ".env") }); let validateResult; let validateKeys; try { let module = require("../../anticheat/anticheat"); validateResult = module.validateResult; validateKeys = module.validateKeys; if (!validateResult || !validateKeys) throw new Error("undefined"); } catch (e) { if (process.env.MODE === "dev") { console.error( "No anticheat module found. Continuing in dev mode, results will not be validated." ); } else { console.error("No anticheat module found."); console.error( "To continue in dev mode, add 'MODE=dev' to the .env file in the backend directory." ); process.exit(1); } } class ResultController { static async getResults(req, res, next) { try { const { uid } = req.decodedToken; const results = await ResultDAO.getResults(uid); return res.status(200).json(results); } catch (e) { next(e); } } static async deleteAll(req, res, next) { try { const { uid } = req.decodedToken; await ResultDAO.deleteAll(uid); Logger.log("user_results_deleted", "", uid); return res.sendStatus(200); } catch (e) { next(e); } } static async updateTags(req, res, next) { try { const { uid } = req.decodedToken; const { tags, resultid } = req.body; await ResultDAO.updateTags(uid, resultid, tags); return res.sendStatus(200); } catch (e) { next(e); } } static async addResult(req, res, next) { try { const { uid } = req.decodedToken; const { result } = req.body; result.uid = uid; if (validateObjectValues(result) > 0) return res.status(400).json({ message: "Bad input" }); if ( result.wpm <= 0 || result.wpm > 350 || result.acc < 75 || result.acc > 100 || result.consistency > 100 ) { return res.status(400).json({ message: "Bad input" }); } if (result.wpm == result.raw && result.acc != 100) { return res.status(400).json({ message: "Bad input" }); } if ( (result.mode === "time" && result.mode2 < 15 && result.mode2 > 0) || (result.mode === "time" && result.mode2 == 0 && result.testDuration < 15) || (result.mode === "words" && result.mode2 < 10 && result.mode2 > 0) || (result.mode === "words" && result.mode2 == 0 && result.testDuration < 15) || (result.mode === "custom" && result.customText !== undefined && !result.customText.isWordRandom && !result.customText.isTimeRandom && result.customText.textLen < 10) || (result.mode === "custom" && result.customText !== undefined && result.customText.isWordRandom && !result.customText.isTimeRandom && result.customText.word < 10) || (result.mode === "custom" && result.customText !== undefined && !result.customText.isWordRandom && result.customText.isTimeRandom && result.customText.time < 15) ) { return res.status(400).json({ message: "Test too short" }); } let resulthash = result.hash; delete result.hash; const serverhash = objecthash(result); if (serverhash !== resulthash) { Logger.log( "incorrect_result_hash", { serverhash, resulthash, result, }, uid ); return res.status(400).json({ message: "Incorrect result hash" }); } if (validateResult) { if (!validateResult(result)) { return res .status(400) .json({ message: "Result data doesn't make sense" }); } } else { if (process.env.MODE === "dev") { console.error( "No anticheat module found. Continuing in dev mode, results will not be validated." ); } else { throw new Error("No anticheat module found"); } } result.timestamp = Math.round(result.timestamp / 1000) * 1000; //dont use - result timestamp is unreliable, can be changed by system time and stuff // if (result.timestamp > Math.round(Date.now() / 1000) * 1000 + 10) { // Logger.log( // "time_traveler", // { // resultTimestamp: result.timestamp, // serverTimestamp: Math.round(Date.now() / 1000) * 1000 + 10, // }, // uid // ); // return res.status(400).json({ message: "Time traveler detected" }); // this probably wont work if we replace the timestamp with the server time later // let timestampres = await ResultDAO.getResultByTimestamp( // uid, // result.timestamp // ); // if (timestampres) { // return res.status(400).json({ message: "Duplicate result" }); // } //convert result test duration to miliseconds const testDurationMilis = result.testDuration * 1000; //get latest result ordered by timestamp let lastResultTimestamp; try { lastResultTimestamp = (await ResultDAO.getLastResult(uid)).timestamp - 1000; } catch (e) { lastResultTimestamp = null; } result.timestamp = Math.round(Date.now() / 1000) * 1000; //check if its greater than server time - milis or result time - milis if ( lastResultTimestamp && (lastResultTimestamp + testDurationMilis > result.timestamp || lastResultTimestamp + testDurationMilis > Math.round(Date.now() / 1000) * 1000) ) { Logger.log( "invalid_result_spacing", { lastTimestamp: lastResultTimestamp, resultTime: result.timestamp, difference: lastResultTimestamp + testDurationMilis - result.timestamp, }, uid ); return res.status(400).json({ message: "Invalid result spacing" }); } try { result.keySpacingStats = { average: result.keySpacing.reduce( (previous, current) => (current += previous) ) / result.keySpacing.length, sd: stdDev(result.keySpacing), }; } catch (e) { // } try { result.keyDurationStats = { average: result.keyDuration.reduce( (previous, current) => (current += previous) ) / result.keyDuration.length, sd: stdDev(result.keyDuration), }; } catch (e) { // } const user = await UserDAO.getUser(uid); // result.name = user.name; //check keyspacing and duration here for bots if ( result.mode === "time" && result.wpm > 130 && result.testDuration < 122 ) { if (user.verified === false || user.verified === undefined) { if ( result.keySpacingStats !== null && result.keyDurationStats !== null ) { if (validateKeys) { if (!validateKeys(result, uid)) { return res .status(400) .json({ message: "Possible bot detected" }); } } else { if (process.env.MODE === "dev") { console.error( "No anticheat module found. Continuing in dev mode, results will not be validated." ); } else { throw new Error("No anticheat module found"); } } } else { return res.status(400).json({ message: "Missing key data" }); } } } delete result.keySpacing; delete result.keyDuration; delete result.smoothConsistency; delete result.wpmConsistency; try { result.keyDurationStats.average = roundTo2( result.keyDurationStats.average ); result.keyDurationStats.sd = roundTo2(result.keyDurationStats.sd); result.keySpacingStats.average = roundTo2( result.keySpacingStats.average ); result.keySpacingStats.sd = roundTo2(result.keySpacingStats.sd); } catch (e) { // } let isPb = false; let tagPbs = []; if (!result.bailedOut) { isPb = await UserDAO.checkIfPb(uid, result); tagPbs = await UserDAO.checkIfTagPb(uid, result); } if (isPb) { result.isPb = true; } if (result.mode === "time" && String(result.mode2) === "60") { UserDAO.incrementBananas(uid, result.wpm); if (isPb && user.discordId) { BotDAO.updateDiscordRole(user.discordId, result.wpm); } } if (result.challenge && user.discordId) { BotDAO.awardChallenge(user.discordId, result.challenge); } else { delete result.challenge; } let tt = 0; let afk = result.afkDuration; if (afk == undefined) { afk = 0; } tt = result.testDuration + result.incompleteTestSeconds - afk; await UserDAO.updateTypingStats(uid, result.restartCount, tt); await PublicStatsDAO.updateStats(result.restartCount, tt); if (result.bailedOut === false) delete result.bailedOut; if (result.blindMode === false) delete result.blindMode; if (result.lazyMode === false) delete result.lazyMode; if (result.difficulty === "normal") delete result.difficulty; if (result.funbox === "none") delete result.funbox; if (result.language === "english") delete result.language; if (result.numbers === false) delete result.numbers; if (result.punctuation === false) delete result.punctuation; if (result.mode !== "custom") delete result.customText; let addedResult = await ResultDAO.addResult(uid, result); if (isPb) { Logger.log( "user_new_pb", `${result.mode + " " + result.mode2} ${result.wpm} ${result.acc}% ${ result.rawWpm } ${result.consistency}% (${addedResult.insertedId})`, uid ); } return res.status(200).json({ message: "Result saved", isPb, name: result.name, tagPbs, insertedId: addedResult.insertedId, }); } catch (e) { next(e); } } static async getLeaderboard(req, res, next) { try { // const { type, mode, mode2 } = req.params; // const results = await ResultDAO.getLeaderboard(type, mode, mode2); // return res.status(200).json(results); return res .status(503) .json({ message: "Leaderboard temporarily disabled" }); } catch (e) { next(e); } } static async checkLeaderboardQualification(req, res, next) { try { // const { uid } = req.decodedToken; // const { result } = req.body; // const data = await ResultDAO.checkLeaderboardQualification(uid, result); // return res.status(200).json(data); return res .status(503) .json({ message: "Leaderboard temporarily disabled" }); } catch (e) { next(e); } } } module.exports = ResultController; ==> ./monkeytype/backend/api/routes/leaderboards.js <== const { authenticateRequest } = require("../../middlewares/auth"); const LeaderboardsController = require("../controllers/leaderboards"); const RateLimit = require("../../middlewares/rate-limit"); const { Router } = require("express"); const router = Router(); router.get("/", RateLimit.leaderboardsGet, LeaderboardsController.get); router.get( "/rank", RateLimit.leaderboardsGet, authenticateRequest, LeaderboardsController.getRank ); module.exports = router; ==> ./monkeytype/backend/api/routes/preset.js <== const { authenticateRequest } = require("../../middlewares/auth"); const PresetController = require("../controllers/preset"); const RateLimit = require("../../middlewares/rate-limit"); const { Router } = require("express"); const router = Router(); router.get( "/", RateLimit.presetsGet, authenticateRequest, PresetController.getPresets ); router.post( "/add", RateLimit.presetsAdd, authenticateRequest, PresetController.addPreset ); router.post( "/edit", RateLimit.presetsEdit, authenticateRequest, PresetController.editPreset ); router.post( "/remove", RateLimit.presetsRemove, authenticateRequest, PresetController.removePreset ); module.exports = router; ==> ./monkeytype/backend/api/routes/core.js <== const { authenticateRequest } = require("../../middlewares/auth"); const { Router } = require("express"); const router = Router(); router.post("/test", authenticateRequest); ==> ./monkeytype/backend/api/routes/user.js <== const { authenticateRequest } = require("../../middlewares/auth"); const { Router } = require("express"); const UserController = require("../controllers/user"); const RateLimit = require("../../middlewares/rate-limit"); const router = Router(); router.get( "/", RateLimit.userGet, authenticateRequest, UserController.getUser ); router.post( "/signup", RateLimit.userSignup, authenticateRequest, UserController.createNewUser ); router.post("/checkName", RateLimit.userCheckName, UserController.checkName); router.post( "/delete", RateLimit.userDelete, authenticateRequest, UserController.deleteUser ); router.post( "/updateName", RateLimit.userUpdateName, authenticateRequest, UserController.updateName ); router.post( "/updateLbMemory", RateLimit.userUpdateLBMemory, authenticateRequest, UserController.updateLbMemory ); router.post( "/updateEmail", RateLimit.userUpdateEmail, authenticateRequest, UserController.updateEmail ); router.post( "/clearPb", RateLimit.userClearPB, authenticateRequest, UserController.clearPb ); router.post( "/tags/add", RateLimit.userTagsAdd, authenticateRequest, UserController.addTag ); router.get( "/tags", RateLimit.userTagsGet, authenticateRequest, UserController.getTags ); router.post( "/tags/clearPb", RateLimit.userTagsClearPB, authenticateRequest, UserController.clearTagPb ); router.post( "/tags/remove", RateLimit.userTagsRemove, authenticateRequest, UserController.removeTag ); router.post( "/tags/edit", RateLimit.userTagsEdit, authenticateRequest, UserController.editTag ); router.post( "/discord/link", RateLimit.userDiscordLink, authenticateRequest, UserController.linkDiscord ); router.post( "/discord/unlink", RateLimit.userDiscordUnlink, authenticateRequest, UserController.unlinkDiscord ); module.exports = router; ==> ./monkeytype/backend/api/routes/index.js <== const pathOverride = process.env.API_PATH_OVERRIDE; const BASE_ROUTE = pathOverride ? `/${pathOverride}` : ""; const API_ROUTE_MAP = { "/user": require("./user"), "/config": require("./config"), "/results": require("./result"), "/presets": require("./preset"), "/psa": require("./psa"), "/leaderboard": require("./leaderboards"), "/quotes": require("./quotes"), }; function addApiRoutes(app) { app.get("/", (req, res) => { res.status(200).json({ message: "OK" }); }); Object.keys(API_ROUTE_MAP).forEach((route) => { const apiRoute = `${BASE_ROUTE}${route}`; const router = API_ROUTE_MAP[route]; app.use(apiRoute, router); }); } module.exports = addApiRoutes; ==> ./monkeytype/backend/api/routes/config.js <== const { authenticateRequest } = require("../../middlewares/auth"); const { Router } = require("express"); const ConfigController = require("../controllers/config"); const RateLimit = require("../../middlewares/rate-limit"); const router = Router(); router.get( "/", RateLimit.configGet, authenticateRequest, ConfigController.getConfig ); router.post( "/save", RateLimit.configUpdate, authenticateRequest, ConfigController.saveConfig ); module.exports = router; ==> ./monkeytype/backend/api/routes/quotes.js <== const joi = require("joi"); const { authenticateRequest } = require("../../middlewares/auth"); const { Router } = require("express"); const NewQuotesController = require("../controllers/new-quotes"); const QuoteRatingsController = require("../controllers/quote-ratings"); const RateLimit = require("../../middlewares/rate-limit"); const { requestValidation } = require("../../middlewares/apiUtils"); const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quoteLanguages"); const quotesRouter = Router(); quotesRouter.get( "/", RateLimit.newQuotesGet, authenticateRequest, NewQuotesController.getQuotes ); quotesRouter.post( "/", RateLimit.newQuotesAdd, authenticateRequest, NewQuotesController.addQuote ); quotesRouter.post( "/approve", RateLimit.newQuotesAction, authenticateRequest, NewQuotesController.approve ); quotesRouter.post( "/reject", RateLimit.newQuotesAction, authenticateRequest, NewQuotesController.refuse ); quotesRouter.get( "/rating", RateLimit.quoteRatingsGet, authenticateRequest, QuoteRatingsController.getRating ); quotesRouter.post( "/rating", RateLimit.quoteRatingsSubmit, authenticateRequest, QuoteRatingsController.submitRating ); quotesRouter.post( "/report", RateLimit.quoteReportSubmit, authenticateRequest, requestValidation({ body: { quoteId: joi.string().required(), quoteLanguage: joi .string() .valid(...SUPPORTED_QUOTE_LANGUAGES) .required(), reason: joi .string() .valid( "Grammatical error", "Inappropriate content", "Low quality content" ) .required(), comment: joi.string().allow("").max(250).required(), }, }), (req, res) => { res.sendStatus(200); } ); module.exports = quotesRouter; ==> ./monkeytype/backend/api/routes/psa.js <== const { authenticateRequest } = require("../../middlewares/auth"); const PsaController = require("../controllers/psa"); const RateLimit = require("../../middlewares/rate-limit"); const { Router } = require("express"); const router = Router(); router.get("/", RateLimit.psaGet, PsaController.get); module.exports = router; ==> ./monkeytype/backend/api/routes/result.js <== const { authenticateRequest } = require("../../middlewares/auth"); const { Router } = require("express"); const ResultController = require("../controllers/result"); const RateLimit = require("../../middlewares/rate-limit"); const router = Router(); router.get( "/", RateLimit.resultsGet, authenticateRequest, ResultController.getResults ); router.post( "/add", RateLimit.resultsAdd, authenticateRequest, ResultController.addResult ); router.post( "/updateTags", RateLimit.resultsTagsUpdate, authenticateRequest, ResultController.updateTags ); router.post( "/deleteAll", RateLimit.resultsDeleteAll, authenticateRequest, ResultController.deleteAll ); router.get( "/getLeaderboard/:type/:mode/:mode2", RateLimit.resultsLeaderboardGet, ResultController.getLeaderboard ); router.post( "/checkLeaderboardQualification", RateLimit.resultsLeaderboardQualificationGet, authenticateRequest, ResultController.checkLeaderboardQualification ); module.exports = router; ==> ./monkeytype/backend/jobs/deleteOldLogs.js <== const { CronJob } = require("cron"); const { mongoDB } = require("../init/mongodb"); const CRON_SCHEDULE = "0 0 0 * * *"; const LOG_MAX_AGE_DAYS = 7; const LOG_MAX_AGE_MILLISECONDS = LOG_MAX_AGE_DAYS * 24 * 60 * 60 * 1000; async function deleteOldLogs() { const data = await mongoDB() .collection("logs") .deleteMany({ timestamp: { $lt: Date.now() - LOG_MAX_AGE_MILLISECONDS } }); Logger.log( "system_logs_deleted", `${data.deletedCount} logs deleted older than ${LOG_MAX_AGE_DAYS} day(s)`, undefined ); } module.exports = new CronJob(CRON_SCHEDULE, deleteOldLogs); ==> ./monkeytype/backend/jobs/index.js <== const updateLeaderboards = require("./updateLeaderboards"); const deleteOldLogs = require("./deleteOldLogs"); module.exports = [updateLeaderboards, deleteOldLogs]; ==> ./monkeytype/backend/jobs/updateLeaderboards.js <== const { CronJob } = require("cron"); const { mongoDB } = require("../init/mongodb"); const BotDAO = require("../dao/bot"); const LeaderboardsDAO = require("../dao/leaderboards"); const CRON_SCHEDULE = "30 4/5 * * * *"; const RECENT_AGE_MINUTES = 10; const RECENT_AGE_MILLISECONDS = RECENT_AGE_MINUTES * 60 * 1000; async function getTop10(leaderboardTime) { return await LeaderboardsDAO.get("time", leaderboardTime, "english", 0, 10); } async function updateLeaderboardAndNotifyChanges(leaderboardTime) { const top10BeforeUpdate = await getTop10(leaderboardTime); const previousRecordsMap = Object.fromEntries( top10BeforeUpdate.map((record) => { return [record.uid, record]; }) ); await LeaderboardsDAO.update("time", leaderboardTime, "english"); const top10AfterUpdate = await getTop10(leaderboardTime); const newRecords = top10AfterUpdate.filter((record) => { const userId = record.uid; const userImprovedRank = userId in previousRecordsMap && previousRecordsMap[userId].rank > record.rank; const newUserInTop10 = !(userId in previousRecordsMap); const isRecentRecord = record.timestamp > Date.now() - RECENT_AGE_MILLISECONDS; return (userImprovedRank || newUserInTop10) && isRecentRecord; }); if (newRecords.length > 0) { await BotDAO.announceLbUpdate( newRecords, `time ${leaderboardTime} english` ); } } async function updateLeaderboards() { await updateLeaderboardAndNotifyChanges("15"); await updateLeaderboardAndNotifyChanges("60"); } module.exports = new CronJob(CRON_SCHEDULE, updateLeaderboards); ==> ./monkeytype/backend/handlers/logger.js <== const { mongoDB } = require("../init/mongodb"); async function log(event, message, uid) { console.log(new Date(), "t", event, "t", uid, "t", message); await mongoDB().collection("logs").insertOne({ timestamp: Date.now(), uid, event, message, }); } module.exports = { log, }; ==> ./monkeytype/backend/handlers/misc.js <== module.exports = { roundTo2(num) { return Math.round((num + Number.EPSILON) * 100) / 100; }, 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 ); }, mean(array) { try { return ( array.reduce((previous, current) => (current += previous)) / array.length ); } catch (e) { return 0; } }, }; ==> ./monkeytype/backend/handlers/auth.js <== const admin = require("firebase-admin"); module.exports = { async verifyIdToken(idToken) { return await admin.auth().verifyIdToken(idToken); }, async updateAuthEmail(uid, email) { return await admin.auth().updateUser(uid, { email, emailVerified: false, }); }, }; ==> ./monkeytype/backend/handlers/pb.js <== /* obj structure time: { 10: [ - this is a list because there can be different personal bests for different difficulties, languages and punctuation { acc, consistency, difficulty, language, punctuation, raw, timestamp, wpm } ] }, words: { 10: [ {} ] }, zen: { zen: [ {} ] }, custom: { custom: { [] } } */ module.exports = { checkAndUpdatePb( obj, lbObj, mode, mode2, acc, consistency, difficulty, lazyMode = false, language, punctuation, raw, wpm ) { //verify structure first if (obj === undefined) obj = {}; if (obj[mode] === undefined) obj[mode] = {}; if (obj[mode][mode2] === undefined) obj[mode][mode2] = []; let isPb = false; let found = false; //find a pb obj[mode][mode2].forEach((pb) => { //check if we should compare first if ( (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && lazyMode === false)) && pb.difficulty === difficulty && pb.language === language && pb.punctuation === punctuation ) { found = true; //compare if (pb.wpm < wpm) { //update isPb = true; pb.acc = acc; pb.consistency = consistency; pb.difficulty = difficulty; pb.language = language; pb.punctuation = punctuation; pb.lazyMode = lazyMode; pb.raw = raw; pb.wpm = wpm; pb.timestamp = Date.now(); } } }); //if not found push a new one if (!found) { isPb = true; obj[mode][mode2].push({ acc, consistency, difficulty, lazyMode, language, punctuation, raw, wpm, timestamp: Date.now(), }); } if ( lbObj && mode === "time" && (mode2 == "15" || mode2 == "60") && !lazyMode ) { //updating lbpersonalbests object //verify structure first if (lbObj[mode] === undefined) lbObj[mode] = {}; if (lbObj[mode][mode2] === undefined || Array.isArray(lbObj[mode][mode2])) lbObj[mode][mode2] = {}; let bestForEveryLanguage = {}; if (obj?.[mode]?.[mode2]) { obj[mode][mode2].forEach((pb) => { if (!bestForEveryLanguage[pb.language]) { bestForEveryLanguage[pb.language] = pb; } else { if (bestForEveryLanguage[pb.language].wpm < pb.wpm) { bestForEveryLanguage[pb.language] = pb; } } }); Object.keys(bestForEveryLanguage).forEach((key) => { if (lbObj[mode][mode2][key] === undefined) { lbObj[mode][mode2][key] = bestForEveryLanguage[key]; } else { if (lbObj[mode][mode2][key].wpm < bestForEveryLanguage[key].wpm) { lbObj[mode][mode2][key] = bestForEveryLanguage[key]; } } }); bestForEveryLanguage = {}; } } return { isPb, obj, lbObj, }; }, }; ==> ./monkeytype/backend/handlers/error.js <== const uuid = require("uuid"); class MonkeyError { constructor(status, message, stack = null, uid) { this.status = status ?? 500; this.errorID = uuid.v4(); this.stack = stack; // this.message = // process.env.MODE === "dev" // ? stack // ? String(stack) // : this.status === 500 // ? String(message) // : message // : "Internal Server Error " + this.errorID; if (process.env.MODE === "dev") { this.message = stack ? String(message) + "\nStack: " + String(stack) : String(message); } else { if (this.stack && this.status >= 500) { this.message = "Internal Server Error " + this.errorID; } else { this.message = String(message); } } } } module.exports = MonkeyError; ==> ./monkeytype/backend/handlers/validation.js <== const MonkeyError = require("./error"); function isUsernameValid(name) { if (name === null || name === undefined || name === "") return false; if (/.*miodec.*/.test(name.toLowerCase())) return false; //sorry for the bad words if ( /.*(bitly|fuck|bitch|shit|pussy|nigga|niqqa|niqqer|nigger|ni99a|ni99er|niggas|niga|niger|cunt|faggot|retard).*/.test( name.toLowerCase() ) ) return false; if (name.length > 14) return false; if (/^\..*/.test(name.toLowerCase())) return false; return /^[0-9a-zA-Z_.-]+$/.test(name); } function isTagPresetNameValid(name) { if (name === null || name === undefined || name === "") return false; if (name.length > 16) return false; return /^[0-9a-zA-Z_.-]+$/.test(name); } function isConfigKeyValid(name) { if (name === null || name === undefined || name === "") return false; if (name.length > 40) return false; return /^[0-9a-zA-Z_.\-#+]+$/.test(name); } function validateConfig(config) { Object.keys(config).forEach((key) => { if (!isConfigKeyValid(key)) { throw new MonkeyError(500, `Invalid config: ${key} failed regex check`); } // if (key === "resultFilters") return; // if (key === "customBackground") return; if (key === "customBackground" || key === "customLayoutfluid") { let val = config[key]; if (/[<>]/.test(val)) { throw new MonkeyError( 500, `Invalid config: ${key}:${val} failed regex check` ); } } else { let val = config[key]; if (Array.isArray(val)) { val.forEach((valarr) => { if (!isConfigKeyValid(valarr)) { throw new MonkeyError( 500, `Invalid config: ${key}:${valarr} failed regex check` ); } }); } else { if (!isConfigKeyValid(val)) { throw new MonkeyError( 500, `Invalid config: ${key}:${val} failed regex check` ); } } } }); return true; } function validateObjectValues(val) { let errCount = 0; if (val === null || val === undefined) { // } else if (Array.isArray(val)) { //array val.forEach((val2) => { errCount += validateObjectValues(val2); }); } else if (typeof val === "object" && !Array.isArray(val)) { //object Object.keys(val).forEach((valkey) => { errCount += validateObjectValues(val[valkey]); }); } else { if (!/^[0-9a-zA-Z._\-+]+$/.test(val)) { errCount++; } } return errCount; } module.exports = { isUsernameValid, isTagPresetNameValid, validateConfig, validateObjectValues, }; ==> ./monkeytype/backend/handlers/captcha.js <== const fetch = require("node-fetch"); const path = require("path"); const { config } = require("dotenv"); config({ path: path.join(__dirname, ".env") }); module.exports = { async verify(captcha) { if (process.env.MODE === "dev") return true; let response = await fetch( `https://www.google.com/recaptcha/api/siteverify`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `secret=${process.env.RECAPTCHA_SECRET}&response=${captcha}`, } ); response = await response.json(); return response?.success; }, }; ==> ./monkeytype/backend/handlers/pb_old.js <== // module.exports = { // check(result, userdata) { // let pbs = null; // if (result.mode == "quote") { // return false; // } // if (result.funbox !== "none") { // return false; // } // pbs = userdata?.personalBests; // if(pbs === undefined){ // //userdao set personal best // return true; // } // // try { // // pbs = userdata.personalBests; // // if (pbs === undefined) { // // throw new Error("pb is undefined"); // // } // // } catch (e) { // // User.findOne({ uid: userdata.uid }, (err, user) => { // // user.personalBests = { // // [result.mode]: { // // [result.mode2]: [ // // { // // language: result.language, // // difficulty: result.difficulty, // // punctuation: result.punctuation, // // wpm: result.wpm, // // acc: result.acc, // // raw: result.rawWpm, // // timestamp: Date.now(), // // consistency: result.consistency, // // }, // // ], // // }, // // }; // // }).then(() => { // // return true; // // }); // // } // let toUpdate = false; // let found = false; // try { // if (pbs[result.mode][result.mode2] === undefined) { // pbs[result.mode][result.mode2] = []; // } // pbs[result.mode][result.mode2].forEach((pb) => { // if ( // pb.punctuation === result.punctuation && // pb.difficulty === result.difficulty && // pb.language === result.language // ) { // //entry like this already exists, compare wpm // found = true; // if (pb.wpm < result.wpm) { // //new pb // pb.wpm = result.wpm; // pb.acc = result.acc; // pb.raw = result.rawWpm; // pb.timestamp = Date.now(); // pb.consistency = result.consistency; // toUpdate = true; // } else { // //no pb // return false; // } // } // }); // //checked all pbs, nothing found - meaning this is a new pb // if (!found) { // pbs[result.mode][result.mode2] = [ // { // language: result.language, // difficulty: result.difficulty, // punctuation: result.punctuation, // wpm: result.wpm, // acc: result.acc, // raw: result.rawWpm, // timestamp: Date.now(), // consistency: result.consistency, // }, // ]; // toUpdate = true; // } // } catch (e) { // // console.log(e); // pbs[result.mode] = {}; // pbs[result.mode][result.mode2] = [ // { // language: result.language, // difficulty: result.difficulty, // punctuation: result.punctuation, // wpm: result.wpm, // acc: result.acc, // raw: result.rawWpm, // timestamp: Date.now(), // consistency: result.consistency, // }, // ]; // toUpdate = true; // } // if (toUpdate) { // // User.findOne({ uid: userdata.uid }, (err, user) => { // // user.personalBests = pbs; // // user.save(); // // }); // //userdao update the whole personalBests parameter with pbs object // return true; // } else { // return false; // } // } // } ==> ./monkeytype/backend/credentials/.gitkeep <== ==> ./monkeytype/.prettierignore <== *.min.js *.min.css layouts.js quotes/* chartjs-plugin-*.js sound/* node_modules css/balloon.css _list.json ==> ./monkeytype/.editorconfig <== root = true [*.{html,js,css,scss,json,yml,yaml}] indent_size = 2 indent_style = space ==> ./monkeytype/.gitignore <== # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* firebase-debug.log* # Firebase cache .firebase/ # Firebase config # Uncomment this if you'd like others to create their own Firebase project. # For a team working on the same Firebase project(s), it is recommended to leave # it commented so all members can deploy to the same project(s) in .firebaserc. # .firebaserc # Runtime data pids *.pid *.seed *.pid.lock #Mac files .DS_Store # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env #vs code .vscode *.code-workspace .idea #firebase .firebaserc .firebaserc_copy serviceAccountKey*.json #generated files dist/ #cloudflare y .cloudflareKey.txt .cloudflareKey_copy.txt purgeCfCache.sh static/adtest.html backend/lastId.txt backend/log_success.txt backend/credentials/*.json backend/.env static/adtest.html backend/migrationStats.txt backend/anticheat ==> ./monkeytype/static/index.html <== Monkeytype
:( It seems like the CSS failed to load. Please clear your cache to redownload the styles. If that doesn't help contact support.
(jack@monkeytype.com or discord.gg/monkeytype)
(ctrl/cmd + shift + r on Chromium browsers) If the website works for a bit but then this screen comes back, clear your cache again and then on Monkeytype open the command line (esc) and search for "Clear SW cache".
==> ./monkeytype/static/privacy-policy.html <== Privacy Policy | Monkeytype

Effective date: September 8, 2021

Thanks for trusting Monkeytype ('Monkeytype', 'we', 'us', 'our') with your personal information! We take our responsibility to you very seriously, and so this Privacy Statement describes how we handle your data.

This Privacy Statement applies to all websites we own and operate and to all services we provide (collectively, the 'Services'). So...PLEASE READ THIS PRIVACY STATEMENT CAREFULLY. By using the Services, you are expressly and voluntarily accepting the terms and conditions of this Privacy Statement and our Terms of Service, which include allowing us to process information about you.

Under this Privacy Statement, we are the data controller responsible for processing your personal information. Our contact information appears at the end of this Privacy Statement.

Table of Contents

What data do we collect?

Monkeytype collects the following data:

How do we collect your data?

You directly provide most of the data we collect. We collect data and process data when you:

How will we use your data?

Monkeytype collects your data so that we can:

How do we store your data?

Monkeytype securely stores your data using Firebase Firestore.

What are your data protection rights?

Monkeytype would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:

What log data do we collect?

Like most websites, Monkeytype collects information that your browser sends whenever you visit the website. This data may include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and time spent on each page. THIS DATA DOES NOT CONTAIN ANY PERSONALLY IDENTIFIABLE INFORMATION. We use this information for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.

In our case, this service is provided by Google Analytics.

What are cookies?

Cookies are text files placed on your computer to collect standard Internet log information and visitor behavior information. When you visit our websites, we may collect information from you automatically through cookies or similar technology

For further information, visit www.wikipedia.org/wiki/HTTP_cookie .

How do we use cookies?

Monkeytype uses cookies in a range of ways to improve your experience on our website, including:

What types of cookies do we use?

There are a number of different types of cookies; however, our website uses functionality cookies. Monkeytype uses these cookies so we recognize you on our website and remember your previously selected settings.

How to manage your cookies

You can set your browser not to accept cookies, and the above website tells you how to remove cookies from your browser. However, in a few cases, some of our website features may behave unexpectedly or fail to function as a result.

Privacy policies of other websites

Monkeytype contains links to other external websites. Our privacy policy only applies to our website, so if you click on a link to another website, you should read their privacy policy.

Changes to our privacy policy

Monkeytype keeps its privacy policy under regular review and places any updates on this web page. The Monkeytype privacy policy may be subject to change at any given time without notice. This privacy policy was last updated on 22 April 2021.

How to contact us

If you have any questions about Monkeytype’s privacy policy, the data we hold on you, or you would like to exercise one of your data protection rights, please do not hesitate to contact us.

Email: jack@monkeytype.com

Discord: Miodec#1512
==> ./monkeytype/static/.well-known <== ==> ./monkeytype/static/.well-known/security.txt <== Contact: mailto:jack@monkeytype.com Contact: message @Miodec on discord.gg/monkeytype Expires: 2022-06-03T21:00:00.000Z Preferred-Languages: en Canonical: https://monkeytype.com/.well-known/security.txt Policy: https://monkeytype.com/security-policy ==> ./monkeytype/static/funbox/earthquake.css <== @keyframes shake_dat_ass { 0% { transform: translate(1px, 1px) rotate(0deg); } 10% { transform: translate(-1px, -2px) rotate(-1deg); } 20% { transform: translate(-3px, 0px) rotate(1deg); } 30% { transform: translate(3px, 2px) rotate(0deg); } 40% { transform: translate(1px, -1px) rotate(1deg); } 50% { transform: translate(-1px, 2px) rotate(-1deg); } 60% { transform: translate(-3px, 1px) rotate(0deg); } 70% { transform: translate(3px, 1px) rotate(-1deg); } 80% { transform: translate(-1px, -1px) rotate(1deg); } 90% { transform: translate(1px, 2px) rotate(0deg); } 100% { transform: translate(1px, -2px) rotate(-1deg); } } letter { animation: shake_dat_ass 0.25s infinite linear; } ==> ./monkeytype/static/funbox/nausea.css <== @keyframes woah { 0% { transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) scaleY(0.9); } 25% { transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1) scaleY(0.8); } 50% { transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(0.9) scaleY(0.9); } 75% { transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1.5) scaleY(1.1); } 100% { transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) scaleY(0.9); } } #middle { animation: woah 7s infinite cubic-bezier(0.5, 0, 0.5, 1); } #centerContent { transform: rotate(5deg); perspective: 500px; } body { overflow: hidden; } ==> ./monkeytype/static/funbox/read_ahead_hard.css <== #words .word.active:nth-of-type(n + 2), #words .word.active:nth-of-type(n + 2) + .word, #words .word.active:nth-of-type(n + 2) + .word + .word { color: transparent; } ==> ./monkeytype/static/funbox/space_balls.css <== :root { --bg-color: #000000; --main-color: #ffffff; --caret-color: #ffffff; --sub-color: rgba(255, 255, 255, 0.1); --text-color: #ffd100; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } body { background-image: url("https://thumbs.gfycat.com/SlimyClassicAsianconstablebutterfly-size_restricted.gif"); background-size: cover; background-position: center; } #middle { transform: rotateX(35deg); } #centerContent { perspective: 500px; } ==> ./monkeytype/static/funbox/read_ahead_easy.css <== #words .word.active:nth-of-type(n + 2) { color: transparent; } ==> ./monkeytype/static/funbox/mirror.css <== #middle { transform: scaleX(-1); } ==> ./monkeytype/static/funbox/round_round_baby.css <== @keyframes woah { 0% { transform: rotateZ(0deg); } 50% { transform: rotateZ(180deg); } 100% { transform: rotateZ(360deg); } } #middle { animation: woah 5s infinite linear; } body { overflow: hidden; } ==> ./monkeytype/static/funbox/choo_choo.css <== @keyframes woah { 0% { transform: rotateZ(0deg); } 50% { transform: rotateZ(180deg); } 100% { transform: rotateZ(360deg); } } letter { animation: woah 2s infinite linear; } ==> ./monkeytype/static/funbox/read_ahead.css <== #words .word.active:nth-of-type(n + 2), #words .word.active:nth-of-type(n + 2) + .word { color: transparent; } ==> ./monkeytype/static/funbox/simon_says.css <== /* #words { opacity: 0 !important; } */ #words .word { color: transparent !important; } ==> ./monkeytype/static/email-handler.html <== Email Handler | Monkeytype
==> ./monkeytype/static/terms-of-service.html <== Terms of Service | Monkeytype

These terms of service were last updated on September 11, 2021.

Agreement

By accessing this Website, accessible from monkeytype.com, you are agreeing to be bound by these Website Terms of Service and agree that you are responsible for the agreement in accordance with any applicable local laws. IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, YOU ARE NOT PERMITTED TO ACCESS OR USE OUR SERVICES.

Limitations

You are responsible for your account's security and all activities on your account. You must not, in the use of this site, violate any applicable laws, including, without limitation, copyright laws, or any other laws regarding the security of your personal data, or otherwise misuse this site.

Monkeytype reserves the right to remove or disable any account or any other content on this site at any time for any reason, without prior notice to you, if we believe that you have violated this agreement.

You agree that you will not upload, post, host, or transmit any content that:

  1. is unlawful or promotes unlawful activities;
  2. is or contains sexually obscene content;
  3. is libelous, defamatory, or fraudulent;
  4. is discriminatory or abusive toward any individual or group;
  5. is degrading to others on the basis of gender, race, class, ethnicity, national origin, religion, sexual preference, orientation, or identity, disability, or other classification, or otherwise represents or condones content that: is hate speech, discriminating, threatening, or pornographic; incites violence; or contains nudity or graphic or gratuitous violence;
  6. violates any person's right to privacy or publicity, or otherwise solicits, collects, or publishes data, including personal information and login information, about other Users without consent or for unlawful purposes in violation of any applicable international, federal, state, or local law, statute, ordinance, or regulation; or
  7. contains or installs any active malware or exploits/uses our platform for exploit delivery (such as part of a command or control system); or infringes on any proprietary right of any party, including patent, trademark, trade secret, copyright, right of publicity, or other rights.

While using the Services, you agree that you will not:

  1. harass, abuse, threaten, or incite violence towards any individual or group, including other Users and Monkeytype contributors;
  2. use our servers for any form of excessive automated bulk activity (e.g., spamming), or rely on any other form of unsolicited advertising or solicitation through our servers or Services;
  3. attempt to disrupt or tamper with our servers in ways that could a) harm our Website or Services or b) place undue burden on our servers;
  4. access the Services in ways that exceed your authorization;
  5. falsely impersonate any person or entity, including any of our contributors, misrepresent your identity or the site's purpose, or falsely associate yourself with Monkeytype;
  6. violate the privacy of any third party, such as by posting another person's personal information without their consent;
  7. access or attempt to access any service on the Services by any means other than as permitted in this Agreement, or operating the Services on any computers or accounts which you do not have permission to operate;
  8. facilitate or encourage any violations of this Agreement or interfere with the operation, appearance, security, or functionality of the Services; or
  9. use the Services in any manner that is harmful to minors.

Without limiting the foregoing, you will not transmit or post any content anywhere on the Services that violates any laws. Monkeytype absolutely does not tolerate engaging in activity that significantly harms our Users. We will resolve disputes in favor of protecting our Users as a whole.

Privacy Policy

If you use our Services, you must abide by our Privacy Policy. You acknowledge that you have read our Privacy Policy and understand that it sets forth how we collect, use, and store your information. If you do not agree with our Privacy Statement, then you must stop using the Services immediately. Any person, entity, or service collecting data from the Services must comply with our Privacy Statement. Misuse of any User's Personal Information is prohibited. If you collect any Personal Information from a User, you agree that you will only use the Personal Information you gather for the purpose for which the User has authorized it. You agree that you will reasonably secure any Personal Information you have gathered from the Services, and you will respond promptly to complaints, removal requests, and 'do not contact' requests from us or Users.

Limitations on Automated Use

You shouldn't use bots or access our Services in malicious or un-permitted ways. While accessing or using the Services, you may not:
  1. use bots, hacks, or cheats while using our site;
  2. create manual requests to Monkeytype servers;
  3. tamper with or use non-public areas of the Services, or the computer or delivery systems of Monkeytype and/or its service providers;
  4. probe, scan, or test any system or network (particularly for vulnerabilities), or otherwise attempt to breach or circumvent any security or authentication measures, or search or attempt to access or search the Services by any means (automated or otherwise) other than through our currently available, published interfaces that are provided by Monkeytype (and only pursuant to those terms and conditions), unless you have been specifically allowed to do so in a separate agreement with Monkeytype, Inc., or unless specifically permitted by Monkeytype, Inc.'s robots.txt file or other robot exclusion mechanisms;
  5. scrape the Services, scrape Content from the Services, or use automated means, including spiders, robots, crawlers, data mining tools, or the like to download data from the Services or otherwise access the Services;
  6. employ misleading email or IP addresses or forged headers or otherwise manipulated identifiers in order to disguise the origin of any content transmitted to or through the Services;
  7. use the Services to send altered, deceptive, or false source-identifying information, including, without limitation, by forging TCP-IP packet headers or e-mail headers; or
  8. interfere with, or disrupt or attempt to interfere with or disrupt, the access of any User, host, or network, including, without limitation, by sending a virus to, spamming, or overloading the Services, or by scripted use of the Services in such a manner as to interfere with or create an undue burden on the Services.

Links

Monkeytype is not responsible for the contents of any linked sites. The use of any linked website is at the user's own risk.

Changes

Monkeytype may revise these Terms of Service for its Website at any time without prior notice. By using this Website, you are agreeing to be bound by the current version of these Terms of Service.

Disclaimer

EXCLUDING THE EXPLICITLY STATED WARRANTIES WITHIN THESE TERMS, WE ONLY OFFER OUR SERVICES ON AN 'AS-IS' BASIS. YOUR ACCESS TO AND USE OF THE SERVICES OR ANY CONTENT IS AT YOUR OWN RISK. YOU UNDERSTAND AND AGREE THAT THE SERVICES AND CONTENT ARE PROVIDED TO YOU ON AN 'AS IS,' 'WITH ALL FAULTS,' AND 'AS AVAILABLE' BASIS. WITHOUT LIMITING THE FOREGOING, TO THE FULL EXTENT PERMITTED BY LAW, MONKEYTYPE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. TO THE EXTENT SUCH DISCLAIMER CONFLICTS WITH APPLICABLE LAW, THE SCOPE AND DURATION OF ANY APPLICABLE WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW. MONKEYTYPE MAKES NO REPRESENTATIONS, WARRANTIES, OR GUARANTEES AS TO THE RELIABILITY, TIMELINESS, QUALITY, SUITABILITY, AVAILABILITY, ACCURACY, OR COMPLETENESS OF ANY KIND WITH RESPECT TO THE SERVICES, INCLUDING ANY REPRESENTATION OR WARRANTY THAT THE USE OF THE SERVICES WILL (A) BE TIMELY, UNINTERRUPTED, OR ERROR-FREE, OR OPERATE IN COMBINATION WITH ANY OTHER HARDWARE, SOFTWARE, SYSTEM, OR DATA, (B) MEET YOUR REQUIREMENTS OR EXPECTATIONS, (C) BE FREE FROM ERRORS OR THAT DEFECTS WILL BE CORRECTED, OR (D) BE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. MONKEYTYPE ALSO MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND WITH RESPECT TO CONTENT; USER CONTENT IS PROVIDED BY AND IS SOLELY THE RESPONSIBILITY OF THE RESPECTIVE USER PROVIDING THAT CONTENT. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM MONKEYTYPE OR THROUGH THE SERVICES, WILL CREATE ANY WARRANTY NOT EXPRESSLY MADE HEREIN. MONKEYTYPE DOES NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY USER CONTENT ON THE SERVICES OR ANY HYPERLINKED WEBSITE OR THIRD-PARTY SERVICE, AND MONKEYTYPE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR TRANSACTIONS BETWEEN YOU AND THIRD PARTIES. IF APPLICABLE LAW DOES NOT ALLOW THE EXCLUSION OF SOME OR ALL OF THE ABOVE IMPLIED OR STATUTORY WARRANTIES TO APPLY TO YOU, THE ABOVE EXCLUSIONS WILL APPLY TO YOU TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW.

Contact

If you have any questions about Monkeytype’s privacy policy, the data we hold on you, or you would like to exercise one of your data protection rights, please do not hesitate to contact us.

Email: jack@monkeytype.com

Discord: Miodec#1512

Terms based on Glitch terms

==> ./monkeytype/static/robots.txt <== User-agent: * Disallow: ==> ./monkeytype/static/security-policy.html <== Security Policy | Monkeytype

We take the security and integrity of Monkeytype very seriously. If you have found a vulnerability, please report it ASAP so we can quickly remediate the issue.

Table of Contents

How to Disclose a Vulnerability

For vulnerabilities that impact the confidentiality, integrity, and availability of Monkeytype services, please send your disclosure via (1) email , or (2) ping Miodec#1512 on the Monkeytype Discord server in the #development channel and he can discuss the situation with you further in private. For non-security related platform bugs, follow the bug submission guidelines . Include as much detail as possible to ensure reproducibility. At a minimum, vulnerability disclosures should include:

Submission Guidelines

Do not engage in activities that might cause a denial of service condition, create significant strains on critical resources, or negatively impact users of the site outside of test accounts.

==> ./monkeytype/static/themes/terminal.css <== :root { --bg-color: #191a1b; --caret-color: #79a617; --main-color: #79a617; --sub-color: #48494b; --text-color: #e7eae0; --error-color: #a61717; --error-extra-color: #731010; --colorful-error-color: #a61717; --colorful-error-extra-color: #731010; } ==> ./monkeytype/static/themes/ms_cupcakes.css <== :root { --bg-color: #ffffff; --main-color: #5ed5f3; --caret-color: #303030; --sub-color: #d64090; --text-color: #0a282f; --error-color: #000000; --error-extra-color: #c9c9c9; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/fledgling.css <== :root { --bg-color: #3b363f; --main-color: #fc6e83; --caret-color: #474747; --sub-color: #ead8d6; --text-color: #fc6e83; --error-color: #f52443; --error-extra-color: #bd001c; --colorful-error-color: #ff0a2f; --colorful-error-extra-color: #000000; } ==> ./monkeytype/static/themes/horizon.css <== :root { --bg-color: #1C1E26; --main-color:#c4a88a; --caret-color: #BBBBBB; --sub-color: #db886f; --text-color: #bbbbbb; --error-color: #D55170; --error-extra-color: #ff3d3d; --colorful-error-color: #D55170; --colorful-error-extra-color:#D55170; } #menu .icon-button:nth-child(1) { color: #D55170; } #menu .icon-button:nth-child(2) { color: #E4A88A; } #menu .icon-button:nth-child(3) { color: #DB886F; } #menu .icon-button:nth-child(4) { color: #DB887A; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #FFC819; } ==> ./monkeytype/static/themes/vaporwave.css <== :root { --bg-color: #a4a7ea; --main-color: #e368da; --caret-color: #28cafe; --sub-color: #7c7faf; --text-color: #f1ebf1; --error-color: #573ca9; --error-extra-color: #3d2b77; --colorful-error-color: #28cafe; --colorful-error-extra-color: #25a9ce; } ==> ./monkeytype/static/themes/witch_girl.css <== :root { --bg-color: #f3dbda; --main-color: #56786a; --caret-color: #afc5bd; --sub-color: #ddb4a7; --text-color: #56786a; --error-color: #b29a91; --error-extra-color: #b29a91; --colorful-error-color: #b29a91; --colorful-error-extra-color: #b29a91; } ==> ./monkeytype/static/themes/gruvbox_light.css <== :root { --bg-color: #fbf1c7; --main-color: #689d6a; --caret-color: #689d6a; --sub-color: #a89984; --text-color: #3c3836; --error-color: #cc241d; --error-extra-color: #9d0006; --colorful-error-color: #cc241d; --colorful-error-extra-color: #9d0006; } ==> ./monkeytype/static/themes/soaring_skies.css <== :root { --bg-color: #fff9f2; --main-color: #55c6f0; --caret-color: #1e107a; --sub-color: #1e107a; --text-color: #1d1e1e; --error-color: #fb5745; --error-extra-color: #b03c30; --colorful-error-color: #fb5745; --colorful-error-extra-color: #b03c30; } ==> ./monkeytype/static/themes/terra.css <== :root { --bg-color: #0c100e; --main-color: #89c559; --caret-color: #89c559; --sub-color: #436029; --text-color: #f0edd1; --error-color: #d3ca78; --error-extra-color: #89844d; --colorful-error-color: #d3ca78; --colorful-error-extra-color: #89844d; } ==> ./monkeytype/static/themes/camping.css <== :root { --bg-color: #faf1e4; --main-color: #618c56; --caret-color: #618c56; --sub-color: #c2b8aa; --text-color: #3c403b; --error-color: #ad4f4e; --error-extra-color: #7e3a39; --colorful-error-color: #ad4f4e; --colorful-error-extra-color: #7e3a39; } #top .logo .bottom { color: #ad4f4e; } ==> ./monkeytype/static/themes/lil_dragon.css <== :root { --bg-color: #ebe1ef; --main-color: #8a5bd6; --caret-color: #212b43; --sub-color: #ac76e5; --text-color: #212b43; --error-color: #f794ca; --error-extra-color: #f279c2; --colorful-error-color: #f794ca; --colorful-error-extra-color: #f279c2; } #menu .icon-button { color: #ba96db; } #menu .icon-button:hover { color: #212b43; } ==> ./monkeytype/static/themes/laser.css <== :root { --bg-color: #221b44; --main-color: #009eaf; --caret-color: #009eaf; --sub-color: #b82356; --text-color: #dbe7e8; --error-color: #a8d400; --error-extra-color: #668000; --colorful-error-color: #a8d400; --colorful-error-extra-color: #668000; } ==> ./monkeytype/static/themes/desert_oasis.css <== :root { --bg-color: #fff2d5; /*Background*/ --main-color: #d19d01; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ --caret-color: #3a87fe; /*Cursor Color*/ --sub-color: #0061fe; /*WPM text color of scrollbar and general color, before typed color*/ --text-color: #332800; /*Color of text after hovering over it*/ --error-color: #76bb40; --error-extra-color: #4e7a27; --colorful-error-color: #76bb40; --colorful-error-extra-color: #4e7a27; } #menu .icon-button:nth-child(1) { color: #76bb40; } #menu .icon-button:nth-child(2) { color: #76bb40; } #menu .icon-button:nth-child(3) { color: #76bb40; } #menu .icon-button:nth-child(4) { color: #76bb40; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #76bb40; } ==> ./monkeytype/static/themes/honey.css <== :root { --bg-color: #f2aa00; --main-color: #fff546; --caret-color: #795200; --sub-color: #a66b00; --text-color: #f3eecb; --error-color: #df3333; --error-extra-color: #6d1f1f; --colorful-error-color: #df3333; --colorful-error-extra-color: #6d1f1f; } ==> ./monkeytype/static/themes/9009.css <== :root { --bg-color: #eeebe2; --main-color: #080909; --caret-color: #7fa480; --sub-color: #99947f; --text-color: #080909; --error-color: #c87e74; --colorful-error-color: #a56961; --colorful-error-color: #c87e74; --colorful-error-extra-color: #a56961; } .word letter.incorrect { color: var(--error-color); } .word letter.incorrect.extra { color: var(--colorful-error-color); } .word.error { border-bottom: solid 2px var(--error-color); } key { color: var(--sub-color); background-color: var(--main-color); } #menu .icon-button { color: var(--main-color); } #menu .icon-button:nth-child(1) { color: var(--error-color); } #menu .icon-button:nth-child(4) { color: var(--caret-color); } ==> ./monkeytype/static/themes/sweden.css <== :root { --bg-color: #0058a3; --main-color: #ffcc02; --caret-color: #b5b5b5; --sub-color: #57abdb; --text-color: #ffffff; --error-color: #e74040; --error-extra-color: #a22f2f; --colorful-error-color: #f56674; --colorful-error-extra-color: #e33546; } ==> ./monkeytype/static/themes/solarized_dark.css <== :root { --bg-color: #002b36; --main-color: #859900; --caret-color: #dc322f; --sub-color: #2aa198; --text-color: #268bd2; --error-color: #d33682; --error-extra-color: #9b225c; --colorful-error-color: #d33682; --colorful-error-extra-color: #9b225c; } ==> ./monkeytype/static/themes/mizu.css <== :root { --bg-color: #afcbdd; --main-color: #fcfbf6; --caret-color: #fcfbf6; --sub-color: #85a5bb; --text-color: #1a2633; --error-color: #bf616a; --error-extra-color: #793e44; --colorful-error-color: #bf616a; --colorful-error-extra-color: #793e44; } ==> ./monkeytype/static/themes/terror_below.css <== :root { --bg-color: #0b1e1a; --caret-color: #66ac92; --main-color: #66ac92; --sub-color: #015c53; --text-color: #dceae5; --error-color: #bf616a; --error-extra-color: #793e44; --colorful-error-color: #bf616a; --colorful-error-extra-color: #793e44; } ==> ./monkeytype/static/themes/bingsu.css <== :root { /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */ --bg-color: #b8a7aa; --main-color: #83616e; --caret-color: #ebe6ea; --sub-color: #48373d; --text-color: #ebe6ea; --error-color: #921341; --error-extra-color: #640b2c; --colorful-error-color: #921341; --colorful-error-extra-color: #640b2c; } /* .word.error{ border-bottom: double 4px var(--error-color); } */ #menu .icon-button:nth-child(1) { color: var(--caret-color); } ==> ./monkeytype/static/themes/botanical.css <== :root { --bg-color: #7b9c98; --main-color: #eaf1f3; --caret-color: #abc6c4; --sub-color: #495755; --text-color: #eaf1f3; --error-color: #f6c9b4; --error-extra-color: #f59a71; --colorful-error-color: #f6c9b4; --colorful-error-extra-color: #f59a71; } ==> ./monkeytype/static/themes/iceberg_dark.css <== :root { --bg-color: #161821; --caret-color: #d2d4de; --main-color: #84a0c6; --sub-color: #595e76; --text-color: #c6c8d1; --error-color: #e27878; --error-extra-color: #e2a478; --colorful-error-color: #e27878; --colorful-error-extra-color: #e2a478; } ==> ./monkeytype/static/themes/aether.css <== :root { --bg-color: #101820; --main-color: #eedaea; --caret-color: #eedaea; --sub-color: #cf6bdd; --text-color: #eedaea; --error-color: #ff5253; --error-extra-color: #e3002b; --colorful-error-color: #ff5253; --colorful-error-extra-color: #e3002b; } #menu .icon-button:nth-child(1) { color: #e4002b; } #menu .icon-button:nth-child(2) { color: #c53562; } #menu .icon-button:nth-child(3) { color: #95549e; } #menu .icon-button:nth-child(4) { color: #6744a1; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #393c73; } ==> ./monkeytype/static/themes/magic_girl.css <== :root { --bg-color: #ffffff; --main-color: #f5b1cc; --caret-color: #e45c96; --sub-color: #93e8d3; --text-color: #00ac8c; --error-color: #ffe495; --error-extra-color: #e45c96; --colorful-error-color: #ffe485; --colorful-error-extra-color: #e45c96; } ==> ./monkeytype/static/themes/mashu.css <== :root { --bg-color: #2b2b2c; --main-color: #76689a; --caret-color: #76689a; --sub-color: #d8a0a6; --text-color: #f1e2e4; --error-color: #d44729; --error-extra-color: #8f2f19; --colorful-error-color: #d44729; --colorful-error-extra-color: #8f2f19; } ==> ./monkeytype/static/themes/dark_magic_girl.css <== :root { --bg-color: #091f2c; --main-color: #f5b1cc; --caret-color: #93e8d3; --sub-color: #93e8d3; --text-color: #a288d9; --error-color: #e45c96; --error-extra-color: #e45c96; --colorful-error-color: #00b398; --colorful-error-extra-color: #e45c96; } ==> ./monkeytype/static/themes/mint.css <== :root { --bg-color: #05385b; --main-color: #5cdb95; --caret-color: #5cdb95; --sub-color: #20688a; --text-color: #edf5e1; --error-color: #f35588; --error-extra-color: #a3385a; --colorful-error-color: #f35588; --colorful-error-extra-color: #a3385a; } ==> ./monkeytype/static/themes/rudy.css <== :root { --bg-color: #1a2b3e; --caret-color: #af8f5c; --main-color: #af8f5c; --sub-color: #3a506c; --text-color: #c9c8bf; --error-color: #bf616a; --error-extra-color: #793e44; --colorful-error-color: #bf616a; --colorful-error-extra-color: #793e44; } ==> ./monkeytype/static/themes/dev.css <== /*this theme is based on "Dev theme by KDr3w" color pallet: https://www.deviantart.com/kdr3w/art/Dev-825722799 */ :root { --bg-color: #1b2028; --main-color: #23a9d5; --caret-color: #4b5975; --sub-color: #4b5975; --text-color: #ccccb5; --error-color: #b81b2c; --error-extra-color: #84131f; --colorful-error-color: #b81b2c; --colorful-error-extra-color: #84131f; } ==> ./monkeytype/static/themes/dollar.css <== :root { --bg-color: #e4e4d4; --main-color: #6b886b; --caret-color: #424643; --sub-color: #8a9b69; --text-color: #555a56; --error-color: #d60000; --error-extra-color: #f68484; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/onedark.css <== :root { --bg-color: #2f343f; --caret-color: #61afef; --main-color: #61afef; --sub-color: #eceff4; --text-color: #98c379; --error-color: #e06c75; --error-extra-color: #d62436; --colorful-error-color: #d62436; --colorful-error-extra-color: #ff0019; } ==> ./monkeytype/static/themes/red_dragon.css <== :root { --bg-color: #1a0b0c; --main-color: #ff3a32; --caret-color: #ff3a32; --sub-color: #e2a528; --text-color: #4a4d4e; --error-color: #771b1f; --error-extra-color: #591317; --colorful-error-color: #771b1f; --colorful-error-extra-color: #591317; } ==> ./monkeytype/static/themes/tiramisu.css <== :root { --bg-color: #cfc6b9; --main-color: #c0976f; --caret-color: #7d5448; --sub-color: #c0976f; --text-color: #7d5448; --error-color: #e9632d; --error-extra-color: #e9632d; --colorful-error-color: #e9632d; --colorful-error-extra-color: #e9632d; } ==> ./monkeytype/static/themes/midnight.css <== :root { --bg-color: #0b0e13; --main-color: #60759f; --caret-color: #60759f; --sub-color: #394760; --text-color: #9fadc6; --error-color: #c27070; --error-extra-color: #c28b70; --colorful-error-color: #c27070; --colorful-error-extra-color: #c28b70; } ==> ./monkeytype/static/themes/dots.css <== :root { --bg-color: #121520; --caret-color: #fff; --main-color: #fff; --sub-color: #676e8a; --text-color: #fff; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } #menu { gap: 0.5rem; } #top.focus #menu .icon-button, #top.focus #menu:before, #top.focus #menu:after { background: var(--sub-color); } #menu .icon-button { border-radius: 10rem !important; color: #121520; } /* #menu:before{ content: ""; background: #f94348; width: 1.25rem; height: 1.25rem; padding: .5rem; border-radius: 10rem; } */ #menu .icon-button:nth-child(1) { background: #f94348; } #menu .icon-button:nth-child(2) { background: #9261ff; } #menu .icon-button:nth-child(3) { background: #3cc5f8; } #menu .icon-button:nth-child(4) { background: #4acb8a; } #menu .icon-button:nth-child(5) { background: #ffd543; } #menu .icon-button:nth-child(6), #menu .icon-button:nth-child(7) { background: #ff9349; } /* #menu:after{ content: ""; background: #ff9349; width: 1.25rem; height: 1.25rem; padding: .5rem; border-radius: 10rem; } */ #top.focus #menu .icon-button.discord::after { border-color: transparent; } ==> ./monkeytype/static/themes/ez_mode.css <== :root { --bg-color: #0068c6; --main-color: #fa62d5; --caret-color: #4ddb47; --sub-color: #f5f5f5; --text-color: #fa62d5; --error-color: #4ddb47; --error-extra-color: #42ba3b; --colorful-error-color: #4ddb47; --colorful-error-extra-color: #42ba3b; } .pageSettings .section h1 { color: var(--text-color); } .pageSettings .section > .text { color: var(--sub-color); } .pageAbout .section .title { color: var(--text-color); } .pageAbout .section p { color: var(--sub-color); } #leaderboardsWrapper #leaderboards .title { color: var(--sub-color); } #leaderboardsWrapper #leaderboards .tables table thead { color: var(--sub-color); } #leaderboardsWrapper #leaderboards .tables table tbody { color: var(--sub-color); } ==> ./monkeytype/static/themes/matrix.css <== :root { --bg-color: #000000; --main-color: #15ff00; --caret-color: #15ff00; --sub-color: #003B00; --text-color: #adffa7; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } #liveWpm, #timerNumber { color: white; } ==> ./monkeytype/static/themes/aurora.css <== :root { --bg-color: #011926; --main-color: #00e980; --caret-color: #00e980; --sub-color: #245c69; --text-color: #fff; --error-color: #b94da1; --error-extra-color: #9b3a76; --colorful-error-color: #b94da1; --colorful-error-extra-color: #9b3a76; } @keyframes rgb { 0% { color: #009fb4; } 25% { color: #00e975; } 50% { color: #00ffea; } 75% { color: #00e975; } 100% { color: #009fb4; } } @keyframes rgb-bg { 0% { background: #009fb4; } 25% { background: #00e975; } 50% { background: #00ffea; } 75% { background: #00e975; } 100% { background: #009fb4; } } .button.discord::after, #caret, .pageSettings .section .buttons .button.active, .pageSettings .section.languages .buttons .language.active, .pageAccount .group.filterButtons .buttons .button.active { animation: rgb-bg 5s linear infinite; } #top.focus .button.discord::after, #top .button.discord.dotHidden::after { animation-name: none !important; } .logo .bottom, #top .config .group .buttons .text-button.active, #result .stats .group .bottom, #menu .icon-button:hover, #top .config .group .buttons .text-button:hover, a:hover, #words.flipped .word { animation: rgb 5s linear infinite; } #words.flipped .word letter.correct { color: var(--sub-color); } #words:not(.flipped) .word letter.correct { animation: rgb 5s linear infinite; } ==> ./monkeytype/static/themes/night_runner.css <== :root { --bg-color: #212121; --main-color: #feff04; --caret-color: #feff04; --sub-color: #5c4a9c; --text-color: #e8e8e8; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } ==> ./monkeytype/static/themes/taro.css <== :root { /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */ --bg-color: #b3baff; --main-color: #130f1a; --caret-color: #00e9e5; --sub-color: #6f6c91; --text-color: #130f1a; --error-color: #ffe23e; --error-extra-color: #fff1c3; --colorful-error-color: #ffe23e; --colorful-error-extra-color: #fff1c3; } .word.error { border-bottom: dotted 2px var(--text-color); } #menu .icon-button:nth-child(1) { background: var(--caret-color); border-radius: 50%; } #menu .icon-button:nth-child(2) { background: var(--error-color); border-radius: 50%; } ==> ./monkeytype/static/themes/vscode.css <== :root { --bg-color: #1e1e1e; --main-color: #007acc; --caret-color: #569cd6; --sub-color: #4d4d4d; --text-color: #d4d4d4; --error-color: #f44747; --error-extra-color: #f44747; --colorful-error-color: #f44747; --colorful-error-extra-color: #f44747; } ==> ./monkeytype/static/themes/nord.css <== :root { --bg-color: #242933; --caret-color: #d8dee9; --main-color: #d8dee9; --sub-color: #617b94; --text-color: #d8dee9; --error-color: #bf616a; --error-extra-color: #793e44; --colorful-error-color: #bf616a; --colorful-error-extra-color: #793e44; } ==> ./monkeytype/static/themes/iceberg_light.css <== :root { --bg-color: #e8e9ec; --caret-color: #262a3f; --main-color: #2d539e; --sub-color: #adb1c4; --text-color: #33374c; --error-color: #cc517a; --error-extra-color: #cc3768; --colorful-error-color: #cc517a; --colorful-error-extra-color: #cc3768; } ==> ./monkeytype/static/themes/wavez.css <== :root { --bg-color: #1c292f; --main-color: #6bde3b; --caret-color: #6bde3b; --sub-color: #1a454e; --text-color: #e9efe6; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/pink_lemonade.css <== :root { --bg-color: #f6d992; --main-color: #f6a192; --caret-color: #fcfcf8; --sub-color: #f6b092; --text-color: #fcfcf8; --error-color: #ff6f69; --error-extra-color: #ff6f69; --colorful-error-color: #ff6f69; --colorful-error-extra-color: #ff6f69; } ==> ./monkeytype/static/themes/dualshot.css <== :root { --bg-color: #737373; --main-color: #212222; --caret-color: #212222; --sub-color: #aaaaaa; --text-color: #212222; --error-color: #c82931; --error-extra-color: #ac1823; --colorful-error-color: #c82931; --colorful-error-extra-color: #ac1823; } #menu .icon-button:nth-child(1) { color: #2884bb; } #menu .icon-button:nth-child(2) { color: #25a5a9; } #menu .icon-button:nth-child(3) { color: #de9c24; } #menu .icon-button:nth-child(4) { color: #d82231; } #menu .icon-button:nth-child(5) { color: #212222; } #menu .icon-button:nth-child(6) { color: #212222; } ==> ./monkeytype/static/themes/material.css <== :root { --bg-color: #263238; --main-color: #80cbc4; --caret-color: #80cbc4; --sub-color: #4c6772; --text-color: #e6edf3; --error-color: #fb4934; --error-extra-color: #cc241d; --colorful-error-color: #fb4934; --colorful-error-extra-color: #cc241d; } ==> ./monkeytype/static/themes/paper.css <== :root { --bg-color: #eeeeee; --main-color: #444444; --caret-color: #444444; --sub-color: #b2b2b2; --text-color: #444444; --error-color: #d70000; --error-extra-color: #d70000; --colorful-error-color: #d70000; --colorful-error-extra-color: #d70000; } ==> ./monkeytype/static/themes/metropolis.css <== :root { --bg-color: #0f1f2c; --main-color: #56c3b7; --caret-color: #56c3b7; --sub-color: #326984; --text-color: #e4edf1; --error-color: #d44729; --error-extra-color: #8f2f19; --colorful-error-color: #d44729; --colorful-error-extra-color: #8f2f19; } #top .logo .bottom { color: #f4bc46; } #menu .icon-button:nth-child(1) { color: #d44729; } #menu .icon-button:nth-child(2) { color: #d44729; } #menu .icon-button:nth-child(3) { color: #d44729; } #menu .icon-button:nth-child(4) { color: #d44729; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6), #menu .icon-button:nth-child(7) { color: #d44729; } ==> ./monkeytype/static/themes/nebula.css <== :root { --bg-color: #212135; --main-color: #be3c88; --caret-color: #78c729; --sub-color: #19b3b8; --text-color: #838686; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/alpine.css <== :root { --bg-color: #6c687f; /*Background*/ --main-color: #ffffff; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ --caret-color: #585568; /*Cursor Color*/ --sub-color: #9994b8; /*WPM text color of scrollbar and general color, before typed color*/ --text-color: #ffffff; /*Color of text after hovering over it*/ --error-color: #e32b2b; --error-extra-color: #a62626; --colorful-error-color: #e32b2b; --colorful-error-extra-color: #a62626; } ==> ./monkeytype/static/themes/rgb.css <== :root { --bg-color: #111; --main-color: #eee; --caret-color: #eee; --sub-color: #444; --text-color: #eee; --error-color: #eee; --error-extra-color: #b3b3b3; --colorful-error-color: #eee; --colorful-error-extra-color: #b3b3b3; } @keyframes rgb { 0% { color: #f44336; } 25% { color: #ffc107; } 50% { color: #4caf50; } 75% { color: #3f51b5; } 100% { color: #f44336; } } @keyframes rgb-bg { 0% { background: #f44336; } 25% { background: #ffc107; } 50% { background: #4caf50; } 75% { background: #3f51b5; } 100% { background: #f44336; } } .button.discord::after, #caret, .pageSettings .section .buttons .button.active, .pageSettings .section.languages .buttons .language.active, .pageAccount .group.filterButtons .buttons .button.active { animation: rgb-bg 5s linear infinite; } #top.focus .button.discord::after, #top .button.discord.dotHidden::after { animation-name: none !important; } .logo .bottom, #top .config .group .buttons .text-button.active, #result .stats .group .bottom, #menu .icon-button:hover, #top .config .group .buttons .text-button:hover, a:hover, #words.flipped .word { animation: rgb 5s linear infinite; } /* .word letter.correct{ animation: rgb 5s linear infinite; } */ #words.flipped .word letter.correct { color: var(--sub-color); } #words:not(.flipped) .word letter.correct { animation: rgb 5s linear infinite; } ==> ./monkeytype/static/themes/hanok.css <== :root { --bg-color: #d8d2c3; --main-color: #513a2a; --caret-color: #513a2a; --sub-color: #513a2a; --text-color: #393b3b; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/serika_dark.css <== :root { --bg-color: #323437; --main-color: #e2b714; --caret-color: #e2b714; --sub-color: #646669; --text-color: #d1d0c5; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/modern_ink.css <== :root { --bg-color: #ffffff; --main-color: #ff360d; --caret-color: #ff0000; --sub-color: #b7b7b7; --text-color: #000000; --error-color: #d70000; --error-extra-color: #b00000; --colorful-error-color: #ff1c1c; --colorful-error-extra-color: #b00000; } #menu .icon-button:nth-child(1) { color: #ff0000; } #menu .icon-button:nth-child(5) { color: #ff0000; } /* kinda confusing to type with this */ /* .word letter.incorrect { -webkit-transform: scale(0.5) translate(-100%, -100%); } .word letter.incorrect.extra { -webkit-transform: scale(0.5); } */ .word.error { border-bottom: solid 2px #ff0000; } ==> ./monkeytype/static/themes/muted.css <== :root { --bg-color: #525252; --main-color: #C5B4E3; --caret-color: #B1E4E3; --sub-color: #939eae; --text-color: #B1E4E3; --error-color: #EDC1CD; } ==> ./monkeytype/static/themes/nautilus.css <== :root { --bg-color: #132237; --main-color: #ebb723; --caret-color: #ebb723; --sub-color: #0b4c6c; --text-color: #1cbaac; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } ==> ./monkeytype/static/themes/miami_nights.css <== :root { --bg-color: #18181a; --main-color: #e4609b; --caret-color: #e4609b; --sub-color: #47bac0; --text-color: #fff; --error-color: #fff591; --error-extra-color: #b6af68; --colorful-error-color: #fff591; --colorful-error-extra-color: #b6af68; } ==> ./monkeytype/static/themes/moonlight.css <== /*inspired by GMK MOONLIGHT*/ :root { --bg-color: #1f2730; --main-color: #c69f68; --caret-color: #8f744b; --sub-color: #4b5975; --text-color: #ccccb5; --error-color: #b81b2c; --error-extra-color: #84131f; --colorful-error-color: #b81b2c; --colorful-error-extra-color: #84131f; } #menu { gap: 0.5rem; } #top.focus #menu .icon-button, #top.focus #menu:before, #top.focus #menu:after { background: var(--bg-color); } #menu .icon-button { border-radius: rem !important; color: #1f2730 !important; } #menu .icon-button :hover { border-radius: rem !important; color: #4b5975 !important; transition: 0.25s; } #menu .icon-button:nth-child(1) { background: #c69f68; } #menu .icon-button:nth-child(2) { background: #c69f68; } #menu .icon-button:nth-child(3) { background: #c69f68; } #menu .icon-button:nth-child(4) { background: #c69f68; } #menu .icon-button:nth-child(5) { background: #c69f68; } #menu .icon-button:nth-child(6), #menu .icon-button:nth-child(7) { background: #c69f68; } #top.focus #menu .icon-button.discord::after { border-color: transparent; } ==> ./monkeytype/static/themes/darling.css <== :root { --bg-color: #fec8cd; --main-color: #ffffff; --caret-color: #ffffff; --sub-color: #a30000; --text-color: #ffffff; --error-color: #2e7dde; --error-extra-color: #2e7dde; --colorful-error-color: #2e7dde; --colorful-error-extra-color: #2e7dde; --font: Roboto Mono; } ==> ./monkeytype/static/themes/pastel.css <== :root { --bg-color: #e0b2bd; --main-color: #fbf4b6; --caret-color: #fbf4b6; --sub-color: #b4e9ff; --text-color: #6d5c6f; --error-color: #ff6961; --error-extra-color: #c23b22; --colorful-error-color: #ff6961; --colorful-error-extra-color: #c23b22; } ==> ./monkeytype/static/themes/dark.css <== :root { --bg-color: #111; --main-color: #eee; --caret-color: #eee; --sub-color: #444; --text-color: #eee; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } ==> ./monkeytype/static/themes/ishtar.css <== :root { --bg-color: #202020; --main-color: #91170c; --caret-color: #c58940; --sub-color: #847869; --text-color: #fae1c3; --error-color: #bb1e10; --error-extra-color: #791717; --colorful-error-color: #c5da33; --colorful-error-extra-color: #849224; } #top .logo .bottom { color: #fae1c3; } ==> ./monkeytype/static/themes/retro.css <== :root { --bg-color: #dad3c1; --main-color: #1d1b17; --caret-color: #1d1b17; --sub-color: #918b7d; --text-color: #1d1b17; --error-color: #bf616a; --error-extra-color: #793e44; --colorful-error-color: #bf616a; --colorful-error-extra-color: #793e44; } ==> ./monkeytype/static/themes/stealth.css <== :root { --bg-color: #010203; --main-color: #383e42; --caret-color: #e25303; --sub-color: #5e676e; --text-color: #383e42; --error-color: #e25303; --error-extra-color: #73280c; --colorful-error-color: #e25303; --colorful-error-extra-color: #73280c; } #menu .icon-button:nth-child(4) { color: #e25303; } #timerNumber { color: #5e676e; } ==> ./monkeytype/static/themes/repose_dark.css <== :root { --bg-color: #2F3338; --main-color: #D6D2BC; --caret-color: #D6D2BC; --sub-color: #8F8E84; --text-color: #D6D2BC; --error-color: #FF4A59; --error-extra-color: #C43C53; --colorful-error-color: #FF4A59; --colorful-error-extra-color: #C43C53; } ==> ./monkeytype/static/themes/lime.css <== :root { --bg-color: #7c878e; --main-color: #93c247; --caret-color: #93c247; --sub-color: #4b5257; --text-color: #bfcfdc; --error-color: #ea4221; --error-extra-color: #7e2a33; --colorful-error-color: #ea4221; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/blueberry_light.css <== :root { --bg-color: #dae0f5; --main-color: #506477; --caret-color: #df4576; --sub-color: #92a4be; --text-color: #678198; --error-color: #df4576; --error-extra-color: #d996ac; --colorful-error-color: #df4576; --colorful-error-extra-color: #d996ac; } #top .logo .bottom { color: #df4576; } ==> ./monkeytype/static/themes/fruit_chew.css <== :root { --bg-color: #d6d3d6; --main-color: #5c1e5f; --caret-color: #b92221; --sub-color: #b49cb5; --text-color: #282528; --error-color: #bd2621; --error-extra-color: #a62626; --colorful-error-color: #bd2621; --colorful-error-extra-color: #a62626; } #menu .icon-button:nth-child(1) { color: #a6bf50; } #menu .icon-button:nth-child(2) { color: #c3921a; } #menu .icon-button:nth-child(3) { color: #b92221; } #menu .icon-button:nth-child(4) { color: #88b6ce; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #661968; } ==> ./monkeytype/static/themes/olivia.css <== :root { --bg-color: #1c1b1d; --main-color: #deaf9d; --caret-color: #deaf9d; --sub-color: #4e3e3e; --text-color: #f2efed; --error-color: #bf616a; --error-extra-color: #793e44; --colorful-error-color: #e03d4e; --colorful-error-extra-color: #aa2f3b; } ==> ./monkeytype/static/themes/diner.css <== :root { --bg-color: #537997; --main-color: #c3af5b; --caret-color: #ad5145; --sub-color: #445c7f; --text-color: #dfdbc8; --error-color: #ad5145; --error-extra-color: #7e2a33; --colorful-error-color: #ad5145; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/sewing_tin_light.css <== :root { --bg-color: #ffffff; --main-color: #2d2076; --caret-color: #fbdb8c; --sub-color: #385eca; --text-color: #2d2076; --error-color: #f2ce83; --error-extra-color: #f2ce83; --colorful-error-color: #f2ce83; --colorful-error-extra-color: #f2ce83; } #menu .icon-button { color: #f2ce83; } #menu .icon-button:hover { color: #c6915e; } #top .logo .text { background-color: #ffffff; /* fallback */ background: -webkit-linear-gradient( #2d2076, #2d2076 25%, #2e3395 25%, #2e3395 50%, #3049ba 50%, #3049ba 75%, #385eca 75%, #385eca ); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; } #top .logo .text .top { /* prevent it from being transparent */ -webkit-text-fill-color: #385eca; } ==> ./monkeytype/static/themes/carbon.css <== :root { --bg-color: #313131; --main-color: #f66e0d; --caret-color: #f66e0d; --sub-color: #616161; --text-color: #f5e6c8; --error-color: #e72d2d; --error-extra-color: #7e2a33; --colorful-error-color: #e72d2d; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/cafe.css <== :root { --bg-color: #ceb18d; --main-color: #14120f; --caret-color: #14120f; --sub-color: #d4d2d1; --text-color: #14120f; --error-color: #c82931; --error-extra-color: #ac1823; --colorful-error-color: #c82931; --colorful-error-extra-color: #ac1823; } ==> ./monkeytype/static/themes/future_funk.css <== :root { --bg-color: #2e1a47; --main-color: #f7f2ea; --caret-color: #f7f2ea; --sub-color: #c18fff; --text-color: #f7f2ea; --error-color: #f04e98; --error-extra-color: #bd1c66; --colorful-error-color: #f04e98; --colorful-error-extra-color: #bd1c66; } #menu .icon-button:nth-child(1) { color: #f04e98; } #menu .icon-button:nth-child(2) { color: #f8bed6; } #menu .icon-button:nth-child(3) { color: #f6eb61; } #menu .icon-button:nth-child(4) { color: #a4dbe8; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #a266ed; } ==> ./monkeytype/static/themes/fire.css <== :root { --bg-color: #0f0000; --main-color: #b31313; --caret-color: #b31313; --sub-color: #683434; --text-color: #ffffff; --error-color: #2f3cb6; --error-extra-color: #434a8f; --colorful-error-color: #2f3cb6; --colorful-error-extra-color: #434a8f; } @keyframes rgb { 0% { color: #b31313; } 25% { color: #ff9000; } 50% { color: #fdda16; } 75% { color: #ff9000; } 100% { color: #b31313; } } @keyframes rgb-bg { 0% { background: #b31313; } 25% { background: #ff9000; } 50% { background: #fdda16; } 75% { background: #ff9000; } 100% { background: #b31313; } } .button.discord::after, #caret, .pageSettings .section .buttons .button.active, .pageSettings .section.languages .buttons .language.active, .pageAccount .group.filterButtons .buttons .button.active { animation: rgb-bg 5s linear infinite; } #top.focus .button.discord::after, #top .button.discord.dotHidden::after { animation-name: none !important; } .logo .bottom, #top .config .group .buttons .text-button.active, #result .stats .group .bottom, #menu .icon-button:hover, #top .config .group .buttons .text-button:hover, a:hover, #words.flipped .word { animation: rgb 5s linear infinite; } #words.flipped .word letter.correct { color: var(--sub-color); } #words:not(.flipped) .word letter.correct { animation: rgb 5s linear infinite; } ==> ./monkeytype/static/themes/striker.css <== :root { --bg-color: #124883; --main-color: #d7dcda; --caret-color: #d7dcda; --sub-color: #0f2d4e; --text-color: #d6dbd9; --error-color: #fb4934; --error-extra-color: #cc241d; --colorful-error-color: #fb4934; --colorful-error-extra-color: #cc241d; } ==> ./monkeytype/static/themes/monokai.css <== :root { --bg-color: #272822; --main-color: #a6e22e; --caret-color: #66d9ef; --sub-color: #e6db74; --text-color: #e2e2dc; --error-color: #f92672; --error-extra-color: #fd971f; --colorful-error-color: #f92672; --colorful-error-extra-color: #fd971f; } ==> ./monkeytype/static/themes/8008.css <== :root { --bg-color: #333a45; --main-color: #f44c7f; --caret-color: #f44c7f; --sub-color: #939eae; --text-color: #e9ecf0; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #c5da33; --colorful-error-extra-color: #849224; } ==> ./monkeytype/static/themes/froyo.css <== :root { --bg-color: #e1dacb; --main-color: #7b7d7d; --caret-color: #7b7d7d; --sub-color: #b29c5e; --text-color: #7b7d7d; --error-color: #f28578; --error-extra-color: #d56558; --colorful-error-color: #f28578; --colorful-error-extra-color: #d56558; } #menu .icon-button:nth-child(1) { color: #ff7e73; } #menu .icon-button:nth-child(2) { color: #f5c370; } #menu .icon-button:nth-child(3) { color: #08d9a3; } #menu .icon-button:nth-child(4) { color: #0ca5e2; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #875ac6; } ==> ./monkeytype/static/themes/evil_eye.css <== :root { --bg-color: #0084c2; --main-color: #f7f2ea; --caret-color: #f7f2ea; --sub-color: #01589f; --text-color: #171718; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/deku.css <== :root { --bg-color: #058b8c; --main-color: #b63530; --caret-color: #b63530; --sub-color: #255458; --text-color: #f7f2ea; --error-color: #b63530; --error-extra-color: #530e0e; --colorful-error-color: #ddca1f; --colorful-error-extra-color: #8f8610; } ==> ./monkeytype/static/themes/alduin.css <== :root { --bg-color: #1c1c1c; --main-color: #dfd7af; --caret-color: #e3e3e3; --sub-color: #444444; --text-color: #f5f3ed; --error-color: #af5f5f; --error-extra-color: #4d2113; --colorful-error-color: #af5f5f; --colorful-error-extra-color: #4d2113; } ==> ./monkeytype/static/themes/dracula.css <== :root { --bg-color: #282a36; --main-color: #f2f2f2; --caret-color: #f2f2f2; --sub-color: #bd93f9; --text-color: #f2f2f2; --error-color: #f758a0; --error-extra-color: #732e51; --colorful-error-color: #f758a0; --colorful-error-extra-color: #732e51; } #menu .icon-button:nth-child(1) { color: #ec75c4; } #menu .icon-button:nth-child(2) { color: #8be9fd; } #menu .icon-button:nth-child(3) { color: #50fa7b; } #menu .icon-button:nth-child(4) { color: #f1fa8c; } #menu .icon-button:nth-child(5) { color: #ffb86c; } #menu .icon-button:nth-child(6) { color: #ffb86c; } ==> ./monkeytype/static/themes/metaverse.css <== :root { --bg-color: #232323; --main-color: #d82934; --caret-color: #d82934; --sub-color: #5e5e5e; --text-color: #e8e8e8; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #d7da33; --colorful-error-extra-color: #737917; } ==> ./monkeytype/static/themes/hammerhead.css <== :root { --bg-color: #030613; --main-color: #4fcdb9; --caret-color: #4fcdb9; --sub-color: #1e283a; --text-color: #e2f1f5; --error-color: #e32b2b; --error-extra-color: #a62626; --colorful-error-color: #e32b2b; --colorful-error-extra-color: #a62626; } ==> ./monkeytype/static/themes/bushido.css <== :root { --bg-color: #242933; --main-color: #ec4c56; --caret-color: #ec4c56; --sub-color: #596172; --text-color: #f6f0e9; --error-color: #ec4c56; --error-extra-color: #9b333a; --colorful-error-color: #ecdc4c; --colorful-error-extra-color: #bdb03d; } ==> ./monkeytype/static/themes/matcha_moccha.css <== :root { --bg-color: #523525; --main-color: #7ec160; --caret-color: #7ec160; --sub-color: #9e6749; --text-color: #ecddcc; --error-color: #fb4934; --error-extra-color: #cc241d; --colorful-error-color: #fb4934; --colorful-error-extra-color: #cc241d; } ==> ./monkeytype/static/themes/modern_dolch.css <== :root { --bg-color: #2d2e30; --main-color: #7eddd3; --caret-color: #7eddd3; --sub-color: #54585c; --text-color: #e3e6eb; --error-color: #d36a7b; --error-extra-color: #994154; --colorful-error-color: #d36a7b; --colorful-error-extra-color: #994154; } ==> ./monkeytype/static/themes/creamsicle.css <== :root { --bg-color: #ff9869; --main-color: #fcfcf8; --caret-color: #fcfcf8; --sub-color: #ff661f; --text-color: #fcfcf8; --error-color: #6a0dad; --error-extra-color: #6a0dad; --colorful-error-color: #6a0dad; --colorful-error-extra-color: #6a0dad; } ==> ./monkeytype/static/themes/strawberry.css <== :root { --bg-color: #f37f83; --main-color: #fcfcf8; --caret-color: #fcfcf8; --sub-color: #e53c58; --text-color: #fcfcf8; --error-color: #fcd23f; --error-extra-color: #d7ae1e; --colorful-error-color: #fcd23f; --colorful-error-extra-color: #d7ae1e; } ==> ./monkeytype/static/themes/shadow.css <== :root { --bg-color: #000; --main-color: #eee; --caret-color: #eee; --sub-color: #444; --text-color: #eee; --error-color: #fff; --error-extra-color: #d8d8d8; --colorful-error-color: #fff; --colorful-error-extra-color: #d8d8d8; } #top .logo .icon{ color: #8C3230; } #top .logo .text{ color: #557D8D; } @keyframes shadow { to { color: #000; } } @keyframes shadow-repeat { 50% { color: #000; } 100% { color: #eee; } } #liveWpm, #timerNumber { color: white; } #top .config .group .buttons .text-button.active, #result .stats .group, #menu .icon-button:hover, #top .config .group .buttons .text-button:hover, a:hover { animation: shadow-repeat 3s linear infinite forwards; } #logo, #typingTest .word letter.correct { animation: shadow 5s linear 1 forwards; } ==> ./monkeytype/static/themes/bento.css <== :root { --bg-color: #2d394d; --main-color: #ff7a90; --caret-color: #ff7a90; --sub-color: #4a768d; --text-color: #fffaf8; --error-color: #ee2a3a; --error-extra-color: #f04040; --colorful-error-color: #fc2032; --colorful-error-extra-color: #f04040; } ==> ./monkeytype/static/themes/menthol.css <== :root { --bg-color: #00c18c; --main-color: #ffffff; --caret-color: #99fdd8; --sub-color: #186544; --text-color: #ffffff; --error-color: #e03c3c; --error-extra-color: #b12525; --colorful-error-color: #e03c3c; --colorful-error-extra-color: #b12525; } ==> ./monkeytype/static/themes/our_theme.css <== :root { --bg-color: #ce1226; --main-color: #fcd116; --caret-color: #fcd116; --sub-color: #6d0f19; --text-color: #ffffff; --error-color: #fcd116; --error-extra-color: #fcd116; --colorful-error-color: #1672fc; --colorful-error-extra-color: #1672fc; } ==> ./monkeytype/static/themes/mr_sleeves.css <== :root { --bg-color: #d1d7da; --main-color: #daa99b; --caret-color: #8fadc9; --sub-color: #9a9fa1; --text-color: #1d1d1d; --error-color: #bf6464; --error-extra-color: #793e44; --colorful-error-color: #8fadc9; --colorful-error-extra-color: #667c91; } #top .logo .bottom { color: #8fadc9; } #top .config .group .buttons .text-button.active { color: #daa99b; } /* #menu .icon-button:nth-child(1){ color: #daa99b; } #menu .icon-button:nth-child(2){ color: #daa99b; } #menu .icon-button:nth-child(3){ color: #8fadc9; } #menu .icon-button:nth-child(4), #menu .icon-button:nth-child(5){ color: #8fadc9; } */ ==> ./monkeytype/static/themes/retrocast.css <== :root { --bg-color: #07737a; --main-color: #88dbdf; --caret-color: #88dbdf; --sub-color: #f3e03b; --text-color: #ffffff; --error-color: #ff585d; --error-extra-color: #c04455; --colorful-error-color: #ff585d; --colorful-error-extra-color: #c04455; } #menu .icon-button:nth-child(1) { color: #88dbdf; } #menu .icon-button:nth-child(2) { color: #88dbdf; } #menu .icon-button:nth-child(3) { color: #88dbdf; } #menu .icon-button:nth-child(4) { color: #ff585d; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #f3e03b; } ==> ./monkeytype/static/themes/frozen_llama.css <== :root { --bg-color: #9bf2ea; --main-color: #6d44a6; --caret-color: #ffffff; --sub-color: #b690fd; --text-color: #ffffff; --error-color: #e42629; --error-extra-color: #e42629; --colorful-error-color: #e42629; --colorful-error-extra-color: #e42629; } ==> ./monkeytype/static/themes/solarized_light.css <== :root { --bg-color: #fdf6e3; --main-color: #859900; --caret-color: #dc322f; --sub-color: #2aa198; --text-color: #181819; --error-color: #d33682; --error-extra-color: #9b225c; --colorful-error-color: #d33682; --colorful-error-extra-color: #9b225c; } ==> ./monkeytype/static/themes/bliss.css <== :root { --bg-color: #262727; --main-color: #f0d3c9; --caret-color: #f0d3c9; --sub-color: #665957; --text-color: #fff; --error-color: #bd4141; --error-extra-color: #883434; --colorful-error-color: #bd4141; --colorful-error-extra-color: #883434; } ==> ./monkeytype/static/themes/repose_light.css <== :root { --bg-color: #EFEAD0; --main-color: #5F605E; --caret-color: #5F605E; --sub-color: #8F8E84; --text-color: #333538; --error-color: #C43C53; --error-extra-color: #A52632; --colorful-error-color: #C43C53; --colorful-error-extra-color: #A52632; } ==> ./monkeytype/static/themes/fundamentals.css <== :root { --bg-color: #727474; --main-color: #7fa482; --caret-color: #196378; --sub-color: #cac4be; --text-color: #131313; --error-color: #5e477c; --error-extra-color: #413157; --colorful-error-color: #5e477c; --colorful-error-extra-color: #413157; } #top .logo .bottom { color: #196378; } ==> ./monkeytype/static/themes/miami.css <== :root { --bg-color: #f35588; --main-color: #05dfd7; --caret-color: #a3f7bf; --text-color: #f0e9ec; --sub-color: #94294c; --error-color: #fff591; --error-extra-color: #b9b269; --colorful-error-color: #fff591; --colorful-error-extra-color: #b9b269; } ==> ./monkeytype/static/themes/sewing_tin.css <== :root { --bg-color: #241963; --main-color: #f2ce83; --caret-color: #fbdb8c; --sub-color: #446ad5; --text-color: #ffffff; --error-color: #c6915e; --error-extra-color: #c6915e; --colorful-error-color: #c6915e; --colorful-error-extra-color: #c6915e; } #menu .icon-button { color: #f2ce83; } #menu .icon-button:hover { color: #c6915e; } ==> ./monkeytype/static/themes/fleuriste.css <== :root { --bg-color: #c6b294; --main-color: #405a52; --caret-color: #8a785b; --sub-color: #64374d; --text-color: #091914; --error-color: #990000; --error-extra-color: #8a1414; --colorful-error-color: #a63a3a; --colorful-error-extra-color: #bd4c4c; } #menu .icon-button:nth-child(1, 3, 5) { background: #405a52; } #menu .icon-button:nth-child(2, 4) { background: #64374d; } ==> ./monkeytype/static/themes/rose_pine.css <== :root { --bg-color: #1f1d27; /*Background*/ --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ --caret-color: #f6c177; /*Cursor Color*/ --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ --text-color: #e0def4; /*Color of text after hovering over it*/ --error-color: #eb6f92; --error-extra-color: #ebbcba; --colorful-error-color: #eb6f92; --colorful-error-extra-color: #ebbcba; } ==> ./monkeytype/static/themes/sonokai.css <== :root { --bg-color: #2c2e34; --main-color: #9ed072; --caret-color: #f38c71; --sub-color: #e7c664; --text-color: #e2e2e3; --error-color: #fc5d7c; --error-extra-color: #ecac6a; --colorful-error-color: #fc5d7c; --colorful-error-extra-color: #ecac6a; } ==> ./monkeytype/static/themes/trackday.css <== :root { --bg-color: #464d66; --main-color: #e0513e; --caret-color: #475782; --sub-color: #5c7eb9; --text-color: #cfcfcf; --error-color: #e44e4e; --error-extra-color: #fd3f3f; --colorful-error-color: #ff2e2e; --colorful-error-extra-color: #bb2525; } #menu .icon-button:nth-child(1) { color: #e0513e; } #menu .icon-button:nth-child(3) { color: #cfcfcf; } #menu .icon-button:nth-child(2) { color: #ccc500; } ==> ./monkeytype/static/themes/milkshake.css <== :root { --bg-color: #ffffff; --main-color: #212b43; --caret-color: #212b43; --sub-color: #62cfe6; --text-color: #212b43; --error-color: #f19dac; --error-extra-color: #e58c9d; --colorful-error-color: #f19dac; --colorful-error-extra-color: #e58c9d; } #menu .icon-button:nth-child(1) { color: #f19dac; } #menu .icon-button:nth-child(2) { color: #f6f4a0; } #menu .icon-button:nth-child(3) { color: #73e4d0; } #menu .icon-button:nth-child(4) { color: #61cfe6; } #menu .icon-button:nth-child(5) { color: #ba96db; } #menu .icon-button:nth-child(6) { color: #ba96db; } ==> ./monkeytype/static/themes/trance.css <== :root { --bg-color: #00021b; --main-color: #e51376; --caret-color: #e51376; --sub-color: #3c4c79; --text-color: #fff; --error-color: #02d3b0; --error-extra-color: #3f887c; --colorful-error-color: #02d3b0; --colorful-error-extra-color: #3f887c; } @keyframes rgb { 0% { color: #e51376; } 50% { color: #0e77ee; } 100% { color: #e51376; } } @keyframes rgb-bg { 0% { background: #e51376; } 50% { background: #0e77ee; } 100% { background: #e51376; } } .button.discord::after, #caret, .pageSettings .section .buttons .button.active, .pageSettings .section.languages .buttons .language.active, .pageAccount .group.filterButtons .buttons .button.active { animation: rgb-bg 5s linear infinite; } #top.focus .button.discord::after, #top .button.discord.dotHidden::after { animation-name: none !important; } .logo .bottom, #top .config .group .buttons .text-button.active, #result .stats .group .bottom, #menu .icon-button:hover, #top .config .group .buttons .text-button:hover, a:hover, #words.flipped .word { animation: rgb 5s linear infinite; } #words.flipped .word letter.correct { color: var(--sub-color); } #words:not(.flipped) .word letter.correct { animation: rgb 5s linear infinite; } ==> ./monkeytype/static/themes/nausea.css <== :root { --bg-color: #323437; --main-color: #e2b714; --caret-color: #e2b714; --sub-color: #646669; --text-color: #d1d0c5; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } @keyframes woah { 0% { transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) scaleY(0.9); } 25% { transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1) scaleY(0.8); } 50% { transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(0.9) scaleY(0.9); } 75% { transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1.5) scaleY(1.1); } 100% { transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) scaleY(0.9); } } @keyframes plsstop { 0% { background: #323437; } 50% { background: #3e4146; } 100% { background: #323437; } } #middle { animation: woah 7s infinite cubic-bezier(0.5, 0, 0.5, 1); } #centerContent { transform: rotate(5deg); perspective: 500px; } body { animation: plsstop 10s infinite cubic-bezier(0.5, 0, 0.5, 1); overflow: hidden; } ==> ./monkeytype/static/themes/superuser.css <== :root { --bg-color: #262a33; --main-color: #43ffaf; --caret-color: #43ffaf; --sub-color: #526777; --text-color: #e5f7ef; --error-color: #ff5f5f; --error-extra-color: #d22a2a; --colorful-error-color: #ff5f5f; --colorful-error-extra-color: #d22a2a; } ==> ./monkeytype/static/themes/serika.css <== :root { --main-color: #e2b714; --caret-color: #e2b714; --sub-color: #aaaeb3; --bg-color: #e1e1e3; --text-color: #323437; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } ==> ./monkeytype/static/themes/gruvbox_dark.css <== :root { --bg-color: #282828; --main-color: #d79921; --caret-color: #fabd2f; --sub-color: #665c54; --text-color: #ebdbb2; --error-color: #fb4934; --error-extra-color: #cc241d; --colorful-error-color: #cc241d; --colorful-error-extra-color: #9d0006; } ==> ./monkeytype/static/themes/godspeed.css <== :root { --bg-color: #eae4cf; --main-color: #9abbcd; --caret-color: #f4d476; --sub-color: #c0bcab; --text-color: #646669; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/joker.css <== :root { --bg-color: #1a0e25; --main-color: #99de1e; --caret-color: #99de1e; --sub-color: #7554a3; --text-color: #e9e2f5; --error-color: #e32b2b; --error-extra-color: #a62626; --colorful-error-color: #e32b2b; --colorful-error-extra-color: #a62626; } ==> ./monkeytype/static/themes/rose_pine_dawn.css <== :root { --bg-color: #fffaf3; /*Background*/ --main-color: #56949f; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ --caret-color: #ea9d34; /*Cursor Color*/ --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ --text-color: #286983; /*Color of text after hovering over it*/ --error-color: #b4637a; --error-extra-color: #d7827e; --colorful-error-color: #b4637a; --colorful-error-extra-color: #d7827e; } ==> ./monkeytype/static/themes/grand_prix.css <== :root { --bg-color: #36475c; --main-color: #c0d036; --caret-color: #c0d036; --sub-color: #5c6c80; --text-color: #c1c7d7; --error-color: #fc5727; --error-extra-color: #fc5727; --colorful-error-color: #fc5727; --colorful-error-extra-color: #fc5727; } ==> ./monkeytype/static/themes/lavender.css <== :root { --bg-color: #ada6c2; --main-color: #e4e3e9; --caret-color: #e4e3e9; --sub-color: #e4e3e9; --text-color: #2f2a41; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } #menu .icon-button { border-radius: 10rem !important; background: #2f2a41; color: #e4e3e9; } #menu .icon-button:hover { color: #ada6c2; } ==> ./monkeytype/static/themes/watermelon.css <== :root { --bg-color: #1f4437; --main-color: #d6686f; --caret-color: #d6686f; --sub-color: #3e7a65; --text-color: #cdc6bc; --error-color: #c82931; --error-extra-color: #ac1823; --colorful-error-color: #c82931; --colorful-error-extra-color: #ac1823; } ==> ./monkeytype/static/themes/copper.css <== :root { --bg-color: #442f29; --main-color: #b46a55; --caret-color: #c25c42; --sub-color: #7ebab5; --text-color: #e7e0de; --error-color: #a32424; --error-extra-color: #ec0909; --colorful-error-color: #a32424; --colorful-error-extra-color: #ec0909; } ==> ./monkeytype/static/themes/beach.css <== :root { --bg-color: #ffeead; --main-color: #96ceb4; --caret-color: #ffcc5c; --sub-color: #ffcc5c; --text-color: #5b7869; --error-color: #ff6f69; --error-extra-color: #ff6f69; --colorful-error-color: #ff6f69; --colorful-error-extra-color: #ff6f69; } #menu .icon-button:nth-child(1), #menu .icon-button:nth-child(2), #menu .icon-button:nth-child(3), #menu .icon-button:nth-child(4), #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #ff6f69; } ==> ./monkeytype/static/themes/pulse.css <== :root { --bg-color: #181818; --main-color: #17b8bd; --caret-color: #17b8bd; --sub-color: #53565a; --text-color: #e5f4f4; --error-color: #da3333; --error-extra-color: #791717; --colorful-error-color: #da3333; --colorful-error-extra-color: #791717; } ==> ./monkeytype/static/themes/drowning.css <== :root { --bg-color: #191826; --main-color: #4a6fb5; --caret-color: #4f85e8; --sub-color: #50688c; --text-color: #9393a7; --error-color: #be555f; --error-extra-color: #7e2a33; --colorful-error-color: #be555f; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/blueberry_dark.css <== :root { --bg-color: #212b42; --main-color: #add7ff; --caret-color: #962f7e; --sub-color: #5c7da5; --text-color: #91b4d5; --error-color: #df4576; --error-extra-color: #d996ac; --colorful-error-color: #df4576; --colorful-error-extra-color: #d996ac; } #top .logo .bottom { color: #962f7e; } ==> ./monkeytype/static/themes/arch.css <== :root { --bg-color: #0c0d11; --main-color: #7ebab5; --caret-color: #7ebab5; --sub-color: #454864; --text-color: #f6f5f5; --error-color: #ff4754; --error-extra-color: #b02a33; --colorful-error-color: #ff4754; --colorful-error-extra-color: #b02a33; } ==> ./monkeytype/static/themes/olive.css <== :root { --bg-color: #e9e5cc; --caret-color: #92946f; --main-color: #92946f; --sub-color: #b7b39e; --text-color: #373731; --error-color: #cf2f2f; --error-extra-color: #a22929; --colorful-error-color: #cf2f2f; --colorful-error-extra-color: #a22929; } ==> ./monkeytype/static/themes/luna.css <== :root { --bg-color: #221c35; --main-color: #f67599; --caret-color: #f67599; --sub-color: #5a3a7e; --text-color: #ffe3eb; --error-color: #efc050; --error-extra-color: #c5972c; --colorful-error-color: #efc050; --colorful-error-extra-color: #c5972c; } ==> ./monkeytype/static/themes/red_samurai.css <== :root { --bg-color: #84202c; --main-color: #c79e6e; --caret-color: #c79e6e; --sub-color: #55131b; --text-color: #e2dad0; --error-color: #33bbda; --error-extra-color: #176b79; --colorful-error-color: #33bbda; --colorful-error-extra-color: #176779; } ==> ./monkeytype/static/themes/cyberspace.css <== :root { --bg-color: #181c18; --main-color: #00ce7c; --caret-color: #00ce7c; --sub-color: #9578d3; --text-color: #c2fbe1; --error-color: #ff5f5f; --error-extra-color: #d22a2a; --colorful-error-color: #ff5f5f; --colorful-error-extra-color: #d22a2a; } ==> ./monkeytype/static/themes/shoko.css <== :root { --bg-color: #ced7e0; --main-color: #81c4dd; --caret-color: #81c4dd; --sub-color: #7599b1; --text-color: #3b4c58; --error-color: #bf616a; --error-extra-color: #793e44; --colorful-error-color: #bf616a; --colorful-error-extra-color: #793e44; } ==> ./monkeytype/static/themes/oblivion.css <== :root { --bg-color: #313231; --main-color: #a5a096; --caret-color: #a5a096; --sub-color: #5d6263; --text-color: #f7f5f1; --error-color: #dd452e; --error-extra-color: #9e3423; --colorful-error-color: #dd452e; --colorful-error-extra-color: #9e3423; } #menu .icon-button:nth-child(1) { color: #9a90b4; } #menu .icon-button:nth-child(2) { color: #8db14b; } #menu .icon-button:nth-child(3) { color: #fca321; } #menu .icon-button:nth-child(4) { color: #2984a5; } #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #dd452e; } ==> ./monkeytype/static/themes/comfy.css <== :root { --bg-color: #4a5b6e; --main-color: #f8cdc6; --caret-color: #9ec1cc; --sub-color: #9ec1cc; --text-color: #f5efee; --error-color: #c9465e; --error-extra-color: #c9465e; --colorful-error-color: #c9465e; --colorful-error-extra-color: #c9465e; } ==> ./monkeytype/static/themes/chaos_theory.css <== :root { --bg-color: #141221; --main-color: #fd77d7; --caret-color: #dde5ed; --text-color: #dde5ed; --error-color: #fd77d7; --sub-color: #676e8a; --error-color: #FF5869; --error-extra-color: #b03c47; --colorful-error-color: #FF5869; --colorful-error-extra-color: #b03c47; } #top .logo .text { -webkit-transform: rotateY(180deg); unicode-bidi: bidi-override; transition: 0.5s; } #top .logo .top { font-family: "Comic Sans MS", "Comic Sans", cursive; } #top .logo .icon { -webkit-transform: rotateX(180deg); transition: 0.5s; } #words .incorrect.extra { -webkit-transform: rotateY(180deg); unicode-bidi: bidi-override; direction: rtl; } #bottom .leftright .right .current-theme .text { /* font-family: "Comic Sans MS", "Comic Sans", cursive; */ } #caret { background-image: url(https://i.imgur.com/yN31JmJ.png); background-color: transparent; background-size: 1rem; background-position: center; background-repeat: no-repeat; } #caret.default { width: 4px; } .config .toggleButton { -webkit-transform: rotateY(180deg); unicode-bidi: bidi-override; direction: rtl; align-content: right; } .config .mode .text-button { -webkit-transform: rotateY(180deg); unicode-bidi: bidi-override; direction: rtl; align-content: right; } .config .wordCount .text-button, .config .time .text-button, .config .quoteLength .text-button, .config .customText .text-button { -webkit-transform: rotateY(180deg); unicode-bidi: bidi-override; direction: rtl; align-content: right; } #top.focus #menu .icon-button, #top.focus #menu:before, #top.focus #menu:after { background: var(--sub-color); -webkit-transform: rotateY(180deg) !important; } #top.focus .logo .text, #top.focus .logo:before, #top.focus .logo:after { -webkit-transform: rotateY(0deg); direction: ltr; } #top.focus .logo .icon, #top.focus .logo:before, #top.focus .logo:after { -webkit-transform: rotateX(0deg); direction: ltr; } #bottom .leftright .right .current-theme:hover .fas.fa-fw.fa-palette { -webkit-transform: rotateY(180deg); transition: 0.5s; } #menu { gap: 0.5rem; } #menu .icon-button { border-radius: 10rem i !important; color: var(--bg-color); transition: 0.5s; } #menu .icon-button:nth-child(1) { background: #ab92e1; } #menu .icon-button:nth-child(2) { background: #f3ea5d; } #menu .icon-button:nth-child(3) { background: #7ae1bf; } #menu .icon-button:nth-child(4) { background: #ff5869; } #menu .icon-button:nth-child(5) { background: #fc76d9; } #menu .icon-button:nth-child(6) { background: #fc76d9; } ==> ./monkeytype/static/themes/bouquet.css <== :root { --bg-color: #173f35; --main-color: #eaa09c; --caret-color: #eaa09c; --sub-color: #408e7b; --text-color: #e9e0d2; --error-color: #d44729; --error-extra-color: #8f2f19; --colorful-error-color: #d44729; --colorful-error-extra-color: #8f2f19; } ==> ./monkeytype/static/themes/ryujinscales.css <== :root { --bg-color: #081426; --main-color: #f17754; --caret-color: #ef6d49; --sub-color: #ffbc90; --text-color: #ffe4bc; --error-color: #ca4754; --error-extra-color: #7e2a33; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } /* your theme has been added to the _list file and the textColor property is the theme's main color */ ==> ./monkeytype/static/themes/graen.css <== :root { --bg-color: #303c36; --main-color: #a59682; --caret-color: #601420; --sub-color: #181d1a; --text-color: #a59682; --error-color: #601420; --error-extra-color: #5f0715; --colorful-error-color: #601420; --colorful-error-extra-color: #5f0715; } #menu .icon-button:nth-child(1), #menu .icon-button:nth-child(2), #menu .icon-button:nth-child(3), #menu .icon-button:nth-child(4), #menu .icon-button:nth-child(5), #menu .icon-button:nth-child(6) { color: #601420; } ==> ./monkeytype/static/themes/mountain.css <== :root { --bg-color: #0f0f0f; --main-color: #e7e7e7; --caret-color: #f5f5f5; --sub-color: #4c4c4c; --text-color: #e7e7e7; --error-color: #ac8c8c; --error-extra-color: #c49ea0; --colorful-error-color: #aca98a; --colorful-error-extra-color: #c4c19e; } ==> ./monkeytype/static/themes/voc.css <== :root { --bg-color: #190618; --main-color: #e0caac; --caret-color: #e0caac; --sub-color: #4c1e48; --text-color: #eeeae4; --error-color: #af3735; --error-extra-color: #7e2a29; --colorful-error-color: #af3735; --colorful-error-extra-color: #7e2a29; } ==> ./monkeytype/static/themes/norse.css <== :root { --bg-color: #242425; --main-color: #2b5f6d; --caret-color: #2b5f6d; --sub-color: #505b5e; --text-color: #ccc2b1; --error-color: #7e2a2a; --error-extra-color: #771d1d; --colorful-error-color: #ca4754; --colorful-error-extra-color: #7e2a33; } ==> ./monkeytype/static/themes/rose_pine_moon.css <== :root { --bg-color: #2a273f; /*Background*/ --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ --caret-color: #f6c177; /*Cursor Color*/ --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ --text-color: #e0def4; /*Color of text after hovering over it*/ --error-color: #eb6f92; --error-extra-color: #ebbcba; --colorful-error-color: #eb6f92; --colorful-error-extra-color: #ebbcba; } ==> ./monkeytype/static/themes/80s_after_dark.css <== :root { --bg-color: #1b1d36; --main-color: #fca6d1; --caret-color: #99d6ea; --sub-color: #99d6ea; --text-color: #e1e7ec; --error-color: #fffb85; --error-extra-color: #fffb85; --colorful-error-color: #fffb85; --colorful-error-extra-color: #fffb85; } ==> ./monkeytype/static/themes/peaches.css <== :root { --bg-color: #e0d7c1; --main-color: #dd7a5f; --caret-color: #dd7a5f; --sub-color: #e7b28e; --text-color: #5f4c41; --error-color: #ff6961; --error-extra-color: #c23b22; --colorful-error-color: #ff6961; --colorful-error-extra-color: #c23b22; } ==> ./monkeytype/static/sw.js <== const staticCacheName = "sw-cache"; // this is given a unique name on build self.addEventListener("activate", (event) => { caches.keys().then((names) => { for (let name of names) { if (name !== staticCacheName) event.waitUntil(caches.delete(name)); } }); event.waitUntil(self.clients.claim()); }); self.addEventListener("install", (event) => { event.waitUntil(self.skipWaiting()); event.waitUntil( caches.open(staticCacheName).then((cache) => { // Cache the base file(s) return cache.add("/"); }) ); }); self.addEventListener("fetch", async (event) => { const host = new URL(event.request.url).host; if ( [ "localhost:5005", "api.monkeytype.com", "api.github.com", "www.google-analytics.com", ].includes(host) || host.endsWith("wikipedia.org") ) { // if hostname is a non-static api, fetch request event.respondWith(fetch(event.request)); } else { // Otherwise, assume host is serving a static file, check cache and add response to cache if not found event.respondWith( caches.open(staticCacheName).then((cache) => { return cache.match(event.request).then(async (response) => { // Check if request in cache if (response) { // if response was found in the cache, send from cache return response; } else { // if response was not found in cache fetch from server, cache it and send it response = await fetch(event.request); cache.put(event.request.url, response.clone()); return response; } }); }) ); } }); ==> ./monkeytype/.nvmrc <== 14.18.1 ==> ./monkeytype/.prettierrc <== { "tabWidth": 2, "useTabs": false, "htmlWhitespaceSensitivity": "ignore" } ==> ./monkeytype/gulpfile.js <== const { task, src, dest, series, watch } = require("gulp"); const axios = require("axios"); const browserify = require("browserify"); const babelify = require("babelify"); const concat = require("gulp-concat"); const del = require("del"); const source = require("vinyl-source-stream"); const buffer = require("vinyl-buffer"); const vinylPaths = require("vinyl-paths"); const eslint = require("gulp-eslint"); var sass = require("gulp-sass")(require("dart-sass")); const replace = require("gulp-replace"); const uglify = require("gulp-uglify"); // sass.compiler = require("dart-sass"); let eslintConfig = { parser: "babel-eslint", globals: [ "jQuery", "$", "firebase", "moment", "html2canvas", "ClipboardItem", "grecaptcha", ], envs: ["es6", "browser", "node"], plugins: ["json"], extends: ["plugin:json/recommended"], rules: { "json/*": ["error"], "constructor-super": "error", "for-direction": "error", "getter-return": "error", "no-async-promise-executor": "error", "no-case-declarations": "error", "no-class-assign": "error", "no-compare-neg-zero": "error", "no-cond-assign": "error", "no-const-assign": "error", "no-constant-condition": "error", "no-control-regex": "error", "no-debugger": "error", "no-delete-var": "error", "no-dupe-args": "error", "no-dupe-class-members": "error", "no-dupe-else-if": "warn", "no-dupe-keys": "error", "no-duplicate-case": "error", "no-empty": ["warn", { allowEmptyCatch: true }], "no-empty-character-class": "error", "no-empty-pattern": "error", "no-ex-assign": "error", "no-extra-boolean-cast": "error", "no-extra-semi": "error", "no-fallthrough": "error", "no-func-assign": "error", "no-global-assign": "error", "no-import-assign": "error", "no-inner-declarations": "error", "no-invalid-regexp": "error", "no-irregular-whitespace": "warn", "no-misleading-character-class": "error", "no-mixed-spaces-and-tabs": "error", "no-new-symbol": "error", "no-obj-calls": "error", "no-octal": "error", "no-prototype-builtins": "error", "no-redeclare": "error", "no-regex-spaces": "error", "no-self-assign": "error", "no-setter-return": "error", "no-shadow-restricted-names": "error", "no-sparse-arrays": "error", "no-this-before-super": "error", "no-undef": "error", "no-unexpected-multiline": "warn", "no-unreachable": "error", "no-unsafe-finally": "error", "no-unsafe-negation": "error", "no-unused-labels": "error", "no-unused-vars": ["warn", { argsIgnorePattern: "e|event" }], "no-use-before-define": "warn", "no-useless-catch": "error", "no-useless-escape": "error", "no-with": "error", "require-yield": "error", "use-isnan": "error", "valid-typeof": "error", }, }; //refactored files, which should be es6 modules //once all files are moved here, then can we use a bundler to its full potential const refactoredSrc = [ "./src/js/axios-instance.js", "./src/js/db.js", "./src/js/misc.js", "./src/js/layouts.js", "./src/js/sound.js", "./src/js/theme-colors.js", "./src/js/chart-controller.js", "./src/js/theme-controller.js", "./src/js/config.js", "./src/js/tag-controller.js", "./src/js/preset-controller.js", "./src/js/ui.js", "./src/js/commandline.js", "./src/js/commandline-lists.js", "./src/js/commandline.js", "./src/js/challenge-controller.js", "./src/js/mini-result-chart.js", "./src/js/account-controller.js", "./src/js/simple-popups.js", "./src/js/settings.js", "./src/js/input-controller.js", "./src/js/route-controller.js", "./src/js/ready.js", "./src/js/monkey-power.js", "./src/js/account/all-time-stats.js", "./src/js/account/pb-tables.js", "./src/js/account/result-filters.js", "./src/js/account/verification-controller.js", "./src/js/account.js", "./src/js/elements/monkey.js", "./src/js/elements/notifications.js", "./src/js/elements/leaderboards.js", "./src/js/elements/account-button.js", "./src/js/elements/loader.js", "./src/js/elements/sign-out-button.js", "./src/js/elements/about-page.js", "./src/js/elements/psa.js", "./src/js/elements/new-version-notification.js", "./src/js/elements/mobile-test-config.js", "./src/js/elements/loading-page.js", "./src/js/elements/scroll-to-top.js", "./src/js/popups/custom-text-popup.js", "./src/js/popups/pb-tables-popup.js", "./src/js/popups/quote-search-popup.js", "./src/js/popups/quote-submit-popup.js", "./src/js/popups/quote-approve-popup.js", "./src/js/popups/rate-quote-popup.js", "./src/js/popups/version-popup.js", "./src/js/popups/support-popup.js", "./src/js/popups/contact-popup.js", "./src/js/popups/custom-word-amount-popup.js", "./src/js/popups/custom-test-duration-popup.js", "./src/js/popups/word-filter-popup.js", "./src/js/popups/result-tags-popup.js", "./src/js/popups/edit-tags-popup.js", "./src/js/popups/edit-preset-popup.js", "./src/js/popups/custom-theme-popup.js", "./src/js/popups/import-export-settings-popup.js", "./src/js/popups/custom-background-filter.js", "./src/js/settings/language-picker.js", "./src/js/settings/theme-picker.js", "./src/js/settings/settings-group.js", "./src/js/test/custom-text.js", "./src/js/test/british-english.js", "./src/js/test/lazy-mode.js", "./src/js/test/shift-tracker.js", "./src/js/test/out-of-focus.js", "./src/js/test/caret.js", "./src/js/test/manual-restart-tracker.js", "./src/js/test/test-stats.js", "./src/js/test/focus.js", "./src/js/test/practise-words.js", "./src/js/test/test-ui.js", "./src/js/test/keymap.js", "./src/js/test/result.js", "./src/js/test/live-wpm.js", "./src/js/test/caps-warning.js", "./src/js/test/live-acc.js", "./src/js/test/live-burst.js", "./src/js/test/timer-progress.js", "./src/js/test/test-logic.js", "./src/js/test/funbox.js", "./src/js/test/pace-caret.js", "./src/js/test/pb-crown.js", "./src/js/test/test-timer.js", "./src/js/test/test-config.js", "./src/js/test/layout-emulator.js", "./src/js/test/poetry.js", "./src/js/test/wikipedia.js", "./src/js/test/today-tracker.js", "./src/js/test/weak-spot.js", "./src/js/test/wordset.js", "./src/js/test/tts.js", "./src/js/replay.js", ]; //legacy files //the order of files is important const globalSrc = ["./src/js/global-dependencies.js", "./src/js/exports.js"]; //concatenates and lints legacy js files and writes the output to dist/gen/index.js task("cat", function () { return src(globalSrc) .pipe(concat("index.js")) .pipe(eslint(eslintConfig)) .pipe(eslint.format()) .pipe(eslint.failAfterError()) .pipe(dest("./dist/gen")); }); task("sass", function () { return src("./src/sass/*.scss") .pipe(concat("style.scss")) .pipe(sass({ outputStyle: "compressed" }).on("error", sass.logError)) .pipe(dest("dist/css")); }); task("static", function () { return src("./static/**/*", { dot: true }).pipe(dest("./dist/")); }); //copies refactored js files to dist/gen so that they can be required by dist/gen/index.js task("copy-modules", function () { return src(refactoredSrc, { allowEmpty: true }).pipe(dest("./dist/gen")); }); //bundles the refactored js files together with index.js (the concatenated legacy js files) //it's odd that the entry point is generated, so we should seek a better way of doing this task("browserify", function () { const b = browserify({ //index.js is generated by task "cat" entries: "./dist/gen/index.js", //a source map isn't very useful right now because //the source files are concatenated together debug: false, }); return b .transform( babelify.configure({ presets: ["@babel/preset-env"], plugins: ["@babel/transform-runtime"], }) ) .bundle() .pipe(source("monkeytype.js")) .pipe(buffer()) .pipe( uglify({ mangle: false, }) ) .pipe(dest("./dist/js")); }); //lints only the refactored files task("lint", function () { let filelist = refactoredSrc; filelist.push("./static/**/*.json"); return src(filelist) .pipe(eslint(eslintConfig)) .pipe(eslint.format()) .pipe(eslint.failAfterError()); }); task("clean", function () { return src("./dist/", { allowEmpty: true }).pipe(vinylPaths(del)); }); task("updateSwCacheName", function () { let date = new Date(); let dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + "-" + date.getHours() + "-" + date.getMinutes() + "-" + date.getSeconds(); return src(["static/sw.js"]) .pipe( replace( /const staticCacheName = .*;/g, `const staticCacheName = "sw-cache-${dateString}";` ) ) .pipe(dest("./dist/")); }); task( "compile", series( "lint", "cat", "copy-modules", "browserify", "static", "sass", "updateSwCacheName" ) ); task("watch", function () { watch(["./static/**/*", "./src/**/*"], series("compile")); }); task("build", series("clean", "compile")); ==> monkeytype/src/sass/about.scss <== .pageAbout { display: grid; gap: 2rem; .created { text-align: center; color: var(--sub-color); a { text-decoration: none; } } .section { display: grid; gap: 0.25rem; .title { font-size: 2rem; line-height: 2rem; color: var(--sub-color); margin: 1rem 0; } .contactButtons, .supportButtons { margin-top: 1rem; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 1rem; .button { text-decoration: none; font-size: 1.5rem; padding: 2rem 0; .fas, .fab { margin-right: 1rem; } } } .supporters, .contributors { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0.25rem; color: var(--text-color); } h1 { font-size: 1rem; line-height: 1rem; color: var(--sub-color); margin: 0; font-weight: 300; } p { margin: 0; padding: 0; color: var(--text-color); } } } ==> monkeytype/src/sass/banners.scss <== #bannerCenter { position: fixed; width: 100%; z-index: 9999; .banner { background: var(--sub-color); color: var(--bg-color); display: flex; justify-content: center; .container { max-width: 1000px; display: grid; grid-template-columns: auto 1fr auto; gap: 1rem; align-items: center; width: 100%; justify-items: center; .image { // background-image: url(images/merchdropwebsite2.png); height: 2.3rem; background-size: cover; aspect-ratio: 6/1; background-position: center; background-repeat: no-repeat; margin-left: 2rem; } .icon { margin-left: 1rem; margin-top: 0.5rem; margin-bottom: 0.5rem; } .text { margin-top: 0.5rem; margin-bottom: 0.5rem; } .closeButton { margin-right: 1rem; margin-top: 0.5rem; margin-bottom: 0.5rem; transition: 0.125s; &:hover { cursor: pointer; color: var(--text-color); } } } &.good { background: var(--main-color); } &.bad { background: var(--error-color); } } } ==> monkeytype/src/sass/popups.scss <== .popupWrapper { 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: 2rem 0; } #customTextPopupWrapper { #customTextPopup { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 60vw; .wordfilter { width: 33%; justify-self: right; } textarea { background: rgba(0, 0, 0, 0.1); padding: 1rem; color: var(--main-color); border: none; outline: none; font-size: 1rem; font-family: var(--font); width: 100%; border-radius: var(--roundness); resize: vertical; height: 200px; color: var(--text-color); overflow-x: hidden; overflow-y: scroll; } .inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; justify-items: left; } .randomInputFields { display: grid; grid-template-columns: 1fr auto 1fr; text-align: center; align-items: center; width: 100%; gap: 1rem; } } } #wordFilterPopupWrapper { #wordFilterPopup { color: var(--sub-color); background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 400px; input { width: 100%; } .group { display: grid; gap: 0.5rem; } .lengthgrid { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto 1fr; column-gap: 1rem; } .tip { color: var(--sub-color); font-size: 0.8rem; } .loadingIndicator { justify-self: center; } } } #quoteRatePopupWrapper { #quoteRatePopup { color: var(--sub-color); background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 2rem; width: 800px; display: grid; grid-template-areas: "ratingStats ratingStats submitButton" "spacer spacer spacer" "quote quote quote"; grid-template-columns: auto 1fr; color: var(--text-color); .spacer { grid-area: spacer; grid-column: 1/4; width: 100%; height: 0.1rem; border-radius: var(--roundness); background: var(--sub-color); opacity: 0.25; } .submitButton { font-size: 2rem; grid-area: submitButton; color: var(--sub-color); &:hover { color: var(--text-color); } } .top { color: var(--sub-color); font-size: 0.8rem; } .ratingStats { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; grid-area: ratingStats; .top { font-size: 1rem; } .val { font-size: 2.25rem; } } .quote { display: grid; grid-area: quote; gap: 1rem; grid-template-areas: "text text text" "id length source"; grid-template-columns: 1fr 1fr 3fr; .text { grid-area: text; } .id { grid-area: id; } .length { grid-area: length; } .source { grid-area: source; } } .stars { display: grid; color: var(--sub-color); font-size: 2rem; grid-template-columns: auto auto auto auto auto; justify-content: flex-start; align-items: center; cursor: pointer; } .star { transition: 0.125s; } i { pointer-events: none; } .star.active { color: var(--text-color); } } } #simplePopupWrapper { #simplePopup { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 400px; .title { font-size: 1.5rem; color: var(--sub-color); } .inputs { display: grid; gap: 1rem; } .text { font-size: 1rem; color: var(--text-color); } } } #mobileTestConfigPopupWrapper { #mobileTestConfigPopup { background: var(--bg-color); border-radius: var(--roundness); padding: 1rem; display: grid; gap: 1rem; width: calc(100vw - 2rem); // margin-left: 1rem; max-width: 400px; .title { font-size: 1.5rem; color: var(--sub-color); } .inputs { display: grid; gap: 1rem; } .text { font-size: 1rem; color: var(--text-color); } .group { display: grid; gap: 0.5rem; } } } #customWordAmountPopupWrapper, #customTestDurationPopupWrapper, #practiseWordsPopupWrapper, #pbTablesPopupWrapper { #customWordAmountPopup, #customTestDurationPopup, #practiseWordsPopup, #pbTablesPopup { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 400px; .title { font-size: 1.5rem; color: var(--sub-color); } .tip { font-size: 0.75rem; color: var(--sub-color); } .text { font-size: 1rem; color: var(--text-color); } } #customTestDurationPopup { .preview { font-size: 0.75rem; color: var(--sub-color); } } } #pbTablesPopupWrapper #pbTablesPopup { .title { color: var(--text-color); } min-width: 50rem; max-height: calc(100vh - 10rem); overflow-y: scroll; table { border-spacing: 0; border-collapse: collapse; color: var(--text-color); td { padding: 0.5rem 0.5rem; } thead { color: var(--sub-color); font-size: 0.75rem; } tbody tr:nth-child(odd) td { background: rgba(0, 0, 0, 0.1); } td.infoIcons span { margin: 0 0.1rem; } .miniResultChartButton { opacity: 0.25; transition: 0.25s; cursor: pointer; &:hover { opacity: 1; } } .sub { opacity: 0.5; } td { text-align: right; } td:nth-child(6), td:nth-child(7) { text-align: center; } tbody td:nth-child(1) { font-size: 1.5rem; } } } #customThemeShareWrapper { #customThemeShare { width: 50vw; background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; overflow-y: scroll; } } #quoteSearchPopupWrapper { #quoteSearchPopup { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 80vw; max-width: 1000px; height: 80vh; grid-template-rows: auto auto auto 1fr; #quoteSearchTop { display: flex; justify-content: space-between; .title { font-size: 1.5rem; color: var(--sub-color); } .buttons { width: 33%; display: grid; gap: 0.5rem; .button { width: 100%; } } } #extraResults { text-align: center; color: var(--sub-color); } #quoteSearchResults { display: grid; gap: 0.5rem; height: auto; overflow-y: scroll; .searchResult { display: grid; grid-template-columns: 1fr 1fr 3fr 0fr; grid-template-areas: "text text text text" "id len source report"; grid-auto-rows: auto; width: 100%; gap: 0.5rem; transition: 0.25s; padding: 1rem; box-sizing: border-box; user-select: none; cursor: pointer; height: min-content; .text { grid-area: text; overflow: visible; color: var(--text-color); } .id { grid-area: id; font-size: 0.8rem; color: var(--sub-color); } .length { grid-area: len; font-size: 0.8rem; color: var(--sub-color); } .source { grid-area: source; font-size: 0.8rem; color: var(--sub-color); } .resultChevron { grid-area: chevron; display: flex; align-items: center; justify-items: center; color: var(--sub-color); font-size: 2rem; } .report { grid-area: report; color: var(--sub-color); transition: 0.25s; &:hover { color: var(--text-color); } } .sub { opacity: 0.5; } } .searchResult:hover { background: rgba(0, 0, 0, 0.1); border-radius: 5px; } } } } #settingsImportWrapper { #settingsImport { width: 50vw; background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; overflow-y: scroll; } } #quoteSubmitPopup { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 1000px; grid-template-rows: auto auto auto auto auto auto auto auto auto; height: 100%; max-height: 40rem; overflow-y: scroll; label { color: var(--sub-color); margin-bottom: -1rem; } .title { font-size: 1.5rem; color: var(--sub-color); } textarea { resize: vertical; width: 100%; padding: 10px; line-height: 1.2rem; min-height: 5rem; } .characterCount { position: absolute; top: -1.25rem; right: 0.25rem; color: var(--sub-color); user-select: none; &.red { color: var(--error-color); } } } #quoteApprovePopup { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 1000px; height: 80vh; grid-template-rows: auto 1fr; .top { display: flex; justify-content: space-between; .title { font-size: 1.5rem; color: var(--sub-color); } .button { width: 33%; } } .quotes { display: grid; gap: 1rem; height: auto; overflow-y: scroll; align-content: baseline; .quote { display: grid; grid-template-columns: 1fr auto; grid-auto-rows: auto 2rem; width: 100%; gap: 1rem; transition: 0.25s; box-sizing: border-box; user-select: none; height: min-content; margin-bottom: 1rem; .text { grid-column: 1/2; grid-row: 1/2; overflow: visible; color: var(--text-color); resize: vertical; min-height: 4rem; } .source { grid-column: 1/2; grid-row: 2/3; color: var(--text-color); } .buttons { display: flex; flex-direction: column; justify-content: center; margin-right: 1rem; grid-column: 2/3; grid-row: 1/4; color: var(--sub-color); } .bottom { display: flex; justify-content: space-around; color: var(--sub-color); .length.red { color: var(--error-color); } } .sub { opacity: 0.5; } } .searchResult:hover { background: rgba(0, 0, 0, 0.1); border-radius: 5px; } } } #quoteReportPopupWrapper { #quoteReportPopup { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; width: 1000px; grid-template-rows: auto auto auto auto auto auto auto auto auto; height: auto; max-height: 40rem; overflow-y: scroll; label { color: var(--sub-color); margin-bottom: -1rem; } .text { color: var(--sub-color); } .quote { font-size: 1.5rem; } .title { font-size: 1.5rem; color: var(--sub-color); } textarea { resize: vertical; width: 100%; padding: 10px; line-height: 1.2rem; min-height: 5rem; } .characterCount { position: absolute; top: -1.25rem; right: 0.25rem; color: var(--sub-color); user-select: none; &.red { color: var(--error-color); } } } } #resultEditTagsPanelWrapper { #resultEditTagsPanel { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; overflow-y: scroll; width: 500px; .buttons { display: grid; gap: 0.1rem; grid-template-columns: 1fr 1fr 1fr; } } } #versionHistoryWrapper { 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: start; padding: 5rem 0; #versionHistory { width: 75vw; height: 100%; background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; @extend .ffscroll; overflow-y: scroll; .tip { text-align: center; color: var(--sub-color); } .releases { display: grid; gap: 4rem; .release { display: grid; grid-template-areas: "title date" "body body"; .title { grid-area: title; font-size: 2rem; color: var(--sub-color); } .date { grid-area: date; text-align: right; color: var(--sub-color); align-self: center; } .body { grid-area: body; color: var(--text-color); } &:last-child { margin-bottom: 2rem; } } } } } #supportMeWrapper { #supportMe { width: 900px; // height: 400px; overflow-y: scroll; max-height: 100%; background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; grid-template-rows: auto auto auto; gap: 2rem; @extend .ffscroll; .title { font-size: 2rem; line-height: 2rem; color: var(--main-color); } .text { color: var(--text-color); } .subtext { color: var(--sub-color); font-size: 0.75rem; } .buttons { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 1rem; .button { display: block; width: 100%; height: 100%; padding: 2rem 0; display: grid; gap: 1rem; text-decoration: none; .text { transition: 0.25s; } &:hover .text { color: var(--bg-color); } .icon { font-size: 5rem; line-height: 5rem; } } } } } #contactPopupWrapper { #contactPopup { // height: 400px; overflow-y: scroll; max-height: 100%; background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; grid-template-rows: auto auto auto; gap: 2rem; @extend .ffscroll; margin: 0 2rem; max-width: 900px; .title { font-size: 2rem; line-height: 2rem; color: var(--main-color); } .text { color: var(--text-color); span { color: var(--error-color); } } .subtext { color: var(--sub-color); font-size: 0.75rem; grid-area: subtext; } .buttons { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; .button { display: block; width: 100%; height: 100%; padding: 1rem 0; display: grid; // gap: 0.5rem; text-decoration: none; grid-template-areas: "icon textgroup"; grid-template-columns: auto 1fr; text-align: left; align-items: center; .textGroup { grid-area: textgroup; } .text { font-size: 1.5rem; line-height: 2rem; transition: 0.25s; } &:hover .text { color: var(--bg-color); } .icon { grid-area: icon; font-size: 2rem; line-height: 2rem; padding: 0 1rem; } } } } } #presetWrapper { #presetEdit { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; overflow-y: scroll; } } #tagsWrapper { #tagsEdit { background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 1rem; overflow-y: scroll; } } ==> monkeytype/src/sass/account.scss <== .signOut { font-size: 1rem; line-height: 1rem; justify-self: end; // background: var(--sub-color); color: var(--sub-color); width: fit-content; width: -moz-fit-content; padding: 0.5rem; border-radius: var(--roundness); cursor: pointer; transition: 0.25s; float: right; &:hover { color: var(--text-color); } .fas { margin-right: 0.5rem; } } .pageAccount { display: grid; gap: 1rem; .content { display: grid; gap: 2rem; } .sendVerificationEmail { cursor: pointer; } .timePbTable, .wordsPbTable { .sub { opacity: 0.5; } td { text-align: right; } tbody td:nth-child(1) { font-size: 1.5rem; } } .showAllTimePbs, .showAllWordsPbs { margin-top: 1rem; } .topFilters .buttons { display: flex; justify-content: space-evenly; gap: 1rem; .button { width: 100%; } } .miniResultChartWrapper { // pointer-events: none; z-index: 999; display: none; height: 15rem; background: var(--bg-color); width: 45rem; position: absolute; border-radius: var(--roundness); padding: 1rem; // box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25); } .miniResultChartBg { display: none; z-index: 998; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.25); position: fixed; left: 0; top: 0; } .doublegroup { display: grid; grid-auto-flow: column; gap: 1rem; .titleAndTable { .title { color: var(--sub-color); } } } .triplegroup { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; .text { align-self: center; color: var(--sub-color); } } .group { &.noDataError { margin: 20rem 0; // height: 30rem; // line-height: 30rem; text-align: center; } &.createdDate { text-align: center; color: var(--sub-color); } &.personalBestTables { .tables { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; } } &.history { .active { animation: flashHighlight 4s linear 0s 1; } .loadMoreButton { background: rgba(0, 0, 0, 0.1); color: var(--text-color); text-align: center; padding: 0.5rem; border-radius: var(--roundness); cursor: pointer; -webkit-transition: 0.25s; transition: 0.25s; -webkit-user-select: none; display: -ms-grid; display: grid; -ms-flex-line-pack: center; align-content: center; margin-top: 1rem; &:hover, &:focus { color: var(--bg-color); background: var(--text-color); } } } .title { color: var(--sub-color); } .val { font-size: 3rem; line-height: 3.5rem; } .chartjs-render-monitor { width: 100% !important; } &.chart { position: relative; .above { display: flex; justify-content: center; margin-bottom: 1rem; color: var(--sub-color); flex-wrap: wrap; .group { display: flex; align-items: center; } .fas, .punc { margin-right: 0.25rem; } .spacer { width: 1rem; } } .below { text-align: center; color: var(--sub-color); margin-top: 1rem; display: grid; grid-template-columns: auto 300px; align-items: center; .text { height: min-content; } .buttons { display: grid; gap: 0.5rem; } } .chart { height: 400px; } .chartPreloader { position: absolute; width: 100%; background: rgba(0, 0, 0, 0.5); height: 100%; display: grid; align-items: center; justify-content: center; font-size: 5rem; text-shadow: 0 0 3rem black; } } } table { border-spacing: 0; border-collapse: collapse; color: var(--text-color); td { padding: 0.5rem 0.5rem; } thead { color: var(--sub-color); font-size: 0.75rem; } tbody tr:nth-child(odd) td { background: rgba(0, 0, 0, 0.1); } td.infoIcons span { margin: 0 0.1rem; } .miniResultChartButton { opacity: 0.25; transition: 0.25s; cursor: pointer; &:hover { opacity: 1; } } } #resultEditTags { transition: 0.25s; &:hover { cursor: pointer; color: var(--text-color); opacity: 1 !important; } } } .pageAccount { .group.filterButtons { gap: 1rem; display: grid; grid-template-columns: 1fr 1fr; .buttonsAndTitle { height: fit-content; height: -moz-fit-content; display: grid; gap: 0.25rem; color: var(--sub-color); line-height: 1rem; font-size: 1rem; &.testDate .buttons, &.languages .buttons, &.layouts .buttons, &.funbox .buttons, &.tags .buttons { grid-template-columns: repeat(4, 1fr); grid-auto-flow: unset; } } .buttons { display: grid; grid-auto-flow: column; gap: 1rem; .button { background: rgba(0, 0, 0, 0.1); color: var(--text-color); text-align: center; padding: 0.5rem; border-radius: var(--roundness); cursor: pointer; transition: 0.25s; -webkit-user-select: none; display: grid; align-content: center; &.active { background: var(--main-color); color: var(--bg-color); } &:hover { color: var(--bg-color); background: var(--main-color); } } } } } .header-sorted { font-weight: bold; } .sortable:hover { cursor: pointer; user-select: none; background-color: rgba(0, 0, 0, 0.1); } ==> monkeytype/src/sass/monkey.scss <== #monkey { width: 308px; height: 0; margin: 0 auto; animation: shake 0s infinite; div { height: 200px; width: 308px; position: fixed; } .up { background-image: url("../images/monkey/m3.png"); } .left { background-image: url("../images/monkey/m1.png"); } .right { background-image: url("../images/monkey/m2.png"); } .both { background-image: url("../images/monkey/m4.png"); } .fast { .up { background-image: url("../images/monkey/m3_fast.png"); } .left { background-image: url("../images/monkey/m1_fast.png"); } .right { background-image: url("../images/monkey/m2_fast.png"); } .both { background-image: url("../images/monkey/m4_fast.png"); } } } ==> monkeytype/src/sass/core.scss <== @import url("https://fonts.googleapis.com/css2?family=Fira+Code&family=IBM+Plex+Sans:wght@600&family=Inconsolata&family=Roboto+Mono&family=Source+Code+Pro&family=JetBrains+Mono&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Montserrat&family=Roboto&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Lexend+Deca&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Oxygen&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Nunito&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Itim&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Comfortaa&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Coming+Soon&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Lato&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Lalezar&display=swap"); @import url("https://fonts.googleapis.com/css?family=Noto+Naskh+Arabic&display=swap"); :root { --roundness: 0.5rem; --font: "Roboto Mono"; // scroll-behavior: smooth; scroll-padding-top: 2rem; } ::placeholder { color: var(--sub-color); opacity: 1; /* Firefox */ } #nocss { display: none !important; pointer-events: none; } .ffscroll { scrollbar-width: thin; scrollbar-color: var(--sub-color) transparent; } html { @extend .ffscroll; overflow-y: scroll; } a { display: inline-block; color: var(--sub-color); transition: 0.25s; &:hover { color: var(--text-color); } } body { margin: 0; padding: 0; min-height: 100vh; font-family: var(--font); color: var(--text-color); overflow-x: hidden; background: var(--bg-color); } .customBackground { content: ""; width: 100vw; height: 100vh; position: fixed; left: 0; top: 0; background-position: center center; background-repeat: no-repeat; z-index: -999; justify-content: center; align-items: center; display: flex; } #backgroundLoader { height: 3px; position: fixed; width: 100%; background: var(--main-color); animation: loader 2s cubic-bezier(0.38, 0.16, 0.57, 0.82) infinite; z-index: 9999; } label.checkbox { span { display: block; font-size: 0.76rem; color: var(--sub-color); margin-left: 1.5rem; } input { margin: 0 !important; cursor: pointer; width: 0; height: 0; display: none; & ~ .customTextCheckbox { width: 12px; height: 12px; background: rgba(0, 0, 0, 0.1); border-radius: 2px; box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.1); display: inline-block; margin: 0 0.5rem 0 0.25rem; transition: 0.25s; } &:checked ~ .customTextCheckbox { background: var(--main-color); } } } #centerContent { max-width: 1000px; // min-width: 500px; // margin: 0 auto; display: grid; grid-auto-flow: row; min-height: 100vh; padding-left: 2rem; padding-right: 2rem; padding-top: 2rem; padding-bottom: 2rem; gap: 2rem; align-items: center; z-index: 999; grid-template-rows: auto 1fr auto; width: 100%; &.wide125 { max-width: 1250px; } &.wide150 { max-width: 1500px; } &.wide200 { max-width: 2000px; } &.widemax { max-width: unset; } } key { color: var(--bg-color); background-color: var(--sub-color); /* font-weight: bold; */ padding: 0.1rem 0.3rem; margin: 3px 0; border-radius: 0.1rem; display: inline-block; font-size: 0.7rem; line-height: 0.7rem; } .pageLoading { display: grid; justify-content: center; } .pageLoading, .pageAccount { .preloader { text-align: center; justify-self: center; display: grid; .barWrapper { display: grid; gap: 1rem; grid-row: 1; grid-column: 1; .bar { width: 20rem; height: 0.5rem; background: rgba(0, 0, 0, 0.1); border-radius: var(--roundness); .fill { height: 100%; width: 0%; background: var(--main-color); border-radius: var(--roundness); // transition: 1s; } } } .icon { grid-row: 1; grid-column: 1; font-size: 2rem; color: var(--main-color); margin-bottom: 1rem; } } } .devIndicator { position: fixed; font-size: 3rem; color: var(--sub-color); opacity: 0.25; z-index: -1; &.tl { top: 2rem; left: 2rem; } &.tr { top: 2rem; right: 2rem; } &.bl { bottom: 2rem; left: 2rem; } &.br { bottom: 2rem; right: 2rem; } } * { box-sizing: border-box; } .hidden { display: none !important; } .invisible { opacity: 0 !important; pointer-events: none !important; } .button { color: var(--text-color); cursor: pointer; transition: 0.25s; padding: 0.4rem; border-radius: var(--roundness); background: rgba(0, 0, 0, 0.1); text-align: center; -webkit-user-select: none; // display: grid; align-content: center; height: min-content; height: -moz-min-content; line-height: 1rem; &:hover { color: var(--bg-color); background: var(--text-color); outline: none; } &:focus { color: var(--main-color); background: var(--sub-color); outline: none; } &.active { background: var(--main-color); color: var(--bg-color); &:hover { // color: var(--text-color); background: var(--text-color); outline: none; } &:focus { color: var(--bg-color); background: var(--main-color); outline: none; } } &.disabled { opacity: 0.5; cursor: default; pointer-events: none; &:hover { color: var(--text-color); background: rgba(0, 0, 0, 0.1); outline: none; } } &.disabled.active { opacity: 0.5; cursor: default; pointer-events: none; &:hover { color: var(--bg-color); background: var(--main-color); outline: none; } } } .text-button { transition: 0.25s; color: var(--sub-color); cursor: pointer; margin-right: 0.25rem; cursor: pointer; outline: none; &.active { color: var(--main-color); } &:hover, &:focus { color: var(--text-color); } } .icon-button { display: grid; grid-auto-flow: column; align-content: center; transition: 0.25s; padding: 0.5rem; border-radius: var(--roundness); cursor: pointer; &:hover { color: var(--text-color); } &:focus { // background: var(--sub-color); color: var(--sub-color); border: none; outline: none; } &.disabled { opacity: 0.5; cursor: default; pointer-events: none; } } .scrollToTopButton { bottom: 2rem; right: 2rem; position: fixed; font-size: 2rem; width: 4rem; height: 4rem; text-align: center; line-height: 4rem; background: var(--bg-color); border-radius: 99rem; z-index: 99; cursor: pointer; color: var(--sub-color); transition: 0.25s; &:hover { color: var(--text-color); } } ==> monkeytype/src/sass/login.scss <== .pageLogin { display: flex; grid-auto-flow: column; gap: 1rem; justify-content: space-around; align-items: center; .side { display: grid; gap: 0.5rem; justify-content: center; grid-template-columns: 1fr; input { width: 15rem; } &.login { grid-template-areas: "title forgotButton" "form form"; .title { grid-area: title; } #forgotPasswordButton { grid-area: forgotButton; font-size: 0.75rem; line-height: 0.75rem; height: fit-content; height: -moz-fit-content; align-self: center; justify-self: right; padding: 0.25rem 0; color: var(--sub-color); cursor: pointer; transition: 0.25s; &:hover { color: var(--text-color); } } form { grid-area: form; } } } form { display: grid; gap: 0.5rem; width: 100%; } .preloader { position: fixed; left: 50%; top: 50%; font-size: 2rem; transform: translate(-50%, -50%); color: var(--main-color); transition: 0.25s; } } ==> monkeytype/src/sass/z_media-queries.scss <== @media only screen and (max-width: 1200px) { #leaderboardsWrapper { #leaderboards { .tables { grid-template-columns: unset; } .tables .rightTableWrapper, .tables .leftTableWrapper { height: calc(50vh - 12rem); } } } } @media only screen and (max-width: 1050px) { .pageSettings .section.fullWidth .buttons { grid-template-columns: 1fr 1fr 1fr; } #result .morestats { gap: 1rem; grid-template-rows: 1fr 1fr; } #supportMe { width: 90vw !important; .buttons { .button { .icon { font-size: 3rem !important; line-height: 3rem !important; } } } } #customTextPopup { width: 80vw !important; .wordfilter.button { width: 50% !important; } } } @media only screen and (max-width: 1000px) { #quoteRatePopup { width: 90vw !important; } #bottom { .leftright { .left { gap: 0.25rem 1rem; display: grid; grid-template-rows: 1fr 1fr; grid-auto-flow: row; grid-template-columns: auto auto auto auto; } .right { display: grid; grid-template-rows: 1fr 1fr; gap: 0.25rem 1rem; } } } } @media only screen and (max-width: 900px) { // #leaderboards { // .mainTitle { // font-size: 1.5rem !important; // line-height: 1.5rem !important; // } // } .merchBanner { img { display: none; } .text { padding: 0.25rem 0; } } .pageAccount { .group.personalBestTables { .tables { grid-template-columns: 1fr; } } .group.history { table { thead, tbody { td:nth-child(1), td:nth-child(8), td:nth-child(9) { display: none; } } } } } } @media only screen and (max-width: 800px) { .pageSettings .settingsGroup.quickNav .links { grid-auto-flow: unset; grid-template-columns: 1fr 1fr 1fr; justify-items: center; } #bannerCenter .banner .container { grid-template-columns: 1fr auto; .image { display: none; } .lefticon { display: none; } .text { margin-left: 2rem; } } #centerContent { #top { grid-template-areas: "logo config" "menu config"; grid-template-columns: auto auto; .logo { margin-bottom: 0; } } #menu { gap: 0.5rem; font-size: 0.8rem; line-height: 0.8rem; margin-top: -0.5rem; .icon-button { padding: 0.25rem; } } } #contactPopupWrapper #contactPopup .buttons { grid-template-columns: 1fr; } .pageAbout .section { .contributors, .supporters { grid-template-columns: 1fr 1fr 1fr; } .contactButtons, .supportButtons { grid-template-columns: 1fr 1fr; } } .pageSettings .section.customBackgroundFilter { .groups { grid-template-columns: 1fr; } .saveContainer { grid-column: -1/-2; } } .pageSettings { .section.themes .tabContent.customTheme { } } #commandLine, #commandLineInput { width: 600px !important; } } @media only screen and (max-width: 700px) { #leaderboardsWrapper { #leaderboards { .leaderboardsTop { flex-direction: column; align-items: baseline; } } } .pageAccount { .triplegroup { grid-template-columns: 1fr 1fr; .emptygroup { display: none; } } .group.chart .below { grid-template-columns: 1fr; gap: 0.5rem; } .group.topFilters .buttonsAndTitle .buttons { display: grid; justify-content: unset; } .group.history { table { thead, tbody { td:nth-child(6) { display: none; } } } } } } @media only screen and (max-width: 650px) { #quoteRatePopup { .ratingStats { grid-template-columns: 1fr 1fr !important; } .quote { grid-template-areas: "text text text" "source source source" "id length length" !important; } } .pageSettings .section { grid-template-columns: 1fr; grid-template-areas: "title" "text" "buttons"; & > .text { margin-bottom: 1rem; } } #result { .buttons { grid-template-rows: 1fr 1fr 1fr; #nextTestButton { grid-column: 1/5; width: 100%; text-align: center; } } } #supportMe { width: 80vw !important; .buttons { grid-template-columns: none !important; .button { grid-template-columns: auto auto; align-items: center; .icon { font-size: 2rem !important; line-height: 2rem !important; } } } } .pageSettings .section.fullWidth .buttons { grid-template-columns: 1fr 1fr; } } @media only screen and (max-width: 600px) { .pageAbout .section .supporters, .pageAbout .section .contributors { grid-template-columns: 1fr 1fr; } #top .logo .bottom { margin-top: 0; } .pageLogin { display: grid; gap: 5rem; grid-auto-flow: unset; } #middle { #result { grid-template-areas: "stats stats" "chart chart" "morestats morestats"; .stats { grid-template-areas: "wpm acc"; gap: 2rem; } .stats.morestats { grid-template-rows: 1fr 1fr 1fr; gap: 1rem; } } } #commandLine, #commandLineInput { width: 500px !important; } #customTextPopupWrapper { #customTextPopup { .wordfilter.button { width: 100% !important; justify-self: auto; } .inputs { display: flex !important; flex-direction: column; justify-content: flex-start; } } } #leaderboardsWrapper #leaderboards { padding: 1rem; gap: 1rem; .mainTitle { font-size: 2rem; line-height: 2rem; } .title { font-size: 1rem; } .leaderboardsTop { .buttonGroup { gap: 0.1rem !important; .button { padding: 0.4rem !important; font-size: 0.7rem !important; } } } } .pageAccount { .group.history { table { thead, tbody { td:nth-child(7), td:nth-child(5) { display: none; } } } } } } @media only screen and (max-width: 550px) { .keymap { .row { height: 1.25rem; } .keymap-key { width: 1.25rem; height: 1.25rem; border-radius: 0.3rem; font-size: 0.6rem; } } #contactPopupWrapper #contactPopup .buttons .button .text { font-size: 1rem; } #contactPopupWrapper #contactPopup .buttons .button .icon { font-size: 1.5rem; line-height: 1.5rem; } #contactPopupWrapper #contactPopup { padding: 1rem; } .pageAbout .section .supporters, .pageAbout .section .contributors { grid-template-columns: 1fr; } #simplePopupWrapper #simplePopup { width: 90vw; } .pageSettings { .settingsGroup.quickNav { display: none; } .section.fullWidth .buttons { grid-template-columns: 1fr; } .section .buttons { grid-auto-flow: row; } .section.customBackgroundFilter .groups .group { grid-template-columns: auto 1fr; .title { grid-column: 1/3; } } } .pageAbout .section { .contactButtons, .supportButtons { grid-template-columns: 1fr; } } .pageAccount { .triplegroup { grid-template-columns: 1fr; } .group.history { table { thead, tbody { td:nth-child(3) { display: none; } } } } } #top { align-items: self-end; .logo { .icon { width: 1.5rem !important; } .text { font-size: 1.5rem !important; margin-bottom: 0.3rem !important; } .bottom { font-size: 1.75rem; line-height: 1.75rem; margin-top: 0; } .top { display: none; } } #menu { .icon-button { padding: 0; } } } #bottom { .leftright { .left { gap: 0.25rem 1rem; display: grid; grid-template-rows: 1fr 1fr 1fr; grid-template-columns: auto auto auto; grid-auto-flow: row; } .right { display: grid; grid-template-rows: 1fr 1fr 1fr; gap: 0.25rem 1rem; } } } #centerContent { #top { grid-template-columns: 1fr auto; .desktopConfig { display: none; } .mobileConfig { display: block; } } padding: 1rem; } #middle { #result { .stats { grid-template-areas: "wpm" "acc"; gap: 1rem; } } } #result { .buttons { grid-template-rows: 1fr 1fr 1fr 1fr; #nextTestButton { grid-column: 1/3; width: 100%; text-align: center; } } } #commandLine, #commandLineInput { width: 400px !important; } } @media only screen and (max-width: 400px) { #top .logo .bottom { font-size: 1.5rem; line-height: 1.5rem; margin-top: 0; } #top .config { grid-gap: 0.25rem; .group .buttons { font-size: 0.65rem; line-height: 0.65rem; } } #bottom { font-size: 0.65rem; .leftright { grid-template-columns: 1fr 1fr; .left { grid-template-rows: 1fr 1fr 1fr 1fr; grid-template-columns: 1fr 1fr; grid-auto-flow: row; } .right { // justify-self: left; // grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr 1fr 1fr; gap: 0.25rem 1rem; } } } #commandLine, #commandLineInput { width: 300px !important; } #leaderboardsWrapper #leaderboards .tables .titleAndTable .titleAndButtons { grid-template-columns: unset; } } @media only screen and (max-width: 350px) { .keymap { display: none !important; } .pageLogin .side input { width: 90vw; } } @media (hover: none) and (pointer: coarse) { #commandLineMobileButton { display: block !important; } } ==> monkeytype/src/sass/inputs.scss <== input, textarea { outline: none; border: none; border-radius: var(--roundness); background: rgba(0, 0, 0, 0.1); color: var(--text-color); padding: 0.5rem; font-size: 1rem; font-family: var(--font); } input[type="range"] { -webkit-appearance: none; padding: 0; width: 100%; height: 1rem; border-radius: var(--roundness); &::-webkit-slider-thumb { -webkit-appearance: none; padding: 0; border: none; width: 2rem; height: 1rem; border-radius: var(--roundness); background-color: var(--main-color); } &::-moz-range-thumb { -webkit-appearance: none; padding: 0; border: none; width: 2rem; height: 1rem; border-radius: var(--roundness); background-color: var(--main-color); } } input[type="color"] { height: 3px; //i dont know why its 3, but safari gods have spoken - 3 makes it work opacity: 0; padding: 0; margin: 0; position: absolute; pointer-events: none; } ::-moz-color-swatch { border: none; } input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; -moz-appearance: none; appearance: none; margin: 0; } input[type="number"] { -moz-appearance: textfield; } .select2-dropdown { background-color: var(--bg-color); color: var(--text-color); outline: none; } .select2-selection { background: rgba(0, 0, 0, 0.1); height: fit-content; height: -moz-fit-content; padding: 5px; border-radius: var(--roundness); color: var(--text-color); font: var(--font); border: none; outline: none; } .select2-container--default .select2-selection--single .select2-selection__rendered { color: var(--text-color); outline: none; } .select2-container--default .select2-results__option--highlighted.select2-results__option--selectable { background-color: var(--text-color); color: var(--bg-color); } .select2-container--default .select2-results__option--selected { background-color: var(--bg-color); color: var(--sub-color); } .select2-container--open .select2-dropdown--below { border-color: rgba(0, 0, 0, 0.1); background: var(--bg-color); color: var(--sub-color); border-radius: var(--roundness); } .select2-container--default .select2-selection--single { color: var(--text-color); background: rgba(0, 0, 0, 0.1); outline: none; border: none; height: auto; } .select2-selection:focus { height: fit-content; height: -moz-fit-content; padding: 5px; border-radius: var(--roundness); color: var(--text-color); font: var(--font); border: none; outline: none; } .select2-selection:active { height: fit-content; height: -moz-fit-content; padding: 5px; border-radius: var(--roundness); color: var(--text-color); font: var(--font); border: none; outline: none; } .select2-container--default .select2-selection--single .select2-selection__arrow { height: 35px; } .select2-container--default .select2-selection--single .select2-selection__arrow b { border-color: var(--sub-color) transparent transparent transparent; } .select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { border-color: var(--sub-color) transparent; } .select2-container--default .select2-search--dropdown .select2-search__field { border-color: rgba(0, 0, 0, 0.1); background: var(--bg-color); color: var(--text-color); border-radius: var(--roundness); } ==> monkeytype/src/sass/commandline.scss <== #commandLineWrapper { 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: start; padding: 5rem 0; #commandInput { width: 700px; background: var(--bg-color); border-radius: var(--roundness); input { background: var(--bg-color); padding: 1rem; color: var(--main-color); border: none; outline: none; font-size: 1rem; width: 100%; border-radius: var(--roundness); } .shiftEnter { padding: 0.5rem 1rem; font-size: 0.75rem; line-height: 0.75rem; color: var(--sub-color); text-align: center; } } #commandLine { width: 700px; background: var(--bg-color); border-radius: var(--roundness); .searchicon { color: var(--sub-color); margin: 1px 1rem 0 1rem; } input { background: var(--bg-color); padding: 1rem 1rem 1rem 0; color: var(--text-color); border: none; outline: none; font-size: 1rem; width: 100%; border-radius: var(--roundness); } .separator { background: black; width: 100%; height: 1px; margin-bottom: 0.5rem; } .listTitle { color: var(--text-color); padding: 0.5rem 1rem; font-size: 0.75rem; line-height: 0.75rem; } .suggestions { display: block; @extend .ffscroll; overflow-y: scroll; max-height: calc(100vh - 10rem - 3rem); display: grid; cursor: pointer; user-select: none; .entry { padding: 0.5rem 1rem; font-size: 0.75rem; line-height: 0.75rem; color: var(--sub-color); display: grid; grid-template-columns: auto 1fr; div { pointer-events: none; } .textIcon { font-weight: 900; /* width: 1.25rem; */ display: inline-block; letter-spacing: -0.1rem; margin-right: 0.5rem; text-align: center; width: 1.25em; } .fas { margin-right: 0.5rem; } &:last-child { border-radius: 0 0 var(--roundness) var(--roundness); } &.activeMouse { color: var(--bg-color); background: var(--text-color); cursor: pointer; } &.activeKeyboard { color: var(--bg-color); background: var(--text-color); } // &:hover { // color: var(--text-color); // background: var(--sub-color); // cursor: pointer; // } } } } } ==> monkeytype/src/sass/notifications.scss <== #notificationCenter { width: 350px; z-index: 99999999; display: grid; gap: 1rem; position: fixed; right: 1rem; top: 1rem; .history { display: grid; gap: 1rem; } .notif { user-select: none; .icon { color: var(--bg-color); opacity: 0.5; padding: 1rem 1rem; align-items: center; display: grid; font-size: 1.25rem; } .message { padding: 1rem 1rem 1rem 0; .title { color: var(--bg-color); font-size: 0.75em; opacity: 0.5; line-height: 0.75rem; } } position: relative; background: var(--sub-color); color: var(--bg-color); display: grid; grid-template-columns: min-content auto min-content; border-radius: var(--roundness); border-width: 0.25rem; &.bad { background-color: var(--error-color); } &.good { background-color: var(--main-color); } &:hover { // opacity: .5; // box-shadow: 0 0 20px rgba(0,0,0,.25); cursor: pointer; &::after { opacity: 1; } } &::after { transition: 0.125s; font-family: "Font Awesome 5 Free"; background: rgba(0, 0, 0, 0.5); opacity: 0; font-weight: 900; content: "\f00d"; position: absolute; width: 100%; height: 100%; color: var(--bg-color); font-size: 2.5rem; display: grid; /* align-self: center; */ align-items: center; text-align: center; border-radius: var(--roundness); } } } ==> monkeytype/src/sass/caret.scss <== #caret { height: 1.5rem; background: var(--caret-color); animation: caretFlashSmooth 1s infinite; position: absolute; border-radius: var(--roundness); // transition: 0.05s; transform-origin: top left; } #paceCaret { height: 1.5rem; // background: var(--sub-color); background: var(--sub-color); opacity: 0.5; position: absolute; border-radius: var(--roundness); // transition: 0.25s; transform-origin: top left; width: 2px; } #caret, #paceCaret { &.off { width: 0; } &.default { width: 2px; } &.carrot { background-color: transparent; background-image: url("../images/carrot.png"); background-size: contain; background-position: center; background-repeat: no-repeat; width: 0.25rem; &.size2 { margin-left: -0.1rem; } &.size3 { margin-left: -0.2rem; } &.size4 { margin-left: -0.3rem; } } &.banana { background-color: transparent; background-image: url("../images/banana.png"); background-size: contain; background-position: center; background-repeat: no-repeat; width: 1rem; &.size2 { margin-left: -0.1rem; } &.size3 { margin-left: -0.5rem; } &.size4 { margin-left: -0.3rem; } } &.block { width: 0.7em; margin-left: 0.25em; border-radius: 0; z-index: -1; } &.outline { @extend #caret, .block; animation-name: none; background: transparent; border: 1px solid var(--caret-color); } &.underline { height: 2px; width: 0.8em; margin-top: 1.3em; margin-left: 0.3em; &.size125 { margin-top: 1.8em; } &.size15 { margin-top: 2.1em; } &.size2 { margin-top: 2.7em; } &.size3 { margin-top: 3.9em; } &.size4 { margin-top: 4.7em; } } &.size125 { transform: scale(1.25); } &.size15 { transform: scale(1.45); } &.size2 { transform: scale(1.9); } &.size3 { transform: scale(2.8); } &.size4 { transform: scale(3.7); } } ==> monkeytype/src/sass/test.scss <== #timerWrapper { opacity: 0; transition: 0.25s; z-index: -1; position: relative; z-index: 99; #timer { position: fixed; top: 0; left: 0; width: 100vw; /* height: 0.5rem; */ height: 0.5rem; background: black; /* background: #0f0f0f; */ /* background: red; */ // transition: 1s linear; z-index: -1; &.timerMain { background: var(--main-color); } &.timerSub { background: var(--sub-color); } &.timerText { background: var(--text-color); } } } .pageTest { position: relative; .ssWatermark { font-size: 1.25rem; color: var(--sub-color); line-height: 1rem; text-align: right; } #timerNumber { pointer-events: none; transition: 0.25s; height: 0; color: black; line-height: 0; z-index: -1; text-align: center; left: 0; width: 100%; position: relative; font-size: 10rem; opacity: 0; width: 0; height: 0; margin: 0 auto; display: grid; justify-content: center; bottom: 6rem; transition: none; } #largeLiveWpmAndAcc { font-size: 10rem; color: black; width: 100%; left: 0; text-align: center; z-index: -1; height: 0; line-height: 0; top: 5rem; position: relative; display: grid; grid-auto-flow: column; justify-content: center; gap: 5rem; #liveWpm { opacity: 0; } #liveAcc { opacity: 0; } #liveBurst { opacity: 0; } } #largeLiveWpmAndAcc.timerMain, #timerNumber.timerMain { color: var(--main-color); } #timer.timerMain { background: var(--main-color); } #largeLiveWpmAndAcc.timerSub, #timerNumber.timerSub { color: var(--sub-color); } #timer.timerSub { background: var(--sub-color); } #largeLiveWpmAndAcc.timerText, #timerNumber.timerText { color: var(--text-color); } #timer.timerText { background: var(--text-color); } } #words { height: fit-content; height: -moz-fit-content; display: flex; flex-wrap: wrap; width: 100%; align-content: flex-start; user-select: none; padding-bottom: 1em; .newline { width: inherit; } letter { border-bottom-style: solid; border-bottom-width: 0.05em; border-bottom-color: transparent; &.dead { border-bottom-width: 0.05em; border-bottom-color: var(--sub-color); } &.tabChar, &.nlChar { margin: 0 0.25rem; opacity: 0.2; } } /* a little hack for right-to-left languages */ &.rightToLeftTest { //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages direction: rtl; .word { //flex-direction: row-reverse; direction: rtl; } } &.withLigatures { letter { display: inline; } } &.blurred { opacity: 0.25; filter: blur(4px); -webkit-filter: blur(4px); } &.flipped { .word { color: var(--text-color); & letter.dead { border-bottom-color: var(--sub-color) !important; } & letter.correct { color: var(--sub-color); } & letter.corrected { color: var(--sub-color); border-bottom: 2px dotted var(--main-color); } & letter.extraCorrected { border-right: 2px dotted var(--main-color); } } } &.colorfulMode { .word { & letter.dead { border-bottom-color: var(--main-color) !important; } & letter.correct { color: var(--main-color); } & letter.corrected { color: var(--main-color); border-bottom: 2px dotted var(--text-color); } & letter.extraCorrected { border-right: 2px dotted var(--text-color); } & letter.incorrect { color: var(--colorful-error-color); } & letter.incorrect.extra { color: var(--colorful-error-extra-color); } } } &.flipped.colorfulMode { .word { color: var(--main-color); & letter.dead { border-bottom-color: var(--sub-color) !important; } & letter.correct { color: var(--sub-color); } & letter.corrected { color: var(--sub-color); border-bottom: 2px dotted var(--main-color); } & letter.extraCorrected { border-right: 2px dotted var(--main-color); } & letter.incorrect { color: var(--colorful-error-color); } & letter.incorrect.extra { color: var(--colorful-error-extra-color); } } } } .word { margin: 0.25rem; color: var(--sub-color); font-variant: no-common-ligatures; // display: flex; // transition: 0.25s /* margin-bottom: 1px; */ border-bottom: 2px solid transparent; line-height: 1rem; letter { display: inline-block; } &.lastbeforenewline::after { font-family: "Font Awesome 5 Free"; font-weight: 600; content: "\f107"; margin-left: 0.5rem; opacity: 0.25; } // transition: .25s; .wordInputAfter { opacity: 1; position: absolute; background: var(--sub-color); color: var(--bg-color); /* background: red; */ padding: 0.5rem; /* left: .5rem; */ margin-left: -0.5rem; // margin-top: -1.5rem; border-radius: var(--roundness); // box-shadow: 0 0 10px rgba(0,0,0,.25); transition: 0.25s; text-shadow: none; top: -0.5rem; z-index: 10; cursor: text; .speed { font-size: 0.75rem; } } } #words.size125 .word { line-height: 1.25rem; font-size: 1.25rem; margin: 0.31rem; } #words.size15 .word { line-height: 1.5rem; font-size: 1.5rem; margin: 0.37rem; } #words.size2 .word { line-height: 2rem; font-size: 2rem; margin: 0.5rem; } #words.size3 .word { line-height: 3rem; font-size: 3rem; margin: 0.75rem; } #words.size4 .word { line-height: 4rem; font-size: 4rem; margin: 1rem; } #words.nospace { .word { margin: 0.5rem 0; } } #words.arrows { .word { margin: 0.5rem 0; letter { margin: 0 0.25rem; } } } .word.error { /* margin-bottom: 1px; */ border-bottom: 2px solid var(--error-color); text-shadow: 1px 0px 0px var(--bg-color), // 2px 0px 0px var(--bg-color), -1px 0px 0px var(--bg-color), // -2px 0px 0px var(--bg-color), 0px 1px 0px var(--bg-color), 1px 1px 0px var(--bg-color), -1px 1px 0px var(--bg-color); } #words.noErrorBorder, #resultWordsHistory.noErrorBorder { .word.error { text-shadow: none; } } // .word letter { // transition: .1s; // height: 1rem; // line-height: 1rem; /* margin: 0 1px; */ // } .word letter.correct { color: var(--text-color); } .word letter.corrected { color: var(--text-color); border-bottom: 2px dotted var(--main-color); } .word letter.extraCorrected { border-right: 2px dotted var(--main-color); } .word letter.incorrect { color: var(--error-color); position: relative; } .word letter.incorrect hint { position: absolute; bottom: -1em; color: var(--text-color); line-height: initial; font-size: 0.75em; text-shadow: none; padding: 1px; left: 0; opacity: 0.5; text-align: center; width: 100%; } .word letter.incorrect.extra { color: var(--error-extra-color); } .word letter.missing { opacity: 0.5; } #words.flipped.colorfulMode .word.error, #words.colorfulMode .word.error { border-bottom: 2px solid var(--colorful-error-color); } #wordsInput { opacity: 0; padding: 0; margin: 0; border: none; outline: none; display: block; resize: none; position: fixed; z-index: -1; cursor: default; pointer-events: none; } #capsWarning { background: var(--main-color); color: var(--bg-color); display: table; position: absolute; left: 50%; // top: 66vh; transform: translateX(-50%) translateY(-50%); padding: 1rem; border-radius: var(--roundness); /* margin-top: 1rem; */ transition: 0.25s; z-index: 999; pointer-events: none; i { margin-right: 0.5rem; } } #result { display: grid; // height: 200px; gap: 1rem; // grid-template-columns: auto 1fr; // justify-content: center; align-items: center; grid-template-columns: auto 1fr; grid-template-areas: "stats chart" "morestats morestats"; // "wordsHistory wordsHistory" // "buttons buttons" // "login login" // "ssw ssw"; &:focus { outline: none; } .buttons { display: grid; grid-auto-flow: column; gap: 1rem; justify-content: center; // grid-area: buttons; grid-column: 1/3; } .ssWatermark { // grid-area: ssw; grid-column: 1/3; } #resultWordsHistory, #resultReplay { // grid-area: wordsHistory; color: var(--sub-color); // grid-column: 1/3; margin-bottom: 1rem; .icon-button { padding: 0; margin-left: 0.5rem; } .heatmapLegend { display: inline-grid; grid-template-columns: auto auto auto; gap: 1rem; font-size: 0.75rem; color: var(--sub-color); width: min-content; .boxes { display: flex; .box { width: 1rem; height: 1rem; } .box:nth-child(1) { background: var(--colorful-error-color); border-radius: var(--roundness) 0 0 var(--roundness); } .box:nth-child(2) { background: var(--colorful-error-color); filter: opacity(0.6); } .box:nth-child(3) { background: var(--sub-color); } .box:nth-child(4) { background: var(--main-color); filter: opacity(0.6); } .box:nth-child(5) { background: var(--main-color); border-radius: 0 var(--roundness) var(--roundness) 0; } } } .title { user-select: none; margin-bottom: 0.25rem; } .words { display: flex; flex-wrap: wrap; width: 100%; align-content: flex-start; user-select: none; .word { position: relative; margin: 0.18rem 0.6rem 0.15rem 0; letter.correct { color: var(--text-color); } letter.incorrect { color: var(--error-color); } letter.incorrect.extra { color: var(--error-extra-color); } &.heatmap-0 letter { color: var(--colorful-error-color); } &.heatmap-1 letter { color: var(--colorful-error-color); filter: opacity(0.6); } &.heatmap-2 letter { color: var(--sub-color); } &.heatmap-3 letter { color: var(--main-color); filter: opacity(0.6); } &.heatmap-4 letter { color: var(--main-color); } } &.rightToLeftTest { //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages direction: rtl; .word { //flex-direction: row-reverse; direction: rtl; } } &.withLigatures { letter { display: inline; } } } } .chart { grid-area: chart; width: 100%; canvas { width: 100% !important; height: 100%; } max-height: 200px; height: 200px; .title { color: var(--sub-color); margin-bottom: 1rem; } } .loginTip { grid-column: 1/3; text-align: center; color: var(--sub-color); // grid-area: login; grid-column: 1/3; .link { text-decoration: underline; display: inline-block; cursor: pointer; } } .stats { grid-area: stats; display: grid; // column-gap: 0.5rem; gap: 0.5rem; justify-content: center; align-items: center; // grid-template-areas: // "wpm acc" // "wpm key" // "raw time" // "consistency consistency" // "source source" // "leaderboards leaderboards" // "testType infoAndTags"; // grid-template-areas: // "wpm acc key consistency testType leaderboards source" // "wpm raw time nothing infoAndTags leaderboards source"; grid-template-areas: "wpm" "acc"; margin-bottom: 1rem; &.morestats { display: grid; grid-auto-flow: column; grid-template-areas: none; align-items: flex-start; justify-content: space-between; column-gap: 2rem; grid-area: morestats; // grid-template-areas: "raw consistency testType infoAndTags leaderboards source" // "key time testType infoAndTags leaderboards source"; .subgroup { display: grid; gap: 0.5rem; } } .group { // margin-bottom: 0.5rem; .top { color: var(--sub-color); font-size: 1rem; line-height: 1rem; margin-bottom: 0.25rem; } .bottom { color: var(--main-color); font-size: 2rem; line-height: 2rem; } &.time { .afk, .timeToday { color: var(--sub-color); font-size: 0.75rem; line-height: 0.75rem; margin-left: 0.2rem; } } &.source { #rateQuoteButton, #reportQuoteButton { padding: 0 0.25rem; } #rateQuoteButton { display: inline-grid; gap: 0.25rem; } } } // .infoAndTags { // display: grid; // gap: 0.5rem; // align-self: baseline; // // grid-area: infoAndTags; // color: var(--sub-color); // .top { // font-size: 1rem; // line-height: 1rem; // } // .bottom { // font-size: 1rem; // line-height: 1rem; // } // } .info, .tags, .source { .top { font-size: 1rem; line-height: 1rem; } .bottom { font-size: 1rem; line-height: 1rem; } } .source { max-width: 30rem; } .tags .bottom .fas { margin-left: 0.5rem; } .wpm { grid-area: wpm; .top { font-size: 2rem; line-height: 1.5rem; display: flex; // margin-top: -0.5rem; // .crownWrapper { // width: 1.7rem; // overflow: hidden; // height: 1.7rem; // margin-left: 0.5rem; // // margin-top: 0.98rem; // margin-top: -0.5rem; .crown { height: 1.7rem; width: 1.7rem; margin-left: 0.5rem; margin-top: -0.2rem; font-size: 0.7rem; line-height: 1.7rem; background: var(--main-color); color: var(--bg-color); border-radius: 0.6rem; text-align: center; align-self: center; width: 1.7rem; height: 1.7rem; } // } } .bottom { font-size: 4rem; line-height: 4rem; } } .testType, .leaderboards { .bottom { font-size: 1rem; line-height: 1rem; .lbChange .fas { margin-right: 0.15rem; } } } .acc { grid-area: acc; .top { font-size: 2rem; line-height: 1.5rem; } .bottom { font-size: 4rem; line-height: 4rem; } } .burst { grid-area: burst; .top { font-size: 2rem; line-height: 1.5rem; } .bottom { font-size: 4rem; line-height: 4rem; } } // .key { // grid-area: key; // } // .time { // grid-area: time; // } // .raw { // grid-area: raw; // } } } #restartTestButton, #showWordHistoryButton, #saveScreenshotButton, #restartTestButtonWithSameWordset, #nextTestButton, #practiseWordsButton, #watchReplayButton { position: relative; border-radius: var(--roundness); padding: 1rem 2rem; width: min-content; width: -moz-min-content; color: var(--sub-color); transition: 0.25s; cursor: pointer; &:hover, &:focus { color: var(--main-color); outline: none; } &:focus { background: var(--sub-color); } } #retrySavingResultButton { position: relative; border-radius: var(--roundness); padding: 1rem 2rem; color: var(--error-color); transition: 0.25s; cursor: pointer; width: max-content; width: -moz-max-content; background: var(--colorful-error-color); color: var(--bg-color); justify-self: center; justify-content: center; margin: 0 auto 1rem auto; user-select: none; &:hover, &:focus { background: var(--text-color); outline: none; } &:focus { background: var(--text-color); } } #showWordHistoryButton { opacity: 1; } #replayWords { cursor: pointer; } #replayStopwatch { color: var(--main-color); display: inline-block; margin: 0; } #restartTestButton { margin: 0 auto; margin-top: 1rem; } .pageTest { #wordsWrapper { position: relative; } #memoryTimer { background: var(--main-color); color: var(--bg-color); padding: 1rem; border-radius: var(--roundness); /* width: min-content; */ text-align: center; width: max-content; /* justify-self: center; */ left: 50%; position: absolute; transform: translateX(-50%); top: -6rem; user-select: none; pointer-events: none; opacity: 0; } .outOfFocusWarning { text-align: center; height: 0; line-height: 150px; z-index: 999; position: relative; user-select: none; pointer-events: none; } #testModesNotice { display: grid; grid-auto-flow: column; gap: 1rem; color: var(--sub-color); text-align: center; margin-bottom: 1.25rem; height: 1rem; line-height: 1rem; transition: 0.125s; justify-content: center; user-select: none; .fas { margin-right: 0.5rem; } } #miniTimerAndLiveWpm { height: 0; margin-left: 0.37rem; display: flex; font-size: 1rem; line-height: 1rem; margin-top: -1.5rem; position: absolute; color: black; .time { margin-right: 2rem; } .wpm, .acc { margin-right: 2rem; } .time, .wpm, .acc, .burst { opacity: 0; } &.timerMain { color: var(--main-color); } &.timerSub { color: var(--sub-color); } &.timerText { color: var(--text-color); } &.size125 { margin-top: -1.75rem; font-size: 1.25rem; line-height: 1.25rem; } &.size15 { margin-top: -2rem; font-size: 1.5rem; line-height: 1.5rem; } &.size2 { margin-top: -2.5rem; font-size: 2rem; line-height: 2rem; } &.size3 { margin-top: -3.5rem; font-size: 3rem; line-height: 3rem; } &.size4 { margin-top: -4.5rem; font-size: 4rem; line-height: 4rem; } } } #middle.focus .pageTest { #testModesNotice { opacity: 0 !important; } } ==> monkeytype/src/sass/leaderboards.scss <== #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(95vh - 5rem); overflow-y: auto; background: var(--bg-color); border-radius: var(--roundness); padding: 2rem; display: grid; gap: 2rem 0; grid-template-rows: 3rem auto; grid-template-areas: "title buttons" "tables tables"; grid-template-columns: 1fr 1fr; .leaderboardsTop { width: 200%; min-width: 100%; display: flex; align-items: center; justify-content: space-between; .buttonGroup .button { padding: 0.4rem 2.18rem; } } .mainTitle { font-size: 3rem; line-height: 3rem; grid-area: title; } .subTitle { color: var(--sub-color); } .title { font-size: 2rem; line-height: 2rem; margin-bottom: 0.5rem; } .tables { grid-area: tables; display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; font-size: 0.8rem; width: 100%; .sub { opacity: 0.5; } .alignRight { text-align: right; } .titleAndTable { display: grid; width: 100%; .titleAndButtons { display: grid; grid-template-columns: 1fr auto; .buttons { display: grid; grid-template-columns: auto 1fr 1fr; align-items: center; // margin-top: .1rem; gap: 1rem; color: var(--sub-color); .button { padding-left: 1rem; padding-right: 1rem; } } } .title { grid-area: 1/1; margin-bottom: 0; line-height: 2.5rem; } .subtitle { grid-area: 1/1; align-self: center; justify-self: right; color: var(--sub-color); } } .leftTableWrapper, .rightTableWrapper { height: calc(100vh - 22rem); @extend .ffscroll; overflow-y: scroll; overflow-x: auto; } .leftTableWrapper::-webkit-scrollbar, .rightTableWrapper::-webkit-scrollbar { height: 5px; width: 5px; } table { width: 100%; border-spacing: 0; border-collapse: collapse; tr td:first-child { text-align: center; } tr.me { td { color: var(--main-color); // font-weight: 900; } } td { padding: 0.5rem 0.5rem; } thead { color: var(--sub-color); font-size: 0.75rem; td { padding: 0.5rem; background: var(--bg-color); position: -webkit-sticky; position: sticky; top: 0; z-index: 99; } } tbody { color: var(--text-color); tr:nth-child(odd) td { background: rgba(0, 0, 0, 0.1); } } tfoot { td { padding: 1rem 0.5rem; position: -webkit-sticky; position: sticky; bottom: -5px; background: var(--bg-color); color: var(--main-color); z-index: 4; } } tr { td:first-child { padding-left: 1rem; } td:last-child { padding-right: 1rem; } } } } .buttons { .buttonGroup { display: grid; grid-auto-flow: column; gap: 1rem; grid-area: 1/2; } } } } ==> monkeytype/src/sass/keymap.scss <== .keymap { display: grid; grid-template-rows: 1fr 1fr 1fr; justify-content: center; white-space: nowrap; // height: 140px; gap: 0.25rem; margin-top: 1rem; user-select: none; .row { height: 2rem; gap: 0.25rem; } .keymap-key { display: flex; background-color: transparent; color: var(--sub-color); border-radius: var(--roundness); border: 0.05rem solid; border-color: var(--sub-color); text-align: center; justify-content: center; align-items: center; width: 2rem; height: 2rem; position: relative; .bump { width: 0.75rem; height: 0.05rem; background: var(--sub-color); position: absolute; border-radius: var(--roundness); // margin-top: 1.5rem; bottom: 0.15rem; } &.active-key { color: var(--bg-color); background-color: var(--main-color); border-color: var(--main-color); .bump { background: var(--bg-color); } } &#KeySpace { &:hover { cursor: pointer; color: var(--main-color); } } &#KeySpace, &#KeySpace2 { width: 100%; } &#KeySpace2 { opacity: 0; } &.flash { animation: flashKey 1s cubic-bezier(0.16, 1, 0.3, 1) forwards; } } .hidden-key, .hide-key { opacity: 0; } .keymap-split-spacer, .keymap-stagger-split-spacer, .keymap-matrix-split-spacer { display: none; } .r1 { display: grid; grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .r2 { display: grid; grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1rem; } .r3 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .r4 { display: grid; grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2.75fr; } .r5 { display: grid; grid-template-columns: 3.5fr 6fr 3.5fr; font-size: 0.5rem; // &.matrixSpace { // // grid-template-columns: 6.75fr 1.9fr 6.75fr; // grid-template-columns: 6.9fr 4.6fr 6.9fr; // wider spacebar // } // &.splitSpace { // // grid-template-columns: 6.75fr 1.9fr 6.75fr; // grid-template-columns: 4fr 7.5fr 4fr; // } } &.matrix { .r1, .r2, .r3 { grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .r4 { grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .r5 { grid-template-columns: 3.25fr 5fr 2fr 1fr; } .r1, .r2, .r3 { :nth-child(13) { opacity: 0; } :nth-child(14) { opacity: 0; } } } &.split { .keymap-split-spacer { display: block; } .keymap-stagger-split-spacer { display: block; } .r1 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; } .r2 { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .r3 { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; } .r4 { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr; } .r5 { grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr; } #KeySpace2 { opacity: 1; } } &.split_matrix { .keymap-split-spacer { display: block; width: 2rem; height: 2rem; } .keymap-stagger-split-spacer { display: none; } .keymap-matrix-split-spacer { display: block; width: 2rem; height: 2rem; } .r1, .r2, .r3 { grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .r4 { grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; } .r5 { grid-template-columns: 3.225fr 3fr 1fr 3fr 2fr; } #KeySpace2 { opacity: 1; } .r1 { :nth-child(12) { opacity: 0; } } .r1, .r2, .r3 { :nth-child(13) { opacity: 0; } :nth-child(14) { opacity: 0; } } } &.alice { .keymap-split-spacer { display: block; } .r4 .keymap-split-spacer { display: none; } .keymap-stagger-split-spacer { display: block; } .r1 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; .keymap-key:nth-child(2) { //1 margin-left: 45%; } .keymap-key:nth-child(3) { //2 margin-top: -2px; margin-left: 45%; } .keymap-key:nth-child(4), .keymap-key:nth-child(5), .keymap-key:nth-child(6), .keymap-key:nth-child(7) { //3456 transform: rotate(10deg); margin-left: 45%; } .keymap-key:nth-child(4) { //3 margin-top: 3px; } .keymap-key:nth-child(5) { //4 margin-top: 10px; } .keymap-key:nth-child(6) { //5 margin-top: 17px; } .keymap-key:nth-child(7) { //6 margin-top: 24px; } .keymap-key:nth-child(9), .keymap-key:nth-child(10), .keymap-key:nth-child(11), .keymap-key:nth-child(12) { //7890 transform: rotate(-10deg); margin-left: -48%; } .keymap-key:nth-child(12) { //7 margin-top: -1px; } .keymap-key:nth-child(11) { //8 margin-top: 6px; } .keymap-key:nth-child(10) { //9 margin-top: 13px; } .keymap-key:nth-child(9) { //10 margin-top: 20px; } .keymap-key:nth-child(13), .keymap-key:nth-child(14) { //-= margin-left: -40%; } } .r2 { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; .keymap-key:nth-child(2) { //Q margin-left: 20%; } .keymap-key:nth-child(3), .keymap-key:nth-child(4), .keymap-key:nth-child(5), .keymap-key:nth-child(6) { //WERT transform: rotate(10deg); margin-left: 45%; } .keymap-key:nth-child(4), .keymap-key:nth-child(10) { //EI margin-top: 8px; } .keymap-key:nth-child(5), .keymap-key:nth-child(9) { //RU margin-top: 15px; } .keymap-key:nth-child(6), .keymap-key:nth-child(8) { //TY margin-top: 22px; } .keymap-key:nth-child(8), .keymap-key:nth-child(9), .keymap-key:nth-child(10), .keymap-key:nth-child(11) { //YUIO transform: rotate(-10deg); margin-left: -12%; } } .r3 { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; .keymap-key:nth-child(2) { //A margin-left: -5px; } .keymap-key:nth-child(3), .keymap-key:nth-child(4), .keymap-key:nth-child(5), .keymap-key:nth-child(6) { //SDFG margin-left: -1px; transform: rotate(10deg); } .keymap-key:nth-child(4), .keymap-key:nth-child(10) { //DK margin-top: 8px; } .keymap-key:nth-child(5), .keymap-key:nth-child(9) { //FJ margin-top: 15px; } .keymap-key:nth-child(6), .keymap-key:nth-child(8) { //GH margin-top: 22px; } .keymap-key:nth-child(8), .keymap-key:nth-child(9), .keymap-key:nth-child(10), .keymap-key:nth-child(11) { //HJKL transform: rotate(-10deg); margin-left: -25%; } } .r4 { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr; .keymap-key:nth-child(2) { margin-left: -18px; } .keymap-key:nth-child(3) { //Z margin-left: -15px; } .keymap-key:nth-child(4), .keymap-key:nth-child(5), .keymap-key:nth-child(6), .keymap-key:nth-child(7) { //XCVB margin-left: -11px; transform: rotate(10deg); margin-top: 2px; } .keymap-key:nth-child(12) { //, margin-top: 4px; margin-left: -5px; } .keymap-key:nth-child(5), .keymap-key:nth-child(11) { //CM margin-top: 10px; } .keymap-key:nth-child(6), .keymap-key:nth-child(10) { //VN margin-top: 18px; } .keymap-key:nth-child(7) { //B margin-top: 24px; } .keymap-key:nth-child(10), .keymap-key:nth-child(11), .keymap-key:nth-child(12) { //NM, transform: rotate(-10deg); margin-left: -25%; } } .r5 { grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr; } #KeySpace2 { opacity: 1; } // div#KeyE.keymap-key, // div#KeyD.keymap-key { // margin-top: 6px; // } // div#KeyC.keymap-key { // margin-top: 8px; // } // div#KeyR.keymap-key, // div#KeyF.keymap-key { // margin-top: 12px; // } // div#KeyV.keymap-key { // margin-top: 14px; // } // div#KeyT.keymap-key, // div#KeyG.keymap-key { // margin-top: 18px; // } // div#KeyB.keymap-key { // margin-top: 20px; // } // div#KeyY.keymap-key, // div#KeyU.keymap-key, // div#KeyI.keymap-key, // div#KeyO.keymap-key { // transform: rotate(-10deg); // margin-left: -25%; // } // div#KeyH.keymap-key, // div#KeyJ.keymap-key, // div#KeyK.keymap-key, // div#KeyL.keymap-key { // transform: rotate(-10deg); // margin-left: -35%; // } // div#KeyN.keymap-key, // div#KeyM.keymap-key, // div#KeyComma.keymap-key { // transform: rotate(-10deg); // margin-left: -16%; // } // div#KeyP.keymap-key, // div#KeyLeftBracket.keymap-key, // div#KeyRightBracket.keymap-key { // margin-left: 5%; // } // div#KeySemicolon.keymap-key, // div#KeyQuote.keymap-key { // margin-left: -25%; // } // div#KeyPeriod.keymap-key, // div#KeySlash.keymap-key { // margin-left: -3px; // } // div#KeyO.keymap-key, // div#KeyComma.keymap-key { // margin-top: 3px; // } // div#KeyL.keymap-key { // margin-top: 1px; // } // div#KeyI.keymap-key, // div#KeyM.keymap-key { // margin-top: 9px; // } // div#KeyK.keymap-key { // margin-top: 7px; // } // div#KeyU.keymap-key, // div#KeyN.keymap-key { // margin-top: 15px; // } // div#KeyJ.keymap-key { // margin-top: 13px; // } // div#KeyY.keymap-key { // margin-top: 21px; // } // div#KeyH.keymap-key { // margin-top: 19px; // } div#KeySpace.keymap-key { transform: rotate(10deg); margin-left: -5%; margin-top: 21%; } div#KeySpace2.keymap-key { transform: rotate(-10deg); margin-left: -33%; margin-top: 20%; } div#KeyBackslash.keymap-key { visibility: hidden; } div.extraKey { margin-top: 25px; transform: rotate(-10deg) !important; margin-left: -7px !important; display: flex; background-color: transparent; color: var(--sub-color); border-radius: var(--roundness); border: 0.05rem solid; border-color: var(--sub-color); text-align: center; justify-content: center; align-items: center; width: 2rem; height: 2rem; position: relative; } // div#KeySpace.keymap-key:after { // content: 'Alice'; // text-indent: 0; // font-weight: 600!important; // margin: auto; // font-size: 0.9rem; // color: var(--bg-color) // } } } ==> monkeytype/src/sass/nav.scss <== #menu { font-size: 1rem; line-height: 1rem; color: var(--sub-color); display: grid; grid-auto-flow: column; gap: 0.5rem; // margin-bottom: -0.4rem; width: fit-content; width: -moz-fit-content; .icon-button { // .icon { // display: grid; // align-items: center; // justify-items: center; // text-align: center; // width: 1.25rem; // height: 1.25rem; // } text-decoration: none; .text { font-size: 0.65rem; line-height: 0.65rem; align-self: center; margin-left: 0.25rem; } // &:hover { // cursor: pointer; // color: var(--main-color); // } } .separator { width: 2px; height: 1rem; background-color: var(--sub-color); } } #top.focus #menu .icon-button.discord::after { background: transparent; } #top.focus #menu { color: transparent !important; } #top.focus #menu .icon-button { color: transparent !important; } #top { grid-template-areas: "logo menu config"; line-height: 2.3rem; font-size: 2.3rem; /* text-align: center; */ // transition: 0.25s; padding: 0 5px; display: grid; grid-auto-flow: column; grid-template-columns: auto 1fr auto; z-index: 2; align-items: center; gap: 0.5rem; user-select: none; .logo { // margin-bottom: 0.6rem; cursor: pointer; display: grid; grid-template-columns: auto 1fr; gap: 0.5rem; .icon { width: 2.5rem; display: grid; align-items: center; background-color: transparent; // margin-bottom: 0.15rem; svg path { transition: 0.25s; fill: var(--main-color); } } .text { .top { position: absolute; left: 0.25rem; top: -0.1rem; font-size: 0.65rem; line-height: 0.65rem; color: var(--sub-color); transition: 0.25s; } position: relative; font-size: 2rem; margin-bottom: 0.4rem; font-family: "Lexend Deca"; transition: 0.25s; } white-space: nowrap; user-select: none; .bottom { margin-left: -0.15rem; color: var(--main-color); transition: 0.25s; cursor: pointer; } } .config { grid-area: config; transition: 0.125s; .mobileConfig { display: none; .icon-button { display: grid; grid-auto-flow: column; align-content: center; transition: 0.25s; margin-right: -1rem; padding: 0.5rem 1rem; font-size: 2rem; border-radius: var(--roundness); cursor: pointer; color: var(--sub-color); &:hover { color: var(--text-color); } } } .desktopConfig { justify-self: right; display: grid; // grid-auto-flow: row; grid-template-rows: 0.7rem 0.7rem 0.7rem; grid-gap: 0.2rem; // width: min-content; // width: -moz-min-content; // transition: 0.25s; /* margin-bottom: 0.1rem; */ justify-items: self-end; .group { // transition: 0.25s; .title { color: var(--sub-color); font-size: 0.5rem; line-height: 0.5rem; margin-bottom: 0.15rem; } .buttons { font-size: 0.7rem; line-height: 0.7rem; display: flex; } &.disabled { pointer-events: none; opacity: 0.25; } } .punctuationMode { margin-bottom: -0.1rem; } .numbersMode { margin-bottom: -0.1rem; } } } .result { display: grid; grid-auto-flow: column; grid-gap: 1rem; width: min-content; width: -moz-min-content; transition: 0.25s; grid-column: 3/4; grid-row: 1/2; .group { .title { font-size: 0.65rem; line-height: 0.65rem; color: var(--sub-color); } .val { font-size: 1.7rem; line-height: 1.7rem; color: var(--main-color); transition: 0.25s; } } } //top focus &.focus { color: var(--sub-color) !important; .result { opacity: 0 !important; } .icon svg path { fill: var(--sub-color) !important; } .logo .text { color: var(--sub-color) !important; // opacity: 0 !important; } .logo .top { opacity: 0 !important; } .config { opacity: 0 !important; } } } ==> monkeytype/src/sass/scroll.scss <== /* width */ ::-webkit-scrollbar { width: 7px; } /* Track */ ::-webkit-scrollbar-track { background: transparent; } /* Handle */ ::-webkit-scrollbar-thumb { background: var(--sub-color); transition: 0.25s; border-radius: 2px !important; } /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { background: var(--main-color); } ::-webkit-scrollbar-corner { background: var(--sub-color); } ==> monkeytype/src/sass/settings.scss <== .pageSettings { display: grid; // grid-template-columns: 1fr 1fr; gap: 2rem; .tip { color: var(--sub-color); } .sectionGroupTitle { font-size: 2rem; color: var(--sub-color); line-height: 2rem; cursor: pointer; transition: 0.25s; &:hover { color: var(--text-color); } .fas { margin-left: 0.5rem; &.rotate { transform: rotate(-90deg); } } } .sectionSpacer { height: 1.5rem; } .settingsGroup { display: grid; gap: 2rem; &.quickNav .links { display: grid; grid-auto-flow: column; text-align: center; a { text-decoration: none; width: 100%; cursor: pointer; // opacity: 0.5; &:hover { opacity: 1; } } } } .section { display: grid; // gap: .5rem; grid-template-areas: "title title" "text buttons"; grid-template-columns: 2fr 1fr; column-gap: 2rem; align-items: center; .button.danger { box-shadow: 0px 0px 0px 2px var(--error-color); color: var(--text-color); &:hover { background: var(--text-color); color: var(--bg-color); } } .inputAndButton { display: grid; grid-template-columns: 8fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem; .button { height: auto; .fas { margin-right: 0rem; vertical-align: sub; } } } &.themes .tabContainer [tabcontent="custom"] { label.button:first-child { color: var(--text-color); } label.button { color: var(--bg-color); } } &.customBackgroundFilter { .groups { grid-area: buttons; display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 2rem; .group { display: grid; grid-template-columns: 1fr auto 2fr; gap: 1rem; .title, .value { color: var(--text-color); } } } .saveContainer { grid-column: -1/-3; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; } .fas { margin-right: 0rem; } } &.customTheme { grid-template-columns: 1fr 1fr 1fr 1fr; justify-items: stretch; gap: 0.5rem 2rem; & p { grid-area: unset; grid-column: 1 / span 4; } & .spacer { grid-column: 3 / 5; } } h1 { font-size: 1rem; line-height: 1rem; color: var(--sub-color); margin: 0; grid-area: title; font-weight: 300; } p { grid-area: text; color: var(--sub-color); margin: 0; } & > .text { align-self: normal; color: var(--text-color); grid-area: text; } .buttons { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.5rem; grid-area: buttons; &.vertical { grid-auto-flow: unset; } } &.discordIntegration { .info { grid-area: buttons; text-align: center; color: var(--main-color); } #unlinkDiscordButton { margin-top: 0.5rem; font-size: 0.75rem; color: var(--sub-color); &:hover { color: var(--text-color); } } .howto { margin-top: 1rem; color: var(--text-color); } } &.tags { .tagsListAndButton { grid-area: buttons; } .tag { grid-template-columns: 6fr 1fr 1fr 1fr; margin-bottom: 0.5rem; } .addTagButton { margin-top: 0.5rem; color: var(--text-color); cursor: pointer; transition: 0.25s; padding: 0.2rem 0.5rem; border-radius: var(--roundness); background: rgba(0, 0, 0, 0.1); text-align: center; -webkit-user-select: none; display: grid; align-content: center; height: min-content; height: -moz-min-content; &.active { background: var(--main-color); color: var(--bg-color); } &:hover, &:focus { color: var(--bg-color); background: var(--text-color); outline: none; } } } &.presets { .presetsListAndButton { grid-area: buttons; } .preset { grid-template-columns: 7fr 1fr 1fr; margin-bottom: 0.5rem; } .addPresetButton { margin-top: 0.5rem; color: var(--text-color); cursor: pointer; transition: 0.25s; padding: 0.2rem 0.5rem; border-radius: var(--roundness); background: rgba(0, 0, 0, 0.1); text-align: center; -webkit-user-select: none; display: grid; align-content: center; height: min-content; height: -moz-min-content; &.active { background: var(--main-color); color: var(--bg-color); } &:hover, &:focus { color: var(--bg-color); background: var(--text-color); outline: none; } } } &.fontSize .buttons { grid-template-columns: 1fr 1fr 1fr 1fr; } &.themes { .tabContainer { position: relative; grid-area: buttons; .tabContent { overflow: revert; height: auto; &.customTheme { margin-top: 0.5rem; .colorText { color: var(--text-color); } } .text { align-self: center; } } } .theme.button { display: grid; grid-template-columns: auto 1fr auto; .text { color: inherit; } .activeIndicator { overflow: hidden; width: 1.25rem; transition: 0.25s; opacity: 0; color: inherit; .far { margin: 0; } &.active { width: 1.25rem; opacity: 1; } } .favButton { overflow: hidden; width: 1.25rem; transition: 0.25s; opacity: 0; .far, .fas { margin: 0; pointer-events: none; } &:hover { cursor: pointer; } &.active { width: 1.25rem; opacity: 1; } } &:hover { .favButton { width: 1.25rem; opacity: 1; } } &.active { .activeIndicator { opacity: 1; } } } } &.themes { grid-template-columns: 2fr 1fr; grid-template-areas: "title tabs" "text text" "buttons buttons"; column-gap: 2rem; // row-gap: 0.5rem; .tabs { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.5rem; grid-area: tabs; } .buttons { margin-left: 0; grid-auto-flow: dense; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0.5rem; margin-top: 0.5rem; } } &.fullWidth { grid-template-columns: 2fr 1fr; grid-template-areas: "title tabs" "text text" "buttons buttons"; column-gap: 2rem; // row-gap: 0.5rem; .buttons { margin-left: 0; grid-auto-flow: dense; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0.5rem; margin-top: 1rem; } } &.randomTheme .buttons { grid-template-columns: 1fr 1fr 1fr 1fr 1fr; } } } .buttons div.theme:hover { transform: scale(1.1); } ==> monkeytype/src/sass/animations.scss <== @keyframes loader { 0% { width: 0; left: 0; } 50% { width: 100%; left: 0; } 100% { width: 0; left: 100%; } } @keyframes caretFlashSmooth { 0%, 100% { opacity: 0; } 50% { opacity: 1; } } @keyframes caretFlashHard { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } } @keyframes flashKey { from { color: var(--bg-color); background-color: var(--main-color); border-color: var(--main-color); } to { color: var(--sub-color); background-color: var(--bg-color); border-color: var(--sub-color); } } @keyframes shake { 0% { transform: translate(4px, 0) rotate(0deg); } 50% { transform: translate(-4px, 0) rotate(0deg); } 100% { transform: translate(4px, 0) rotate(0deg); } } @keyframes flashHighlight { 0% { background-color: var(--bg-color); } 10% { background-color: var(--main-color); } 40% { background-color: var(--main-color); } 100% { background-color: var(--bg-color); } } ==> monkeytype/src/sass/footer.scss <== #bottom { position: relative; text-align: center; line-height: 1rem; font-size: 0.75rem; color: var(--sub-color); // transition: 0.25s; padding: 0 5px; // margin-bottom: 2rem; .keyTips { transition: 0.25s; margin-bottom: 2rem; } #supportMeButton { transition: 0.25s; &:hover { color: var(--text-color); cursor: pointer; } } #commandLineMobileButton { display: none; top: -4rem; left: 0; position: absolute; font-size: 1rem; width: 3rem; height: 3rem; text-align: center; line-height: 3rem; background: var(--main-color); border-radius: 99rem; z-index: 99; cursor: pointer; color: var(--bg-color); transition: 0.25s; } .leftright { display: grid; grid-template-columns: auto auto; gap: 1rem; a { text-decoration: none; } .left { text-align: left; display: grid; grid-auto-flow: column; width: fit-content; gap: 1rem; width: -moz-fit-content; } .right { text-align: right; display: grid; grid-auto-flow: column; width: fit-content; width: -moz-fit-content; justify-self: right; gap: 1rem; // align-items: center; } .left a, .right a { display: grid; grid-auto-flow: column; gap: 0.25rem; align-items: baseline; width: max-content; width: -moz-available; &:hover { color: var(--text-color); cursor: pointer; } } } .version { opacity: 0; } } #bottom.focus { .keyTips { opacity: 0 !important; } a { opacity: 0 !important; } #commandLineMobileButton { opacity: 0 !important; pointer-events: none !important; } } ==> ./monkeytype/src/js/theme-colors.js <== // export let bg = "#323437"; // export let main = "#e2b714"; // export let caret = "#e2b714"; // export let sub = "#646669"; // export let text = "#d1d0c5"; // export let error = "#ca4754"; // export let errorExtra = "#7e2a33"; // export let colorfulError = "#ca4754"; // export let colorfulErrorExtra = "#7e2a33"; let colors = { bg: "#323437", main: "#e2b714", caret: "#e2b714", sub: "#646669", text: "#d1d0c5", error: "#ca4754", errorExtra: "#7e2a33", colorfulError: "#ca4754", colorfulErrorExtra: "#7e2a33", }; export async function get(color) { let ret; if (color === undefined) { ret = colors; } else { ret = colors[color]; } return ret; // return check(); // async function run() { // return new Promise(function (resolve, reject) { // window.setTimeout(() => { // update(); // if (color === undefined) { // ret = colors; // } else { // ret = colors[color]; // } // resolve(check()); // }, 250); // }); // } // async function check() { // if (color === undefined) { // if (ret.bg === "") { // return await run(); // } else { // return ret; // } // } else { // if (ret === "") { // return await run(); // } else { // return ret; // } // } // } } export function reset() { colors = { bg: "", main: "", caret: "", sub: "", text: "", error: "", errorExtra: "", colorfulError: "", colorfulErrorExtra: "", }; } export function update() { let st = getComputedStyle(document.body); colors.bg = st.getPropertyValue("--bg-color").replace(" ", ""); colors.main = st.getPropertyValue("--main-color").replace(" ", ""); colors.caret = st.getPropertyValue("--caret-color").replace(" ", ""); colors.sub = st.getPropertyValue("--sub-color").replace(" ", ""); colors.text = st.getPropertyValue("--text-color").replace(" ", ""); colors.error = st.getPropertyValue("--error-color").replace(" ", ""); colors.errorExtra = st .getPropertyValue("--error-extra-color") .replace(" ", ""); colors.colorfulError = st .getPropertyValue("--colorful-error-color") .replace(" ", ""); colors.colorfulErrorExtra = st .getPropertyValue("--colorful-error-extra-color") .replace(" ", ""); } ==> ./monkeytype/src/js/simple-popups.js <== import * as Loader from "./loader"; import * as Notifications from "./notifications"; import * as AccountController from "./account-controller"; import * as DB from "./db"; import * as Settings from "./settings"; import axiosInstance from "./axios-instance"; import * as UpdateConfig from "./config"; export let list = {}; class SimplePopup { constructor( id, type, title, inputs = [], text = "", buttonText = "Confirm", execFn, beforeShowFn ) { this.parameters = []; this.id = id; this.type = type; this.execFn = execFn; this.title = title; this.inputs = inputs; this.text = text; this.wrapper = $("#simplePopupWrapper"); this.element = $("#simplePopup"); this.buttonText = buttonText; this.beforeShowFn = beforeShowFn; } reset() { this.element.html(`
`); } init() { let el = this.element; el.find("input").val(""); // if (el.attr("popupId") !== this.id) { this.reset(); el.attr("popupId", this.id); el.find(".title").text(this.title); el.find(".text").text(this.text); this.initInputs(); if (!this.buttonText) { el.find(".button").remove(); } else { el.find(".button").text(this.buttonText); } // } } initInputs() { let el = this.element; if (this.inputs.length > 0) { if (this.type === "number") { this.inputs.forEach((input) => { el.find(".inputs").append(` `); }); } else if (this.type === "text") { this.inputs.forEach((input) => { if (input.type) { el.find(".inputs").append(` `); } else { el.find(".inputs").append(` `); } }); } el.find(".inputs").removeClass("hidden"); } else { el.find(".inputs").addClass("hidden"); } } exec() { let vals = []; $.each($("#simplePopup input"), (index, el) => { vals.push($(el).val()); }); this.execFn(...vals); this.hide(); } show(parameters) { this.parameters = parameters; this.beforeShowFn(); this.init(); this.wrapper .stop(true, true) .css("opacity", 0) .removeClass("hidden") .animate({ opacity: 1 }, 125, () => { $($("#simplePopup").find("input")[0]).focus(); }); } hide() { this.wrapper .stop(true, true) .css("opacity", 1) .removeClass("hidden") .animate({ opacity: 0 }, 125, () => { this.wrapper.addClass("hidden"); }); } } export function hide() { $("#simplePopupWrapper") .stop(true, true) .css("opacity", 1) .removeClass("hidden") .animate({ opacity: 0 }, 125, () => { $("#simplePopupWrapper").addClass("hidden"); }); } $("#simplePopupWrapper").mousedown((e) => { if ($(e.target).attr("id") === "simplePopupWrapper") { $("#simplePopupWrapper") .stop(true, true) .css("opacity", 1) .removeClass("hidden") .animate({ opacity: 0 }, 125, () => { $("#simplePopupWrapper").addClass("hidden"); }); } }); $(document).on("click", "#simplePopupWrapper .button", (e) => { let id = $("#simplePopup").attr("popupId"); list[id].exec(); }); $(document).on("keyup", "#simplePopupWrapper input", (e) => { if (e.key === "Enter") { e.preventDefault(); let id = $("#simplePopup").attr("popupId"); list[id].exec(); } }); list.updateEmail = new SimplePopup( "updateEmail", "text", "Update Email", [ { placeholder: "Password", type: "password", initVal: "", }, { placeholder: "New email", initVal: "", }, { placeholder: "Confirm new email", initVal: "", }, ], "", "Update", async (password, email, emailConfirm) => { try { const user = firebase.auth().currentUser; if (email !== emailConfirm) { Notifications.add("Emails don't match", 0); return; } if (user.providerData[0].providerId === "password") { const credential = firebase.auth.EmailAuthProvider.credential( user.email, password ); await user.reauthenticateWithCredential(credential); } Loader.show(); let response; try { response = await axiosInstance.post("/user/updateEmail", { uid: user.uid, previousEmail: user.email, newEmail: email, }); } catch (e) { Loader.hide(); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to update email: " + msg, -1); return; } Loader.hide(); if (response.status !== 200) { Notifications.add(response.data.message); return; } else { Notifications.add("Email updated", 1); } } catch (e) { if (e.code == "auth/wrong-password") { Notifications.add("Incorrect password", -1); } else { Notifications.add("Something went wrong: " + e, -1); } } }, () => { const user = firebase.auth().currentUser; if (!user.providerData.find((p) => p.providerId === "password")) { eval(`this.inputs = []`); eval(`this.buttonText = undefined`); eval(`this.text = "Password authentication is not enabled";`); } } ); list.updateName = new SimplePopup( "updateName", "text", "Update Name", [ { placeholder: "Password", type: "password", initVal: "", }, { placeholder: "New name", type: "text", initVal: "", }, ], "", "Update", async (pass, newName) => { try { const user = firebase.auth().currentUser; if (user.providerData[0].providerId === "password") { const credential = firebase.auth.EmailAuthProvider.credential( user.email, pass ); await user.reauthenticateWithCredential(credential); } else if (user.providerData[0].providerId === "google.com") { await user.reauthenticateWithPopup(AccountController.gmailProvider); } Loader.show(); let response; try { response = await axiosInstance.post("/user/checkName", { name: newName, }); } catch (e) { Loader.hide(); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to check name: " + msg, -1); return; } Loader.hide(); if (response.status !== 200) { Notifications.add(response.data.message); return; } try { response = await axiosInstance.post("/user/updateName", { name: newName, }); } catch (e) { Loader.hide(); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to update name: " + msg, -1); return; } Loader.hide(); if (response.status !== 200) { Notifications.add(response.data.message); return; } else { Notifications.add("Name updated", 1); DB.getSnapshot().name = newName; $("#menu .icon-button.account .text").text(newName); } } catch (e) { Loader.hide(); if (e.code == "auth/wrong-password") { Notifications.add("Incorrect password", -1); } else { Notifications.add("Something went wrong: " + e, -1); } } }, () => { const user = firebase.auth().currentUser; if (user.providerData[0].providerId === "google.com") { eval(`this.inputs[0].hidden = true`); eval(`this.buttonText = "Reauthenticate to update"`); } } ); list.updatePassword = new SimplePopup( "updatePassword", "text", "Update Password", [ { placeholder: "Password", type: "password", initVal: "", }, { placeholder: "New password", type: "password", initVal: "", }, { placeholder: "Confirm new password", type: "password", initVal: "", }, ], "", "Update", async (previousPass, newPass, newPassConfirm) => { try { const user = firebase.auth().currentUser; const credential = firebase.auth.EmailAuthProvider.credential( user.email, previousPass ); if (newPass !== newPassConfirm) { Notifications.add("New passwords don't match", 0); return; } Loader.show(); await user.reauthenticateWithCredential(credential); await user.updatePassword(newPass); Loader.hide(); Notifications.add("Password updated", 1); } catch (e) { Loader.hide(); if (e.code == "auth/wrong-password") { Notifications.add("Incorrect password", -1); } else { Notifications.add("Something went wrong: " + e, -1); } } }, () => { const user = firebase.auth().currentUser; if (!user.providerData.find((p) => p.providerId === "password")) { eval(`this.inputs = []`); eval(`this.buttonText = undefined`); eval(`this.text = "Password authentication is not enabled";`); } } ); list.addPasswordAuth = new SimplePopup( "addPasswordAuth", "text", "Add Password Authentication", [ { placeholder: "email", type: "email", initVal: "", }, { placeholder: "confirm email", type: "email", initVal: "", }, { placeholder: "new password", type: "password", initVal: "", }, { placeholder: "confirm new password", type: "password", initVal: "", }, ], "", "Add", async (email, emailConfirm, pass, passConfirm) => { if (email !== emailConfirm) { Notifications.add("Emails don't match", 0); return; } if (pass !== passConfirm) { Notifications.add("Passwords don't match", 0); return; } AccountController.addPasswordAuth(email, pass); }, () => {} ); list.deleteAccount = new SimplePopup( "deleteAccount", "text", "Delete Account", [ { placeholder: "Password", type: "password", initVal: "", }, ], "This is the last time you can change your mind. After pressing the button everything is gone.", "Delete", async (password) => { // try { const user = firebase.auth().currentUser; if (user.providerData[0].providerId === "password") { const credential = firebase.auth.EmailAuthProvider.credential( user.email, password ); await user.reauthenticateWithCredential(credential); } else if (user.providerData[0].providerId === "google.com") { await user.reauthenticateWithPopup(AccountController.gmailProvider); } Loader.show(); Notifications.add("Deleting stats...", 0); let response; try { response = await axiosInstance.post("/user/delete"); } catch (e) { Loader.hide(); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to delete user stats: " + msg, -1); return; } if (response.status !== 200) { throw response.data.message; } Notifications.add("Deleting results...", 0); try { response = await axiosInstance.post("/results/deleteAll"); } catch (e) { Loader.hide(); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to delete user results: " + msg, -1); return; } if (response.status !== 200) { throw response.data.message; } Notifications.add("Deleting login information...", 0); await firebase.auth().currentUser.delete(); Notifications.add("Goodbye", 1, 5); setTimeout(() => { location.reload(); }, 3000); } catch (e) { Loader.hide(); if (e.code == "auth/wrong-password") { Notifications.add("Incorrect password", -1); } else { Notifications.add("Something went wrong: " + e, -1); } } }, () => { const user = firebase.auth().currentUser; if (user.providerData[0].providerId === "google.com") { eval(`this.inputs = []`); eval(`this.buttonText = "Reauthenticate to delete"`); } } ); list.clearTagPb = new SimplePopup( "clearTagPb", "text", "Clear Tag PB", [], `Are you sure you want to clear this tags PB?`, "Clear", () => { let tagid = eval("this.parameters[0]"); Loader.show(); axiosInstance .post("/user/tags/clearPb", { tagid: tagid, }) .then((res) => { Loader.hide(); if (res.data.resultCode === 1) { let tag = DB.getSnapshot().tags.filter((t) => t.id === tagid)[0]; tag.pb = 0; $( `.pageSettings .section.tags .tagsList .tag[id="${tagid}"] .clearPbButton` ).attr("aria-label", "No PB found"); Notifications.add("Tag PB cleared.", 0); } else { Notifications.add("Something went wrong: " + res.data.message, -1); } }) .catch((e) => { Loader.hide(); if (e.code == "auth/wrong-password") { Notifications.add("Incorrect password", -1); } else { Notifications.add("Something went wrong: " + e, -1); } }); // console.log(`clearing for ${eval("this.parameters[0]")} ${eval("this.parameters[1]")}`); }, () => { eval( "this.text = `Are you sure you want to clear PB for tag ${eval('this.parameters[1]')}?`" ); } ); list.applyCustomFont = new SimplePopup( "applyCustomFont", "text", "Custom font", [{ placeholder: "Font name", initVal: "" }], "Make sure you have the font installed on your computer before applying.", "Apply", (fontName) => { if (fontName === "") return; Settings.groups.fontFamily?.setValue(fontName.replace(/\s/g, "_")); }, () => {} ); list.resetPersonalBests = new SimplePopup( "resetPersonalBests", "text", "Reset Personal Bests", [ { placeholder: "Password", type: "password", initVal: "", }, ], "", "Reset", async (password) => { try { const user = firebase.auth().currentUser; if (user.providerData[0].providerId === "password") { const credential = firebase.auth.EmailAuthProvider.credential( user.email, password ); await user.reauthenticateWithCredential(credential); } else if (user.providerData[0].providerId === "google.com") { await user.reauthenticateWithPopup(AccountController.gmailProvider); } Loader.show(); let response; try { response = await axiosInstance.post("/user/clearPb"); } catch (e) { Loader.hide(); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to reset personal bests: " + msg, -1); return; } Loader.hide(); if (response.status !== 200) { Notifications.add(response.data.message); } else { Notifications.add("Personal bests have been reset", 1); DB.getSnapshot().personalBests = {}; } } catch (e) { Loader.hide(); Notifications.add(e, -1); } }, () => { const user = firebase.auth().currentUser; if (user.providerData[0].providerId === "google.com") { eval(`this.inputs = []`); eval(`this.buttonText = "Reauthenticate to reset"`); } } ); list.resetSettings = new SimplePopup( "resetSettings", "text", "Reset Settings", [], "Are you sure you want to reset all your settings?", "Reset", () => { UpdateConfig.reset(); // setTimeout(() => { // location.reload(); // }, 1000); }, () => {} ); list.unlinkDiscord = new SimplePopup( "unlinkDiscord", "text", "Unlink Discord", [], "Are you sure you want to unlink your Discord account?", "Unlink", async () => { Loader.show(); let response; try { response = await axiosInstance.post("/user/discord/unlink", {}); } catch (e) { Loader.hide(); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to unlink Discord: " + msg, -1); return; } Loader.hide(); if (response.status !== 200) { Notifications.add(response.data.message); } else { Notifications.add("Accounts unlinked", 1); DB.getSnapshot().discordId = undefined; Settings.updateDiscordSection(); } }, () => {} ); ==> ./monkeytype/src/js/settings/settings-group.js <== import Config from "./config"; export default class SettingsGroup { constructor( configName, toggleFunction, setCallback = null, updateCallback = null ) { this.configName = configName; this.configValue = Config[configName]; this.onOff = typeof this.configValue === "boolean"; this.toggleFunction = toggleFunction; this.setCallback = setCallback; this.updateCallback = updateCallback; this.updateButton(); $(document).on( "click", `.pageSettings .section.${this.configName} .button`, (e) => { let target = $(e.currentTarget); if (target.hasClass("disabled") || target.hasClass("no-auto-handle")) return; if (this.onOff) { if (target.hasClass("on")) { this.setValue(true); } else { this.setValue(false); } this.updateButton(); if (this.setCallback !== null) this.setCallback(); } else { const value = target.attr(configName); const params = target.attr("params"); if (!value && !params) return; this.setValue(value, params); } } ); } setValue(value, params = undefined) { if (params === undefined) { this.toggleFunction(value); } else { this.toggleFunction(value, ...params); } this.updateButton(); if (this.setCallback !== null) this.setCallback(); } updateButton() { this.configValue = Config[this.configName]; $(`.pageSettings .section.${this.configName} .button`).removeClass( "active" ); if (this.onOff) { const onOffString = this.configValue ? "on" : "off"; $( `.pageSettings .section.${this.configName} .buttons .button.${onOffString}` ).addClass("active"); } else { $( `.pageSettings .section.${this.configName} .button[${this.configName}='${this.configValue}']` ).addClass("active"); } if (this.updateCallback !== null) this.updateCallback(); } } ==> ./monkeytype/src/js/settings/language-picker.js <== import * as Misc from "./misc"; import Config, * as UpdateConfig from "./config"; export async function setActiveGroup(groupName, clicked = false) { let currentGroup; if (groupName === undefined) { currentGroup = await Misc.findCurrentGroup(Config.language); } else { let groups = await Misc.getLanguageGroups(); groups.forEach((g) => { if (g.name === groupName) { currentGroup = g; } }); } $(`.pageSettings .section.languageGroups .button`).removeClass("active"); $( `.pageSettings .section.languageGroups .button[group='${currentGroup.name}']` ).addClass("active"); let langEl = $(".pageSettings .section.language .buttons").empty(); currentGroup.languages.forEach((language) => { langEl.append( `
${language.replace( /_/g, " " )}
` ); }); if (clicked) { $($(`.pageSettings .section.language .buttons .button`)[0]).addClass( "active" ); UpdateConfig.setLanguage(currentGroup.languages[0]); } else { $( `.pageSettings .section.language .buttons .button[language=${Config.language}]` ).addClass("active"); } } ==> ./monkeytype/src/js/settings/theme-picker.js <== import Config, * as UpdateConfig from "./config"; import * as ThemeController from "./theme-controller"; import * as Misc from "./misc"; import * as Notifications from "./notifications"; import * as CommandlineLists from "./commandline-lists"; import * as ThemeColors from "./theme-colors"; import * as ChartController from "./chart-controller"; export function updateActiveButton() { let activeThemeName = Config.theme; if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) { activeThemeName = ThemeController.randomTheme; } $(`.pageSettings .section.themes .theme`).removeClass("active"); $(`.pageSettings .section.themes .theme[theme=${activeThemeName}]`).addClass( "active" ); } function updateColors(colorPicker, color, onlyStyle, noThemeUpdate = false) { if (onlyStyle) { let colorid = colorPicker.find("input[type=color]").attr("id"); if (!noThemeUpdate) document.documentElement.style.setProperty(colorid, color); let pickerButton = colorPicker.find("label"); pickerButton.val(color); pickerButton.attr("value", color); if (pickerButton.attr("for") !== "--bg-color") pickerButton.css("background-color", color); colorPicker.find("input[type=text]").val(color); colorPicker.find("input[type=color]").attr("value", color); return; } let colorREGEX = [ { rule: /\b[0-9]{1,3},\s?[0-9]{1,3},\s?[0-9]{1,3}\s*\b/, start: "rgb(", end: ")", }, { rule: /\b[A-Z, a-z, 0-9]{6}\b/, start: "#", end: "", }, { rule: /\b[0-9]{1,3},\s?[0-9]{1,3}%,\s?[0-9]{1,3}%?\s*\b/, start: "hsl(", end: ")", }, ]; color = color.replace("°", ""); for (let regex of colorREGEX) { if (color.match(regex.rule)) { color = regex.start + color + regex.end; break; } } $(".colorConverter").css("color", color); color = Misc.convertRGBtoHEX($(".colorConverter").css("color")); if (!color) { return; } let colorid = colorPicker.find("input[type=color]").attr("id"); if (!noThemeUpdate) document.documentElement.style.setProperty(colorid, color); let pickerButton = colorPicker.find("label"); pickerButton.val(color); pickerButton.attr("value", color); if (pickerButton.attr("for") !== "--bg-color") pickerButton.css("background-color", color); colorPicker.find("input[type=text]").val(color); colorPicker.find("input[type=color]").attr("value", color); } export function refreshButtons() { let favThemesEl = $( ".pageSettings .section.themes .favThemes.buttons" ).empty(); let themesEl = $(".pageSettings .section.themes .allThemes.buttons").empty(); let activeThemeName = Config.theme; if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) { activeThemeName = ThemeController.randomTheme; } Misc.getSortedThemesList().then((themes) => { //first show favourites if (Config.favThemes.length > 0) { favThemesEl.css({ paddingBottom: "1rem" }); themes.forEach((theme) => { if (Config.favThemes.includes(theme.name)) { let activeTheme = activeThemeName === theme.name ? "active" : ""; favThemesEl.append( `
${theme.name.replace(/_/g, " ")}
` ); } }); } else { favThemesEl.css({ paddingBottom: "0" }); } //then the rest themes.forEach((theme) => { if (!Config.favThemes.includes(theme.name)) { let activeTheme = activeThemeName === theme.name ? "active" : ""; themesEl.append( `
${theme.name.replace(/_/g, " ")}
` ); } }); updateActiveButton(); }); } export function setCustomInputs(noThemeUpdate) { $( ".pageSettings .section.themes .tabContainer .customTheme .colorPicker" ).each((n, index) => { let currentColor = Config.customThemeColors[ ThemeController.colorVars.indexOf( $(index).find("input[type=color]").attr("id") ) ]; //todo check if needed // $(index).find("input[type=color]").val(currentColor); // $(index).find("input[type=color]").attr("value", currentColor); // $(index).find("input[type=text]").val(currentColor); updateColors($(index), currentColor, false, noThemeUpdate); }); } function toggleFavourite(themename) { if (Config.favThemes.includes(themename)) { //already favourite, remove UpdateConfig.setFavThemes( Config.favThemes.filter((t) => { if (t !== themename) { return t; } }) ); } else { //add to favourites let newlist = Config.favThemes; newlist.push(themename); UpdateConfig.setFavThemes(newlist); } UpdateConfig.saveToLocalStorage(); refreshButtons(); // showFavouriteThemesAtTheTop(); CommandlineLists.updateThemeCommands(); } export function updateActiveTab() { $(".pageSettings .section.themes .tabs .button").removeClass("active"); if (!Config.customTheme) { $(".pageSettings .section.themes .tabs .button[tab='preset']").addClass( "active" ); // UI.swapElements( // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), // 250 // ); } else { $(".pageSettings .section.themes .tabs .button[tab='custom']").addClass( "active" ); // UI.swapElements( // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), // 250 // ); } } $(".pageSettings .section.themes .tabs .button").click((e) => { $(".pageSettings .section.themes .tabs .button").removeClass("active"); var $target = $(e.currentTarget); $target.addClass("active"); setCustomInputs(); if ($target.attr("tab") == "preset") { UpdateConfig.setCustomTheme(false); // ThemeController.set(Config.theme); // applyCustomThemeColors(); // UI.swapElements( // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), // 250 // ); } else { UpdateConfig.setCustomTheme(true); // ThemeController.set("custom"); // applyCustomThemeColors(); // UI.swapElements( // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), // 250 // ); } }); $(document).on( "click", ".pageSettings .section.themes .theme .favButton", (e) => { let theme = $(e.currentTarget).parents(".theme.button").attr("theme"); toggleFavourite(theme); } ); $(document).on("click", ".pageSettings .section.themes .theme.button", (e) => { let theme = $(e.currentTarget).attr("theme"); if (!$(e.target).hasClass("favButton")) { UpdateConfig.setTheme(theme); // ThemePicker.refreshButtons(); updateActiveButton(); } }); $( ".pageSettings .section.themes .tabContainer .customTheme input[type=color]" ).on("input", (e) => { // UpdateConfig.setCustomTheme(true, true); let $colorVar = $(e.currentTarget).attr("id"); let $pickedColor = $(e.currentTarget).val(); //todo check if needed // document.documentElement.style.setProperty($colorVar, $pickedColor); // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); // $(".colorPicker #" + $colorVar).val($pickedColor); // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); // }); // $( // ".pageSettings .section.themes .tabContainer .customTheme input[type=text]" // ).on("input", (e) => { // // UpdateConfig.setCustomTheme(true, true); // let $colorVar = $(e.currentTarget).attr("id").replace("-txt", ""); // let $pickedColor = $(e.currentTarget).val(); // document.documentElement.style.setProperty($colorVar, $pickedColor); // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); // $(".colorPicker #" + $colorVar).val($pickedColor); // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor, true); }); $( ".pageSettings .section.themes .tabContainer .customTheme input[type=color]" ).on("change", (e) => { // UpdateConfig.setCustomTheme(true, true); let $colorVar = $(e.currentTarget).attr("id"); let $pickedColor = $(e.currentTarget).val(); //todo check if needed // document.documentElement.style.setProperty($colorVar, $pickedColor); // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); // $(".colorPicker #" + $colorVar).val($pickedColor); // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); // }); // $( // ".pageSettings .section.themes .tabContainer .customTheme input[type=text]" // ).on("input", (e) => { // // UpdateConfig.setCustomTheme(true, true); // let $colorVar = $(e.currentTarget).attr("id").replace("-txt", ""); // let $pickedColor = $(e.currentTarget).val(); // document.documentElement.style.setProperty($colorVar, $pickedColor); // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); // $(".colorPicker #" + $colorVar).val($pickedColor); // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); }); $(".pageSettings .section.themes .tabContainer .customTheme input[type=text]") .on("blur", (e) => { let $colorVar = $(e.currentTarget).attr("id"); let $pickedColor = $(e.currentTarget).val(); updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); }) .on("keypress", function (e) { if (e.which === 13) { $(this).attr("disabled", "disabled"); let $colorVar = $(e.currentTarget).attr("id"); let $pickedColor = $(e.currentTarget).val(); updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); $(this).removeAttr("disabled"); } }); $(".pageSettings .saveCustomThemeButton").click((e) => { let save = []; $.each( $(".pageSettings .section.customTheme [type='color']"), (index, element) => { save.push($(element).attr("value")); } ); UpdateConfig.setCustomThemeColors(save); ThemeController.set("custom"); Notifications.add("Custom theme colors saved", 1); }); $(".pageSettings #loadCustomColorsFromPreset").click((e) => { // previewTheme(Config.theme); $("#currentTheme").attr("href", `themes/${Config.theme}.css`); ThemeController.colorVars.forEach((e) => { document.documentElement.style.setProperty(e, ""); }); setTimeout(async () => { ChartController.updateAllChartColors(); let themecolors = await ThemeColors.get(); ThemeController.colorVars.forEach((colorName) => { let color; if (colorName === "--bg-color") { color = themecolors.bg; } else if (colorName === "--main-color") { color = themecolors.main; } else if (colorName === "--sub-color") { color = themecolors.sub; } else if (colorName === "--caret-color") { color = themecolors.caret; } else if (colorName === "--text-color") { color = themecolors.text; } else if (colorName === "--error-color") { color = themecolors.error; } else if (colorName === "--error-extra-color") { color = themecolors.errorExtra; } else if (colorName === "--colorful-error-color") { color = themecolors.colorfulError; } else if (colorName === "--colorful-error-extra-color") { color = themecolors.colorfulErrorExtra; } updateColors($(".colorPicker #" + colorName).parent(), color); }); }, 250); }); ==> ./monkeytype/src/js/challenge-controller.js <== import * as Misc from "./misc"; import * as Notifications from "./notifications"; import * as ManualRestart from "./manual-restart-tracker"; import * as CustomText from "./custom-text"; import * as TestLogic from "./test-logic"; import * as Funbox from "./funbox"; import Config, * as UpdateConfig from "./config"; import * as UI from "./ui"; import * as TestUI from "./test-ui"; export let active = null; let challengeLoading = false; export function clearActive() { if (active && !challengeLoading && !TestUI.testRestarting) { Notifications.add("Challenge cleared", 0); active = null; } } export function verify(result) { try { if (active) { let afk = (result.afkDuration / result.testDuration) * 100; if (afk > 10) { Notifications.add(`Challenge failed: AFK time is greater than 10%`, 0); return null; } if (!active.requirements) { Notifications.add(`${active.display} challenge passed!`, 1); return active.name; } else { let requirementsMet = true; let failReasons = []; for (let requirementType in active.requirements) { if (requirementsMet == false) return; let requirementValue = active.requirements[requirementType]; if (requirementType == "wpm") { let wpmMode = Object.keys(requirementValue)[0]; if (wpmMode == "exact") { if (Math.round(result.wpm) != requirementValue.exact) { requirementsMet = false; failReasons.push(`WPM not ${requirementValue.exact}`); } } else if (wpmMode == "min") { if (result.wpm < requirementValue.min) { requirementsMet = false; failReasons.push(`WPM below ${requirementValue.min}`); } } } else if (requirementType == "acc") { let accMode = Object.keys(requirementValue)[0]; if (accMode == "exact") { if (result.acc != requirementValue.exact) { requirementsMet = false; failReasons.push(`Accuracy not ${requirementValue.exact}`); } } else if (accMode == "min") { if (result.acc < requirementValue.min) { requirementsMet = false; failReasons.push(`Accuracy below ${requirementValue.min}`); } } } else if (requirementType == "afk") { let afkMode = Object.keys(requirementValue)[0]; if (afkMode == "max") { if (Math.round(afk) > requirementValue.max) { requirementsMet = false; failReasons.push( `AFK percentage above ${requirementValue.max}` ); } } } else if (requirementType == "time") { let timeMode = Object.keys(requirementValue)[0]; if (timeMode == "min") { if (Math.round(result.testDuration) < requirementValue.min) { requirementsMet = false; failReasons.push(`Test time below ${requirementValue.min}`); } } } else if (requirementType == "funbox") { let funboxMode = requirementValue; if (funboxMode != result.funbox) { requirementsMet = false; failReasons.push(`${funboxMode} funbox not active`); } } else if (requirementType == "raw") { let rawMode = Object.keys(requirementValue)[0]; if (rawMode == "exact") { if (Math.round(result.rawWpm) != requirementValue.exact) { requirementsMet = false; failReasons.push(`Raw WPM not ${requirementValue.exact}`); } } } else if (requirementType == "con") { let conMode = Object.keys(requirementValue)[0]; if (conMode == "exact") { if (Math.round(result.consistency) != requirementValue.exact) { requirementsMet = false; failReasons.push(`Consistency not ${requirementValue.exact}`); } } } else if (requirementType == "config") { for (let configKey in requirementValue) { let configValue = requirementValue[configKey]; if (Config[configKey] != configValue) { requirementsMet = false; failReasons.push(`${configKey} not set to ${configValue}`); } } } } if (requirementsMet) { if (active.autoRole) { Notifications.add( "You will receive a role shortly. Please don't post a screenshot in challenge submissions.", 1, 5 ); } Notifications.add(`${active.display} challenge passed!`, 1); return active.name; } else { Notifications.add( `${active.display} challenge failed: ${failReasons.join(", ")}`, 0 ); return null; } } } else { return null; } } catch (e) { console.error(e); Notifications.add( `Something went wrong when verifying challenge: ${e.message}`, 0 ); return null; } } export async function setup(challengeName) { challengeLoading = true; if (UI.getActivePage() !== "pageTest") { UI.changePage("", true); } let list = await Misc.getChallengeList(); let challenge = list.filter((c) => c.name === challengeName)[0]; let notitext; try { if (challenge === undefined) { Notifications.add("Challenge not found", 0); ManualRestart.set(); TestLogic.restart(false, true); setTimeout(() => { $("#top .config").removeClass("hidden"); $(".page.pageTest").removeClass("hidden"); }, 250); return; } if (challenge.type === "customTime") { UpdateConfig.setTimeConfig(challenge.parameters[0], true); UpdateConfig.setMode("time", true); UpdateConfig.setDifficulty("normal", true); if (challenge.name === "englishMaster") { UpdateConfig.setLanguage("english_10k", true); UpdateConfig.setNumbers(true, true); UpdateConfig.setPunctuation(true, true); } } else if (challenge.type === "customWords") { UpdateConfig.setWordCount(challenge.parameters[0], true); UpdateConfig.setMode("words", true); UpdateConfig.setDifficulty("normal", true); } else if (challenge.type === "customText") { CustomText.setText(challenge.parameters[0].split(" ")); CustomText.setIsWordRandom(challenge.parameters[1]); CustomText.setWord(parseInt(challenge.parameters[2])); UpdateConfig.setMode("custom", true); UpdateConfig.setDifficulty("normal", true); } else if (challenge.type === "script") { let scriptdata = await fetch("/challenges/" + challenge.parameters[0]); scriptdata = await scriptdata.text(); let text = scriptdata.trim(); text = text.replace(/[\n\rt ]/gm, " "); text = text.replace(/ +/gm, " "); CustomText.setText(text.split(" ")); CustomText.setIsWordRandom(false); UpdateConfig.setMode("custom", true); UpdateConfig.setDifficulty("normal", true); if (challenge.parameters[1] != null) { UpdateConfig.setTheme(challenge.parameters[1]); } if (challenge.parameters[2] != null) { Funbox.activate(challenge.parameters[2]); } } else if (challenge.type === "accuracy") { UpdateConfig.setTimeConfig(0, true); UpdateConfig.setMode("time", true); UpdateConfig.setDifficulty("master", true); } else if (challenge.type === "funbox") { UpdateConfig.setFunbox(challenge.parameters[0], true); UpdateConfig.setDifficulty("normal", true); if (challenge.parameters[1] === "words") { UpdateConfig.setWordCount(challenge.parameters[2], true); } else if (challenge.parameters[1] === "time") { UpdateConfig.setTimeConfig(challenge.parameters[2], true); } UpdateConfig.setMode(challenge.parameters[1], true); if (challenge.parameters[3] !== undefined) { UpdateConfig.setDifficulty(challenge.parameters[3], true); } } else if (challenge.type === "special") { if (challenge.name === "semimak") { // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout? UpdateConfig.setMode("time", true); UpdateConfig.setTimeConfig(120, true); UpdateConfig.setLanguage("english_10k", true); UpdateConfig.setPunctuation(true, true); UpdateConfig.setStopOnError("word", true); UpdateConfig.setLayout("semimak", true); UpdateConfig.setKeymapLayout("overrideSync", true); UpdateConfig.setKeymapMode("static", true); } } ManualRestart.set(); TestLogic.restart(false, true); notitext = challenge.message; $("#top .config").removeClass("hidden"); $(".page.pageTest").removeClass("hidden"); if (notitext === undefined) { Notifications.add(`Challenge '${challenge.display}' loaded.`, 0); } else { Notifications.add("Challenge loaded. " + notitext, 0); } active = challenge; challengeLoading = false; } catch (e) { Notifications.add("Something went wrong: " + e, -1); } } ==> ./monkeytype/src/js/ui.js <== import Config, * as UpdateConfig from "./config"; import * as Notifications from "./notifications"; import * as Caret from "./caret"; import * as TestLogic from "./test-logic"; import * as CustomText from "./custom-text"; import * as CommandlineLists from "./commandline-lists"; import * as Commandline from "./commandline"; import * as TestUI from "./test-ui"; import * as TestConfig from "./test-config"; import * as SignOutButton from "./sign-out-button"; import * as TestStats from "./test-stats"; import * as ManualRestart from "./manual-restart-tracker"; import * as Settings from "./settings"; import * as Account from "./account"; import * as Leaderboards from "./leaderboards"; import * as Funbox from "./funbox"; import * as About from "./about-page"; export let pageTransition = true; let activePage = "pageLoading"; export function getActivePage() { return activePage; } export function setActivePage(active) { activePage = active; } export function setPageTransition(val) { pageTransition = val; } export function updateKeytips() { if (Config.swapEscAndTab) { $(".pageSettings .tip").html(` tip: You can also change all these settings quickly using the command line ( tab )`); $("#bottom .keyTips").html(` esc - restart test
tab - command line`); } else { $(".pageSettings .tip").html(` tip: You can also change all these settings quickly using the command line ( esc )`); $("#bottom .keyTips").html(` tab - restart test
esc or ctrl/cmd+shift+p - command line`); } } export function swapElements( el1, el2, totalDuration, callback = function () { return; }, middleCallback = function () { return; } ) { if ( (el1.hasClass("hidden") && !el2.hasClass("hidden")) || (!el1.hasClass("hidden") && el2.hasClass("hidden")) ) { //one of them is hidden and the other is visible if (el1.hasClass("hidden")) { callback(); return false; } $(el1) .removeClass("hidden") .css("opacity", 1) .animate( { opacity: 0, }, totalDuration / 2, () => { middleCallback(); $(el1).addClass("hidden"); $(el2) .removeClass("hidden") .css("opacity", 0) .animate( { opacity: 1, }, totalDuration / 2, () => { callback(); } ); } ); } else if (el1.hasClass("hidden") && el2.hasClass("hidden")) { //both are hidden, only fade in the second $(el2) .removeClass("hidden") .css("opacity", 0) .animate( { opacity: 1, }, totalDuration, () => { callback(); } ); } else { callback(); } } export function changePage(page, norestart = false) { if (pageTransition) { console.log(`change page ${page} stopped`); return; } if (page == undefined) { //use window loacation let pages = { "/": "test", "/login": "login", "/settings": "settings", "/about": "about", "/account": "account", }; let path = pages[window.location.pathname]; if (!path) { path = "test"; } page = path; } console.log(`change page ${page}`); let activePageElement = $(".page.active"); let check = activePage + ""; setTimeout(() => { if (check === "pageAccount" && page !== "account") { Account.reset(); } else if (check === "pageSettings" && page !== "settings") { Settings.reset(); } else if (check === "pageAbout" && page !== "about") { About.reset(); } }, 250); activePage = undefined; $(".page").removeClass("active"); $("#wordsInput").focusout(); if (page == "test" || page == "") { setPageTransition(true); swapElements( activePageElement, $(".page.pageTest"), 250, () => { setPageTransition(false); TestUI.focusWords(); $(".page.pageTest").addClass("active"); activePage = "pageTest"; history.pushState("/", null, "/"); }, () => { TestConfig.show(); } ); SignOutButton.hide(); // restartCount = 0; // incompleteTestSeconds = 0; TestStats.resetIncomplete(); ManualRestart.set(); if (!norestart) TestLogic.restart(); Funbox.activate(Config.funbox); } else if (page == "about") { setPageTransition(true); TestLogic.restart(); swapElements(activePageElement, $(".page.pageAbout"), 250, () => { setPageTransition(false); history.pushState("about", null, "about"); $(".page.pageAbout").addClass("active"); activePage = "pageAbout"; }); About.fill(); Funbox.activate("none"); TestConfig.hide(); SignOutButton.hide(); } else if (page == "settings") { setPageTransition(true); TestLogic.restart(); swapElements(activePageElement, $(".page.pageSettings"), 250, () => { setPageTransition(false); history.pushState("settings", null, "settings"); $(".page.pageSettings").addClass("active"); activePage = "pageSettings"; }); Funbox.activate("none"); Settings.fillSettingsPage().then(() => { Settings.update(); }); // Settings.update(); TestConfig.hide(); SignOutButton.hide(); } else if (page == "account") { if (!firebase.auth().currentUser) { console.log( `current user is ${firebase.auth().currentUser}, going back to login` ); changePage("login"); } else { setPageTransition(true); TestLogic.restart(); swapElements(activePageElement, $(".page.pageAccount"), 250, () => { setPageTransition(false); history.pushState("account", null, "account"); $(".page.pageAccount").addClass("active"); activePage = "pageAccount"; }); Funbox.activate("none"); Account.update(); TestConfig.hide(); } } else if (page == "login") { if (firebase.auth().currentUser != null) { changePage("account"); } else { setPageTransition(true); TestLogic.restart(); swapElements(activePageElement, $(".page.pageLogin"), 250, () => { setPageTransition(false); history.pushState("login", null, "login"); $(".page.pageLogin").addClass("active"); activePage = "pageLogin"; }); Funbox.activate("none"); TestConfig.hide(); SignOutButton.hide(); } } } //checking if the project is the development site /* if (firebase.app().options.projectId === "monkey-type-dev-67af4") { $("#top .logo .bottom").text("monkey-dev"); $("head title").text("Monkey Dev"); $("body").append( `
DEV
DEV
` ); } */ if (window.location.hostname === "localhost") { window.onerror = function (error) { Notifications.add(error, -1); }; $("#top .logo .top").text("localhost"); $("head title").text($("head title").text() + " (localhost)"); //firebase.functions().useFunctionsEmulator("http://localhost:5001"); $("body").append( `
local
local
` ); $(".pageSettings .discordIntegration .buttons a").attr( "href", "https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fverify&response_type=token&scope=identify" ); } //stop space scrolling window.addEventListener("keydown", function (e) { if (e.keyCode == 32 && e.target == document.body) { e.preventDefault(); } }); $(document).on("click", "#bottom .leftright .right .current-theme", (e) => { if (e.shiftKey) { UpdateConfig.toggleCustomTheme(); } else { // if (Config.customTheme) { // toggleCustomTheme(); // } CommandlineLists.pushCurrent(CommandlineLists.themeCommands); Commandline.show(); } }); $(document.body).on("click", ".pageAbout .aboutEnableAds", () => { CommandlineLists.pushCurrent(CommandlineLists.commandsEnableAds); Commandline.show(); }); window.addEventListener("beforeunload", (event) => { // Cancel the event as stated by the standard. if ( (Config.mode === "words" && Config.words < 1000) || (Config.mode === "time" && Config.time < 3600) || Config.mode === "quote" || (Config.mode === "custom" && CustomText.isWordRandom && CustomText.word < 1000) || (Config.mode === "custom" && CustomText.isTimeRandom && CustomText.time < 1000) || (Config.mode === "custom" && !CustomText.isWordRandom && CustomText.text.length < 1000) ) { //ignore } else { if (TestLogic.active) { event.preventDefault(); // Chrome requires returnValue to be set. event.returnValue = ""; } } }); $(window).resize(() => { Caret.updatePosition(); }); $(document).on("click", "#top .logo", (e) => { changePage("test"); }); $(document).on("click", "#top #menu .icon-button", (e) => { if ($(e.currentTarget).hasClass("leaderboards")) { Leaderboards.show(); } else { const href = $(e.currentTarget).attr("href"); ManualRestart.set(); changePage(href.replace("/", "")); } return false; }); ==> ./monkeytype/src/js/axios-instance.js <== import axios from "axios"; let apiPath = ""; let baseURL; if (window.location.hostname === "localhost") { baseURL = "http://localhost:5005" + apiPath; } else { baseURL = "https://api.monkeytype.com" + apiPath; } const axiosInstance = axios.create({ baseURL: baseURL, timeout: 10000, }); // Request interceptor for API calls axiosInstance.interceptors.request.use( async (config) => { let idToken; if (firebase.auth().currentUser != null) { idToken = await firebase.auth().currentUser.getIdToken(); } else { idToken = null; } if (idToken) { config.headers = { Authorization: `Bearer ${idToken}`, Accept: "application/json", "Content-Type": "application/json", }; } else { config.headers = { Accept: "application/json", "Content-Type": "application/json", }; } return config; }, (error) => { Promise.reject(error); } ); axiosInstance.interceptors.response.use( (response) => response, (error) => { // whatever you want to do with the error // console.log('interctepted'); // if(error.response.data.message){ // Notifications.add(`${error.response.data.message}`); // }else{ // Notifications.add(`${error.response.status} ${error.response.statusText}`); // } // return error.response; throw error; } ); export default axiosInstance; ==> ./monkeytype/src/js/commandline.js <== import * as Leaderboards from "./leaderboards"; import * as ThemeController from "./theme-controller"; import Config, * as UpdateConfig from "./config"; import * as Focus from "./focus"; import * as CommandlineLists from "./commandline-lists"; import * as TestUI from "./test-ui"; import * as PractiseWords from "./practise-words"; import * as SimplePopups from "./simple-popups"; import * as CustomWordAmountPopup from "./custom-word-amount-popup"; import * as CustomTestDurationPopup from "./custom-test-duration-popup"; import * as CustomTextPopup from "./custom-text-popup"; import * as QuoteSearchPopupWrapper from "./quote-search-popup"; let commandLineMouseMode = false; function showInput(command, placeholder, defaultValue = "") { $("#commandLineWrapper").removeClass("hidden"); $("#commandLine").addClass("hidden"); $("#commandInput").removeClass("hidden"); $("#commandInput input").attr("placeholder", placeholder); $("#commandInput input").val(defaultValue); $("#commandInput input").focus(); $("#commandInput input").attr("command", ""); $("#commandInput input").attr("command", command); if (defaultValue != "") { $("#commandInput input").select(); } } export function isSingleListCommandLineActive() { return $("#commandLine").hasClass("allCommands"); } function showFound() { $("#commandLine .suggestions").empty(); let commandsHTML = ""; let list = CommandlineLists.current[CommandlineLists.current.length - 1]; $.each(list.list, (index, obj) => { if (obj.found && (obj.available !== undefined ? obj.available() : true)) { let icon = obj.icon ?? "fa-chevron-right"; let faIcon = /^fa-/g.test(icon); if (!faIcon) { icon = `
${icon}
`; } else { icon = ``; } if (list.configKey) { if ( (obj.configValueMode && obj.configValueMode === "include" && Config[list.configKey].includes(obj.configValue)) || Config[list.configKey] === obj.configValue ) { icon = ``; } else { icon = ``; } } let iconHTML = `
${icon}
`; if (obj.noIcon && !isSingleListCommandLineActive()) { iconHTML = ""; } commandsHTML += `
${iconHTML}
${obj.display}
`; } }); $("#commandLine .suggestions").html(commandsHTML); if ($("#commandLine .suggestions .entry").length == 0) { $("#commandLine .separator").css({ height: 0, margin: 0 }); } else { $("#commandLine .separator").css({ height: "1px", "margin-bottom": ".5rem", }); } let entries = $("#commandLine .suggestions .entry"); if (entries.length > 0) { $(entries[0]).addClass("activeKeyboard"); try { $.each(list.list, (index, obj) => { if (obj.found) { if ( (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && !ThemeController.randomTheme ) ThemeController.clearPreview(); if (!/font/gi.test(obj.id)) UpdateConfig.previewFontFamily(Config.fontFamily); obj.hover(); return false; } }); } catch (e) {} } $("#commandLine .listTitle").remove(); } function updateSuggested() { let inputVal = $("#commandLine input") .val() .toLowerCase() .split(" ") .filter((s, i) => s || i == 0); //remove empty entries after first let list = CommandlineLists.current[CommandlineLists.current.length - 1]; if ( inputVal[0] === "" && Config.singleListCommandLine === "on" && CommandlineLists.current.length === 1 ) { $.each(list.list, (index, obj) => { obj.found = false; }); showFound(); return; } //ignore the preceeding ">"s in the command line input if (inputVal[0] && inputVal[0][0] == ">") inputVal[0] = inputVal[0].replace(/^>+/, ""); if (inputVal[0] == "" && inputVal.length == 1) { $.each(list.list, (index, obj) => { if (obj.visible !== false) obj.found = true; }); } else { $.each(list.list, (index, obj) => { let foundcount = 0; $.each(inputVal, (index2, obj2) => { if (obj2 == "") return; let escaped = obj2.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); let re = new RegExp("\\b" + escaped, "g"); let res = obj.display.toLowerCase().match(re); let res2 = obj.alias !== undefined ? obj.alias.toLowerCase().match(re) : null; if ( (res != null && res.length > 0) || (res2 != null && res2.length > 0) ) { foundcount++; } else { foundcount--; } }); if (foundcount > inputVal.length - 1) { obj.found = true; } else { obj.found = false; } }); } showFound(); } export let show = () => { if (!$(".page.pageLoading").hasClass("hidden")) return; Focus.set(false); $("#commandLine").removeClass("hidden"); $("#commandInput").addClass("hidden"); if ($("#commandLineWrapper").hasClass("hidden")) { $("#commandLineWrapper") .stop(true, true) .css("opacity", 0) .removeClass("hidden") .animate( { opacity: 1, }, 100 ); } $("#commandLine input").val(""); updateSuggested(); $("#commandLine input").focus(); }; function hide() { UpdateConfig.previewFontFamily(Config.fontFamily); // applyCustomThemeColors(); if (!ThemeController.randomTheme) { ThemeController.clearPreview(); } $("#commandLineWrapper") .stop(true, true) .css("opacity", 1) .animate( { opacity: 0, }, 100, () => { $("#commandLineWrapper").addClass("hidden"); $("#commandLine").removeClass("allCommands"); TestUI.focusWords(); } ); TestUI.focusWords(); } function trigger(command) { let subgroup = false; let input = false; let list = CommandlineLists.current[CommandlineLists.current.length - 1]; let sticky = false; $.each(list.list, (i, obj) => { if (obj.id == command) { if (obj.input) { input = true; let escaped = obj.display.split("")[1] ?? obj.display; showInput(obj.id, escaped, obj.defaultValue); } else if (obj.subgroup) { subgroup = true; if (obj.beforeSubgroup) { obj.beforeSubgroup(); } CommandlineLists.current.push(obj.subgroup); show(); } else { obj.exec(); if (obj.sticky === true) { sticky = true; } } } }); if (!subgroup && !input && !sticky) { try { firebase.analytics().logEvent("usedCommandLine", { command: command, }); } catch (e) { console.log("Analytics unavailable"); } hide(); } } function addChildCommands( unifiedCommands, commandItem, parentCommandDisplay = "", parentCommand = "" ) { let commandItemDisplay = commandItem.display.replace(/\s?\.\.\.$/g, ""); let icon = ``; if ( commandItem.configValue !== undefined && Config[parentCommand.configKey] === commandItem.configValue ) { icon = ``; } if (commandItem.noIcon) { icon = ""; } if (parentCommandDisplay) commandItemDisplay = parentCommandDisplay + " > " + icon + commandItemDisplay; if (commandItem.subgroup) { if (commandItem.beforeSubgroup) commandItem.beforeSubgroup(); try { commandItem.subgroup.list.forEach((cmd) => { commandItem.configKey = commandItem.subgroup.configKey; addChildCommands(unifiedCommands, cmd, commandItemDisplay, commandItem); }); // commandItem.exec(); // const currentCommandsIndex = CommandlineLists.current.length - 1; // CommandlineLists.current[currentCommandsIndex].list.forEach((cmd) => { // if (cmd.alias === undefined) cmd.alias = commandItem.alias; // addChildCommands(unifiedCommands, cmd, commandItemDisplay); // }); // CommandlineLists.current.pop(); } catch (e) {} } else { let tempCommandItem = { ...commandItem }; tempCommandItem.icon = parentCommand.icon; if (parentCommandDisplay) tempCommandItem.display = commandItemDisplay; unifiedCommands.push(tempCommandItem); } } function generateSingleListOfCommands() { const allCommands = []; const oldShowCommandLine = show; show = () => {}; CommandlineLists.defaultCommands.list.forEach((c) => addChildCommands(allCommands, c) ); show = oldShowCommandLine; return { title: "All Commands", list: allCommands, }; } function useSingleListCommandLine(sshow = true) { let allCommands = generateSingleListOfCommands(); // if (Config.singleListCommandLine == "manual") { // CommandlineLists.pushCurrent(allCommands); // } else if (Config.singleListCommandLine == "on") { CommandlineLists.setCurrent([allCommands]); // } if (Config.singleListCommandLine != "off") $("#commandLine").addClass("allCommands"); if (sshow) show(); } function restoreOldCommandLine(sshow = true) { if (isSingleListCommandLineActive()) { $("#commandLine").removeClass("allCommands"); CommandlineLists.setCurrent( CommandlineLists.current.filter((l) => l.title != "All Commands") ); if (CommandlineLists.current.length < 1) CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); } if (sshow) show(); } $("#commandLine input").keyup((e) => { commandLineMouseMode = false; $("#commandLineWrapper #commandLine .suggestions .entry").removeClass( "activeMouse" ); if ( e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Enter" || e.key === "Tab" || e.code == "AltLeft" || (e.key.length > 1 && e.key !== "Backspace" && e.key !== "Delete") ) return; updateSuggested(); }); $(document).ready((e) => { $(document).keydown((event) => { // opens command line if escape, ctrl/cmd + shift + p, or tab is pressed if the setting swapEscAndTab is enabled if ( event.key === "Escape" || (event.key && event.key.toLowerCase() === "p" && (event.metaKey || event.ctrlKey) && event.shiftKey) || (event.key === "Tab" && Config.swapEscAndTab) ) { event.preventDefault(); if (!$("#leaderboardsWrapper").hasClass("hidden")) { //maybe add more condition for closing other dialogs in the future as well event.preventDefault(); Leaderboards.hide(); } else if (!$("#practiseWordsPopupWrapper").hasClass("hidden")) { event.preventDefault(); PractiseWords.hide(); } else if (!$("#simplePopupWrapper").hasClass("hidden")) { event.preventDefault(); SimplePopups.hide(); } else if (!$("#customWordAmountPopupWrapper").hasClass("hidden")) { event.preventDefault(); CustomWordAmountPopup.hide(); } else if (!$("#customTestDurationPopupWrapper").hasClass("hidden")) { event.preventDefault(); CustomTestDurationPopup.hide(); } else if (!$("#customTextPopupWrapper").hasClass("hidden")) { event.preventDefault(); CustomTextPopup.hide(); } else if (!$("#quoteSearchPopupWrapper").hasClass("hidden")) { event.preventDefault(); QuoteSearchPopupWrapper.hide(); } else if (!$("#commandLineWrapper").hasClass("hidden")) { if (CommandlineLists.current.length > 1) { CommandlineLists.current.pop(); $("#commandLine").removeClass("allCommands"); show(); } else { hide(); } UpdateConfig.setFontFamily(Config.fontFamily, true); } else if (event.key === "Tab" || !Config.swapEscAndTab) { if (Config.singleListCommandLine == "on") { useSingleListCommandLine(false); } else { CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); } show(); } } }); }); $("#commandInput input").keydown((e) => { if (e.key === "Enter") { //enter e.preventDefault(); let command = $("#commandInput input").attr("command"); let value = $("#commandInput input").val(); let list = CommandlineLists.current[CommandlineLists.current.length - 1]; $.each(list.list, (i, obj) => { if (obj.id == command) { obj.exec(value); if (obj.subgroup !== null && obj.subgroup !== undefined) { //TODO: what is this for? // subgroup = obj.subgroup; } } }); try { firebase.analytics().logEvent("usedCommandLine", { command: command, }); } catch (e) { console.log("Analytics unavailable"); } hide(); } return; }); $(document).on("mousemove", () => { if (!commandLineMouseMode) commandLineMouseMode = true; }); $(document).on( "mouseenter", "#commandLineWrapper #commandLine .suggestions .entry", (e) => { if (!commandLineMouseMode) return; $(e.target).addClass("activeMouse"); } ); $(document).on( "mouseleave", "#commandLineWrapper #commandLine .suggestions .entry", (e) => { if (!commandLineMouseMode) return; $(e.target).removeClass("activeMouse"); } ); $("#commandLineWrapper #commandLine .suggestions").on("mouseover", (e) => { if (!commandLineMouseMode) return; // console.log("clearing keyboard active"); $("#commandLineWrapper #commandLine .suggestions .entry").removeClass( "activeKeyboard" ); let hoverId = $(e.target).attr("command"); try { let list = CommandlineLists.current[CommandlineLists.current.length - 1]; $.each(list.list, (index, obj) => { if (obj.id == hoverId) { if ( (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && !ThemeController.randomTheme ) ThemeController.clearPreview(); if (!/font/gi.test(obj.id)) UpdateConfig.previewFontFamily(Config.fontFamily); obj.hover(); } }); } catch (e) {} }); $(document).on( "click", "#commandLineWrapper #commandLine .suggestions .entry", (e) => { $(".suggestions .entry").removeClass("activeKeyboard"); trigger($(e.currentTarget).attr("command")); } ); $("#commandLineWrapper").click((e) => { if ($(e.target).attr("id") === "commandLineWrapper") { hide(); UpdateConfig.setFontFamily(Config.fontFamily, true); // if (Config.customTheme === true) { // applyCustomThemeColors(); // } else { // setTheme(Config.theme, true); // } } }); //might come back to it later // function shiftCommand(){ // let activeEntries = $("#commandLineWrapper #commandLine .suggestions .entry.activeKeyboard, #commandLineWrapper #commandLine .suggestions .entry.activeMouse"); // activeEntries.each((index, activeEntry) => { // let commandId = activeEntry.getAttribute('command'); // let foundCommand = null; // CommandlineLists.defaultCommands.list.forEach(command => { // if(foundCommand === null && command.id === commandId){ // foundCommand = command; // } // }) // if(foundCommand.shift){ // $(activeEntry).find('div').text(foundCommand.shift.display); // } // }) // } // let shiftedCommands = false; // $(document).keydown((e) => { // if (e.key === "Shift") { // if(shiftedCommands === false){ // shiftedCommands = true; // shiftCommand(); // } // } // }); // $(document).keyup((e) => { // if (e.key === "Shift") { // shiftedCommands = false; // } // }); $(document).keydown((e) => { // if (isPreviewingTheme) { // console.log("applying theme"); // applyCustomThemeColors(); // previewTheme(Config.theme, false); // } if (!$("#commandLineWrapper").hasClass("hidden")) { $("#commandLine input").focus(); if (e.key == ">" && Config.singleListCommandLine == "manual") { if (!isSingleListCommandLineActive()) { useSingleListCommandLine(false); return; } else if ($("#commandLine input").val() == ">") { //so that it will ignore succeeding ">" when input is already ">" e.preventDefault(); return; } } if (e.key === "Backspace" || e.key === "Delete") { setTimeout(() => { let inputVal = $("#commandLine input").val(); if ( Config.singleListCommandLine == "manual" && isSingleListCommandLineActive() && inputVal[0] !== ">" ) { restoreOldCommandLine(false); } }, 1); } if (e.key === "Enter") { //enter e.preventDefault(); let command = $(".suggestions .entry.activeKeyboard").attr("command"); trigger(command); return; } if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Tab") { e.preventDefault(); $("#commandLineWrapper #commandLine .suggestions .entry").unbind( "mouseenter mouseleave" ); let entries = $(".suggestions .entry"); let activenum = -1; let hoverId; $.each(entries, (index, obj) => { if ($(obj).hasClass("activeKeyboard")) activenum = index; }); if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { entries.removeClass("activeKeyboard"); if (activenum == 0) { $(entries[entries.length - 1]).addClass("activeKeyboard"); hoverId = $(entries[entries.length - 1]).attr("command"); } else { $(entries[--activenum]).addClass("activeKeyboard"); hoverId = $(entries[activenum]).attr("command"); } } if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) { entries.removeClass("activeKeyboard"); if (activenum + 1 == entries.length) { $(entries[0]).addClass("activeKeyboard"); hoverId = $(entries[0]).attr("command"); } else { $(entries[++activenum]).addClass("activeKeyboard"); hoverId = $(entries[activenum]).attr("command"); } } try { let scroll = Math.abs( $(".suggestions").offset().top - $(".entry.activeKeyboard").offset().top - $(".suggestions").scrollTop() ) - $(".suggestions").outerHeight() / 2 + $($(".entry")[0]).outerHeight(); $(".suggestions").scrollTop(scroll); } catch (e) { console.log("could not scroll suggestions: " + e.message); } // console.log(`scrolling to ${scroll}`); try { let list = CommandlineLists.current[CommandlineLists.current.length - 1]; $.each(list.list, (index, obj) => { if (obj.id == hoverId) { if ( (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && !ThemeController.randomTheme ) ThemeController.clearPreview(); if (!/font/gi.test(obj.id)) UpdateConfig.previewFontFamily(Config.fontFamily); obj.hover(); } }); } catch (e) {} return false; } } }); $(document).on("click", "#commandLineMobileButton", () => { CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); show(); }); ==> ./monkeytype/src/js/test/poetry.js <== const bannedChars = ["—", "_", " "]; const maxWords = 100; const apiURL = "https://poetrydb.org/random"; export class Poem { constructor(title, author, words) { this.title = title; this.author = author; this.words = words; this.cleanUpText(); } cleanUpText() { var count = 0; var scrubbedWords = []; for (var i = 0; i < this.words.length; i++) { let scrubbed = ""; for (var j = 0; j < this.words[i].length; j++) { if (!bannedChars.includes(this.words[i][j])) scrubbed += this.words[i][j]; } if (scrubbed == "") continue; scrubbedWords.push(scrubbed); count++; if (count == maxWords) break; } this.words = scrubbedWords; } } export async function getPoem() { return new Promise((res, rej) => { console.log("Getting poem"); var poemReq = new XMLHttpRequest(); poemReq.onload = () => { if (poemReq.readyState == 4) { if (poemReq.status == 200) { let poemObj = JSON.parse(poemReq.responseText)[0]; let words = []; poemObj.lines.forEach((line) => { line.split(" ").forEach((word) => { words.push(word); }); }); let poem = new Poem(poemObj.title, poemObj.author, words); res(poem); } else { rej(poemReq.status); } } }; poemReq.open("GET", apiURL); poemReq.send(); }); } ==> ./monkeytype/src/js/test/test-timer.js <== //most of the code is thanks to //https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript import Config, * as UpdateConfig from "./config"; import * as CustomText from "./custom-text"; import * as TimerProgress from "./timer-progress"; import * as LiveWpm from "./live-wpm"; import * as TestStats from "./test-stats"; import * as Monkey from "./monkey"; import * as Misc from "./misc"; import * as Notifications from "./notifications"; import * as TestLogic from "./test-logic"; import * as Caret from "./caret"; export let slowTimer = false; let slowTimerCount = 0; export let time = 0; let timer = null; const interval = 1000; let expected = 0; function setSlowTimer() { if (slowTimer) return; slowTimer = true; console.error("Slow timer, disabling animations"); // Notifications.add("Slow timer detected", -1, 5); } function clearSlowTimer() { slowTimer = false; slowTimerCount = 0; } let timerDebug = false; export function enableTimerDebug() { timerDebug = true; } export function clear() { time = 0; clearTimeout(timer); } function premid() { if (timerDebug) console.time("premid"); document.querySelector("#premidSecondsLeft").innerHTML = Config.time - time; if (timerDebug) console.timeEnd("premid"); } function updateTimer() { if (timerDebug) console.time("timer progress update"); if ( Config.mode === "time" || (Config.mode === "custom" && CustomText.isTimeRandom) ) { TimerProgress.update(time); } if (timerDebug) console.timeEnd("timer progress update"); } function calculateWpmRaw() { if (timerDebug) console.time("calculate wpm and raw"); let wpmAndRaw = TestLogic.calculateWpmAndRaw(); if (timerDebug) console.timeEnd("calculate wpm and raw"); if (timerDebug) console.time("update live wpm"); LiveWpm.update(wpmAndRaw.wpm, wpmAndRaw.raw); if (timerDebug) console.timeEnd("update live wpm"); if (timerDebug) console.time("push to history"); TestStats.pushToWpmHistory(wpmAndRaw.wpm); TestStats.pushToRawHistory(wpmAndRaw.raw); if (timerDebug) console.timeEnd("push to history"); return wpmAndRaw; } function monkey(wpmAndRaw) { if (timerDebug) console.time("update monkey"); Monkey.updateFastOpacity(wpmAndRaw.wpm); if (timerDebug) console.timeEnd("update monkey"); } function calculateAcc() { if (timerDebug) console.time("calculate acc"); let acc = Misc.roundTo2(TestStats.calculateAccuracy()); if (timerDebug) console.timeEnd("calculate acc"); return acc; } function layoutfluid() { if (timerDebug) console.time("layoutfluid"); if (Config.funbox === "layoutfluid" && Config.mode === "time") { const layouts = Config.customLayoutfluid ? Config.customLayoutfluid.split("#") : ["qwerty", "dvorak", "colemak"]; // console.log(Config.customLayoutfluid); // console.log(layouts); const numLayouts = layouts.length; let index = 0; index = Math.floor(time / (Config.time / numLayouts)); if ( time == Math.floor(Config.time / numLayouts) - 3 || time == (Config.time / numLayouts) * 2 - 3 ) { Notifications.add("3", 0, 1); } if ( time == Math.floor(Config.time / numLayouts) - 2 || time == Math.floor(Config.time / numLayouts) * 2 - 2 ) { Notifications.add("2", 0, 1); } if ( time == Math.floor(Config.time / numLayouts) - 1 || time == Math.floor(Config.time / numLayouts) * 2 - 1 ) { Notifications.add("1", 0, 1); } if (Config.layout !== layouts[index] && layouts[index] !== undefined) { Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0); UpdateConfig.setLayout(layouts[index], true); UpdateConfig.setKeymapLayout(layouts[index], true); } } if (timerDebug) console.timeEnd("layoutfluid"); } function checkIfFailed(wpmAndRaw, acc) { if (timerDebug) console.time("fail conditions"); TestStats.pushKeypressesToHistory(); if ( Config.minWpm === "custom" && wpmAndRaw.wpm < parseInt(Config.minWpmCustomSpeed) && TestLogic.words.currentIndex > 3 ) { clearTimeout(timer); TestLogic.fail("min wpm"); clearSlowTimer(); return; } if ( Config.minAcc === "custom" && acc < parseInt(Config.minAccCustom) && TestLogic.words.currentIndex > 3 ) { clearTimeout(timer); TestLogic.fail("min accuracy"); clearSlowTimer(); return; } if (timerDebug) console.timeEnd("fail conditions"); } function checkIfTimeIsUp() { if (timerDebug) console.time("times up check"); if ( Config.mode == "time" || (Config.mode === "custom" && CustomText.isTimeRandom) ) { if ( (time >= Config.time && Config.time !== 0 && Config.mode === "time") || (time >= CustomText.time && CustomText.time !== 0 && Config.mode === "custom") ) { //times up clearTimeout(timer); Caret.hide(); TestLogic.input.pushHistory(); TestLogic.corrected.pushHistory(); TestLogic.finish(); clearSlowTimer(); return; } } if (timerDebug) console.timeEnd("times up check"); } // --------------------------------------- let timerStats = []; export function getTimerStats() { return timerStats; } async function timerStep() { if (timerDebug) console.time("timer step -----------------------------"); time++; premid(); updateTimer(); let wpmAndRaw = calculateWpmRaw(); let acc = calculateAcc(); monkey(wpmAndRaw); layoutfluid(); checkIfFailed(wpmAndRaw, acc); checkIfTimeIsUp(); if (timerDebug) console.timeEnd("timer step -----------------------------"); } export async function start() { clearSlowTimer(); timerStats = []; expected = TestStats.start + interval; (function loop() { const delay = expected - performance.now(); timerStats.push({ dateNow: Date.now(), now: performance.now(), expected: expected, nextDelay: delay, }); if ( (Config.mode === "time" && Config.time < 130 && Config.time > 0) || (Config.mode === "words" && Config.words < 250 && Config.words > 0) ) { if (delay < interval / 2) { //slow timer setSlowTimer(); } if (delay < interval / 10) { slowTimerCount++; if (slowTimerCount > 5) { //slow timer Notifications.add( "Stopping the test due to bad performance. This would cause test calculations to be incorrect. If this happens a lot, please report this.", -1 ); TestLogic.fail("slow timer"); } } } timer = setTimeout(function () { // time++; if (!TestLogic.active) { clearTimeout(timer); clearSlowTimer(); return; } timerStep(); expected += interval; loop(); }, delay); })(); } ==> ./monkeytype/src/js/test/caps-warning.js <== import Config from "./config"; function show() { if ($("#capsWarning").hasClass("hidden")) { $("#capsWarning").removeClass("hidden"); } } function hide() { if (!$("#capsWarning").hasClass("hidden")) { $("#capsWarning").addClass("hidden"); } } $(document).keydown(function (event) { try { if ( Config.capsLockWarning && event.originalEvent.getModifierState("CapsLock") ) { show(); } else { hide(); } } catch {} }); $(document).keyup(function (event) { try { if ( Config.capsLockWarning && event.originalEvent.getModifierState("CapsLock") ) { show(); } else { hide(); } } catch {} }); ==> ./monkeytype/src/js/test/shift-tracker.js <== import Config from "./config"; import Layouts from "./layouts"; export let leftState = false; export let rightState = false; let keymapStrings = { left: null, right: null, keymap: null, }; function buildKeymapStrings() { if (keymapStrings.keymap === Config.keymapLayout) return; let layout = Layouts[Config.keymapLayout]?.keys; if (!layout) { keymapStrings = { left: null, right: null, keymap: Config.keymapLayout, }; } else { keymapStrings.left = ( layout.slice(0, 7).join(" ") + " " + layout.slice(13, 19).join(" ") + " " + layout.slice(26, 31).join(" ") + " " + layout.slice(38, 43).join(" ") ).replace(/ /g, ""); keymapStrings.right = ( layout.slice(6, 13).join(" ") + " " + layout.slice(18, 26).join(" ") + " " + layout.slice(31, 38).join(" ") + " " + layout.slice(42, 48).join(" ") ).replace(/ /g, ""); keymapStrings.keymap = Config.keymapLayout; } } $(document).keydown((e) => { if (e.code === "ShiftLeft") { leftState = true; rightState = false; } else if (e.code === "ShiftRight") { leftState = false; rightState = true; } }); $(document).keyup((e) => { if (e.code === "ShiftLeft" || e.code === "ShiftRight") { leftState = false; rightState = false; } }); export function reset() { leftState = false; rightState = false; } let leftSideKeys = [ "KeyQ", "KeyW", "KeyE", "KeyR", "KeyT", "KeyA", "KeyS", "KeyD", "KeyF", "KeyG", "KeyZ", "KeyX", "KeyC", "KeyV", "Backquote", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", ]; let rightSideKeys = [ "KeyU", "KeyI", "KeyO", "KeyP", "KeyH", "KeyJ", "KeyK", "KeyL", "KeyN", "KeyM", "Digit7", "Digit8", "Digit9", "Digit0", "Backslash", "BracketLeft", "BracketRight", "Semicolon", "Quote", "Comma", "Period", "Slash", ]; export function isUsingOppositeShift(event) { if (!leftState && !rightState) return null; if (Config.oppositeShiftMode === "on") { if ( !rightSideKeys.includes(event.code) && !leftSideKeys.includes(event.code) ) return null; if ( (leftState && rightSideKeys.includes(event.code)) || (rightState && leftSideKeys.includes(event.code)) ) { return true; } else { return false; } } else if (Config.oppositeShiftMode === "keymap") { buildKeymapStrings(); if (!keymapStrings.left || !keymapStrings.right) return null; if ( (leftState && keymapStrings.right.includes(event.key)) || (rightState && keymapStrings.left.includes(event.key)) ) { return true; } else { return false; } } } ==> ./monkeytype/src/js/test/keymap.js <== import Config, * as UpdateConfig from "./config"; import * as ThemeColors from "./theme-colors"; import layouts from "./layouts"; import * as CommandlineLists from "./commandline-lists"; import * as Commandline from "./commandline"; import * as TestTimer from "./test-timer"; export function highlightKey(currentKey) { if (Config.mode === "zen") return; try { if ($(".active-key") != undefined) { $(".active-key").removeClass("active-key"); } let highlightKey; switch (currentKey) { case "\\": case "|": highlightKey = "#KeyBackslash"; break; case "}": case "]": highlightKey = "#KeyRightBracket"; break; case "{": case "[": highlightKey = "#KeyLeftBracket"; break; case '"': case "'": highlightKey = "#KeyQuote"; break; case ":": case ";": highlightKey = "#KeySemicolon"; break; case "<": case ",": highlightKey = "#KeyComma"; break; case ">": case ".": highlightKey = "#KeyPeriod"; break; case "?": case "/": highlightKey = "#KeySlash"; break; case "": highlightKey = "#KeySpace"; break; default: highlightKey = `#Key${currentKey}`; } $(highlightKey).addClass("active-key"); if (highlightKey === "#KeySpace") { $("#KeySpace2").addClass("active-key"); } } catch (e) { console.log("could not update highlighted keymap key: " + e.message); } } export async function flashKey(key, correct) { if (key == undefined) return; switch (key) { case "\\": case "|": key = "#KeyBackslash"; break; case "}": case "]": key = "#KeyRightBracket"; break; case "{": case "[": key = "#KeyLeftBracket"; break; case '"': case "'": key = "#KeyQuote"; break; case ":": case ";": key = "#KeySemicolon"; break; case "<": case ",": key = "#KeyComma"; break; case ">": case ".": key = "#KeyPeriod"; break; case "?": case "/": key = "#KeySlash"; break; case "" || "Space": key = "#KeySpace"; break; default: key = `#Key${key.toUpperCase()}`; } if (key == "#KeySpace") { key = ".key-split-space"; } let themecolors = await ThemeColors.get(); try { if (correct || Config.blindMode) { $(key) .stop(true, true) .css({ color: themecolors.bg, backgroundColor: themecolors.main, borderColor: themecolors.main, }) .animate( { color: themecolors.sub, backgroundColor: "transparent", borderColor: themecolors.sub, }, TestTimer.slowTimer ? 0 : 500, "easeOutExpo" ); } else { $(key) .stop(true, true) .css({ color: themecolors.bg, backgroundColor: themecolors.error, borderColor: themecolors.error, }) .animate( { color: themecolors.sub, backgroundColor: "transparent", borderColor: themecolors.sub, }, TestTimer.slowTimer ? 0 : 500, "easeOutExpo" ); } } catch (e) {} } export function hide() { $(".keymap").addClass("hidden"); } export function show() { $(".keymap").removeClass("hidden"); } export function refreshKeys(layout) { try { let lts = layouts[layout]; //layout to show let layoutString = layout; if (Config.keymapLayout === "overrideSync") { if (Config.layout === "default") { lts = layouts["qwerty"]; layoutString = "default"; } else { lts = layouts[Config.layout]; layoutString = Config.layout; } } if (lts.keymapShowTopRow) { $(".keymap .r1").removeClass("hidden"); } else { $(".keymap .r1").addClass("hidden"); } if (Config.keymapStyle === "alice") { $(".keymap .extraKey").removeClass("hidden"); } else { $(".keymap .extraKey").addClass("hidden"); } $($(".keymap .r5 .keymap-key .letter")[0]).text( layoutString.replace(/_/g, " ") ); if (lts.iso) { $(".keymap .r4 .keymap-key.first").removeClass("hidden-key"); } else { $(".keymap .r4 .keymap-key.first").addClass("hidden-key"); } var toReplace = lts.keys.slice(1, 48); var count = 0; // let repeatB = false; $(".keymap .keymap-key .letter") .map(function () { if (count < toReplace.length) { var key = toReplace[count].charAt(0); this.innerHTML = key; switch (key) { case "\\": case "|": this.parentElement.id = "KeyBackslash"; break; case "}": case "]": this.parentElement.id = "KeyRightBracket"; break; case "{": case "[": this.parentElement.id = "KeyLeftBracket"; break; case '"': case "'": this.parentElement.id = "KeyQuote"; break; case ":": case ";": this.parentElement.id = "KeySemicolon"; break; case "<": case ",": this.parentElement.id = "KeyComma"; break; case ">": case ".": this.parentElement.id = "KeyPeriod"; break; case "?": case "/": this.parentElement.id = "KeySlash"; break; case "": this.parentElement.id = "KeySpace"; break; default: this.parentElement.id = `Key${key.toUpperCase()}`; } } // if (count == 41 && !repeatB) { // repeatB = true; // }else{ // repeatB = false; // count++; // } count++; // } }) .get(); } catch (e) { console.log( "something went wrong when changing layout, resettings: " + e.message ); UpdateConfig.setKeymapLayout("qwerty", true); } } $(document).on("click", ".keymap .r5 #KeySpace", (e) => { CommandlineLists.setCurrent([CommandlineLists.commandsKeymapLayouts]); Commandline.show(); }); ==> ./monkeytype/src/js/test/wordset.js <== import Config from "./config"; let currentWordset = null; let currentWordGenerator = null; class Wordset { constructor(words) { this.words = words; this.length = this.words.length; } randomWord() { return this.words[Math.floor(Math.random() * this.length)]; } } const prefixSize = 2; class CharDistribution { constructor() { this.chars = {}; this.count = 0; } addChar(char) { this.count++; if (char in this.chars) { this.chars[char]++; } else { this.chars[char] = 1; } } randomChar() { const randomIndex = Math.floor(Math.random() * this.count); let runningCount = 0; for (const [char, charCount] of Object.entries(this.chars)) { runningCount += charCount; if (runningCount > randomIndex) { return char; } } } } class WordGenerator extends Wordset { constructor(words) { super(words); // Can generate an unbounded number of words in theory. this.length = Infinity; this.ngrams = {}; for (let word of words) { // Mark the end of each word with a space. word += " "; let prefix = ""; for (const c of word) { // Add `c` to the distribution of chars that can come after `prefix`. if (!(prefix in this.ngrams)) { this.ngrams[prefix] = new CharDistribution(); } this.ngrams[prefix].addChar(c); prefix = (prefix + c).substr(-prefixSize); } } } randomWord() { let word = ""; for (;;) { const prefix = word.substr(-prefixSize); let charDistribution = this.ngrams[prefix]; if (!charDistribution) { // This shouldn't happen if this.ngrams is complete. If it does // somehow, start generating a new word. word = ""; continue; } // Pick a random char from the distribution that comes after `prefix`. const nextChar = charDistribution.randomChar(); if (nextChar == " ") { // A space marks the end of the word, so stop generating and return. break; } word += nextChar; } return word; } } export function withWords(words) { if (Config.funbox == "pseudolang") { if (currentWordGenerator == null || words !== currentWordGenerator.words) { currentWordGenerator = new WordGenerator(words); } return currentWordGenerator; } else { if (currentWordset == null || words !== currentWordset.words) { currentWordset = new Wordset(words); } return currentWordset; } } ==> ./monkeytype/src/js/test/live-acc.js <== import Config from "./config"; import * as TestLogic from "./test-logic"; export function update(acc) { let number = Math.floor(acc); if (Config.blindMode) { number = 100; } document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = number + "%"; document.querySelector("#liveAcc").innerHTML = number + "%"; } export function show() { if (!Config.showLiveAcc) return; if (!TestLogic.active) return; if (Config.timerStyle === "mini") { // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity); if (!$("#miniTimerAndLiveWpm .acc").hasClass("hidden")) return; $("#miniTimerAndLiveWpm .acc") .removeClass("hidden") .css("opacity", 0) .animate( { opacity: Config.timerOpacity, }, 125 ); } else { // $("#liveWpm").css("opacity", Config.timerOpacity); if (!$("#liveAcc").hasClass("hidden")) return; $("#liveAcc").removeClass("hidden").css("opacity", 0).animate( { opacity: Config.timerOpacity, }, 125 ); } } export function hide() { // $("#liveWpm").css("opacity", 0); // $("#miniTimerAndLiveWpm .wpm").css("opacity", 0); $("#liveAcc").animate( { opacity: Config.timerOpacity, }, 125, () => { $("#liveAcc").addClass("hidden"); } ); $("#miniTimerAndLiveWpm .acc").animate( { opacity: Config.timerOpacity, }, 125, () => { $("#miniTimerAndLiveWpm .acc").addClass("hidden"); } ); } ==> ./monkeytype/src/js/test/weak-spot.js <== import * as TestStats from "./test-stats"; // Changes how quickly it 'learns' scores - very roughly the score for a char // is based on last perCharCount occurrences. Make it smaller to adjust faster. const perCharCount = 50; // Choose the highest scoring word from this many random words. Higher values // will choose words with more weak letters on average. const wordSamples = 20; // Score penatly (in milliseconds) for getting a letter wrong. const incorrectPenalty = 5000; let scores = {}; class Score { constructor() { this.average = 0.0; this.count = 0; } update(score) { if (this.count < perCharCount) { this.count++; } const adjustRate = 1.0 / this.count; // Keep an exponential moving average of the score over time. this.average = score * adjustRate + this.average * (1 - adjustRate); } } export function updateScore(char, isCorrect) { const timings = TestStats.keypressTimings.spacing.array; if (timings.length == 0) { return; } let score = timings[timings.length - 1]; if (!isCorrect) { score += incorrectPenalty; } if (!(char in scores)) { scores[char] = new Score(); } scores[char].update(score); } function score(word) { let total = 0.0; let numChars = 0; for (const c of word) { if (c in scores) { total += scores[c].average; numChars++; } } return numChars == 0 ? 0.0 : total / numChars; } export function getWord(wordset) { let highScore; let randomWord; for (let i = 0; i < wordSamples; i++) { let newWord = wordset.randomWord(); let newScore = score(newWord); if (i == 0 || newScore > highScore) { randomWord = newWord; highScore = newScore; } } return randomWord; } ==> ./monkeytype/src/js/test/tts.js <== import Config from "./config"; import * as Misc from "./misc"; let voice; export async function setLanguage(lang = Config.language) { if (!voice) return; let language = await Misc.getLanguage(lang); let bcp = language.bcp47 ? language.bcp47 : "en-US"; voice.lang = bcp; } export async function init() { voice = new SpeechSynthesisUtterance(); setLanguage(); } export function clear() { voice = undefined; } export function speak(text) { if (!voice) init(); voice.text = text; window.speechSynthesis.cancel(); window.speechSynthesis.speak(voice); } ==> ./monkeytype/src/js/test/test-logic.js <== import * as TestUI from "./test-ui"; import * as ManualRestart from "./manual-restart-tracker"; import Config, * as UpdateConfig from "./config"; import * as Misc from "./misc"; import * as Notifications from "./notifications"; import * as CustomText from "./custom-text"; import * as TestStats from "./test-stats"; import * as PractiseWords from "./practise-words"; import * as ShiftTracker from "./shift-tracker"; import * as Focus from "./focus"; import * as Funbox from "./funbox"; import * as Keymap from "./keymap"; import * as ThemeController from "./theme-controller"; import * as PaceCaret from "./pace-caret"; import * as Caret from "./caret"; import * as LiveWpm from "./live-wpm"; import * as LiveAcc from "./live-acc"; import * as LiveBurst from "./live-burst"; import * as TimerProgress from "./timer-progress"; import * as UI from "./ui"; import * as QuoteSearchPopup from "./quote-search-popup"; import * as QuoteSubmitPopup from "./quote-submit-popup"; import * as PbCrown from "./pb-crown"; import * as TestTimer from "./test-timer"; import * as OutOfFocus from "./out-of-focus"; import * as AccountButton from "./account-button"; import * as DB from "./db"; import * as Replay from "./replay.js"; import axiosInstance from "./axios-instance"; import * as MonkeyPower from "./monkey-power"; import * as Poetry from "./poetry.js"; import * as Wikipedia from "./wikipedia.js"; import * as TodayTracker from "./today-tracker"; import * as WeakSpot from "./weak-spot"; import * as Wordset from "./wordset"; import * as ChallengeContoller from "./challenge-controller"; import * as RateQuotePopup from "./rate-quote-popup"; import * as BritishEnglish from "./british-english"; import * as LazyMode from "./lazy-mode"; import * as Result from "./result"; const objecthash = require("object-hash"); export let glarsesMode = false; let failReason = ""; export function toggleGlarses() { glarsesMode = true; console.log( "Glarses Mode On - test result will be hidden. You can check the stats in the console (here)" ); console.log("To disable Glarses Mode refresh the page."); } export let notSignedInLastResult = null; export function clearNotSignedInResult() { notSignedInLastResult = null; } export function setNotSignedInUid(uid) { notSignedInLastResult.uid = uid; delete notSignedInLastResult.hash; notSignedInLastResult.hash = objecthash(notSignedInLastResult); } class Words { constructor() { this.list = []; this.length = 0; this.currentIndex = 0; } get(i, raw = false) { if (i === undefined) { return this.list; } else { if (raw) { return this.list[i]?.replace(/[.?!":\-,]/g, "")?.toLowerCase(); } else { return this.list[i]; } } } getCurrent() { return this.list[this.currentIndex]; } getLast() { return this.list[this.list.length - 1]; } push(word) { this.list.push(word); this.length = this.list.length; } reset() { this.list = []; this.currentIndex = 0; this.length = this.list.length; } resetCurrentIndex() { this.currentIndex = 0; } decreaseCurrentIndex() { this.currentIndex--; } increaseCurrentIndex() { this.currentIndex++; } clean() { for (let s of this.list) { if (/ +/.test(s)) { let id = this.list.indexOf(s); let tempList = s.split(" "); this.list.splice(id, 1); for (let i = 0; i < tempList.length; i++) { this.list.splice(id + i, 0, tempList[i]); } } } } } class Input { constructor() { this.current = ""; this.history = []; this.length = 0; } reset() { this.current = ""; this.history = []; this.length = 0; } resetHistory() { this.history = []; this.length = 0; } setCurrent(val) { this.current = val; this.length = this.current.length; } appendCurrent(val) { this.current += val; this.length = this.current.length; } resetCurrent() { this.current = ""; } getCurrent() { return this.current; } pushHistory() { this.history.push(this.current); this.historyLength = this.history.length; this.resetCurrent(); } popHistory() { return this.history.pop(); } getHistory(i) { if (i === undefined) { return this.history; } else { return this.history[i]; } } getHistoryLast() { return this.history[this.history.length - 1]; } } class Corrected { constructor() { this.current = ""; this.history = []; } setCurrent(val) { this.current = val; } appendCurrent(val) { this.current += val; } resetCurrent() { this.current = ""; } resetHistory() { this.history = []; } reset() { this.resetCurrent(); this.resetHistory(); } getHistory(i) { return this.history[i]; } popHistory() { return this.history.pop(); } pushHistory() { this.history.push(this.current); this.current = ""; } } export let active = false; export let words = new Words(); export let input = new Input(); export let corrected = new Corrected(); export let currentWordIndex = 0; export let isRepeated = false; export let isPaceRepeat = false; export let lastTestWpm = 0; export let hasTab = false; export let randomQuote = null; export let bailout = false; export function setActive(tf) { active = tf; if (!tf) MonkeyPower.reset(); } export function setRepeated(tf) { isRepeated = tf; } export function setPaceRepeat(tf) { isPaceRepeat = tf; } export function setHasTab(tf) { hasTab = tf; } export function setBailout(tf) { bailout = tf; } export function setRandomQuote(rq) { randomQuote = rq; } let spanishSentenceTracker = ""; export function punctuateWord(previousWord, currentWord, index, maxindex) { let word = currentWord; let currentLanguage = Config.language.split("_")[0]; let lastChar = Misc.getLastChar(previousWord); if (Config.funbox === "58008") { if (currentWord.length > 3) { if (Math.random() < 0.75) { let special = ["/", "*", "-", "+"][Math.floor(Math.random() * 4)]; word = Misc.setCharAt(word, Math.floor(word.length / 2), special); } } } else { if ( (index == 0 || lastChar == "." || lastChar == "?" || lastChar == "!") && currentLanguage != "code" ) { //always capitalise the first word or if there was a dot unless using a code alphabet word = Misc.capitalizeFirstLetter(word); if (currentLanguage == "spanish" || currentLanguage == "catalan") { let rand = Math.random(); if (rand > 0.9) { word = "¿" + word; spanishSentenceTracker = "?"; } else if (rand > 0.8) { word = "¡" + word; spanishSentenceTracker = "!"; } } } else if ( (Math.random() < 0.1 && lastChar != "." && lastChar != "," && index != maxindex - 2) || index == maxindex - 1 ) { if (currentLanguage == "spanish" || currentLanguage == "catalan") { if (spanishSentenceTracker == "?" || spanishSentenceTracker == "!") { word += spanishSentenceTracker; spanishSentenceTracker = ""; } } else { let rand = Math.random(); if (rand <= 0.8) { word += "."; } else if (rand > 0.8 && rand < 0.9) { if (currentLanguage == "french") { word = "?"; } else if ( currentLanguage == "arabic" || currentLanguage == "persian" || currentLanguage == "urdu" ) { word += "؟"; } else if (currentLanguage == "greek") { word += ";"; } else { word += "?"; } } else { if (currentLanguage == "french") { word = "!"; } else { word += "!"; } } } } else if ( Math.random() < 0.01 && lastChar != "," && lastChar != "." && currentLanguage !== "russian" ) { word = `"${word}"`; } else if ( Math.random() < 0.011 && lastChar != "," && lastChar != "." && currentLanguage !== "russian" && currentLanguage !== "ukrainian" ) { word = `'${word}'`; } else if (Math.random() < 0.012 && lastChar != "," && lastChar != ".") { if (currentLanguage == "code") { let r = Math.random(); if (r < 0.25) { word = `(${word})`; } else if (r < 0.5) { word = `{${word}}`; } else if (r < 0.75) { word = `[${word}]`; } else { word = `<${word}>`; } } else { word = `(${word})`; } } else if ( Math.random() < 0.013 && lastChar != "," && lastChar != "." && lastChar != ";" && lastChar != "؛" && lastChar != ":" ) { if (currentLanguage == "french") { word = ":"; } else if (currentLanguage == "greek") { word = "·"; } else { word += ":"; } } else if ( Math.random() < 0.014 && lastChar != "," && lastChar != "." && previousWord != "-" ) { word = "-"; } else if ( Math.random() < 0.015 && lastChar != "," && lastChar != "." && lastChar != ";" && lastChar != "؛" && lastChar != ":" ) { if (currentLanguage == "french") { word = ";"; } else if (currentLanguage == "greek") { word = "·"; } else if (currentLanguage == "arabic") { word += "؛"; } else { word += ";"; } } else if (Math.random() < 0.2 && lastChar != ",") { if ( currentLanguage == "arabic" || currentLanguage == "urdu" || currentLanguage == "persian" ) { word += "،"; } else { word += ","; } } else if (Math.random() < 0.25 && currentLanguage == "code") { let specials = ["{", "}", "[", "]", "(", ")", ";", "=", "+", "%", "/"]; word = specials[Math.floor(Math.random() * 10)]; } } return word; } export function startTest() { if (UI.pageTransition) { return false; } if (!Config.dbConfigLoaded) { UpdateConfig.setChangedBeforeDb(true); } try { if (firebase.auth().currentUser != null) { firebase.analytics().logEvent("testStarted"); } else { firebase.analytics().logEvent("testStartedNoLogin"); } } catch (e) { console.log("Analytics unavailable"); } setActive(true); Replay.startReplayRecording(); Replay.replayGetWordsList(words.list); TestStats.resetKeypressTimings(); TimerProgress.restart(); TimerProgress.show(); $("#liveWpm").text("0"); LiveWpm.show(); LiveAcc.show(); LiveBurst.show(); TimerProgress.update(TestTimer.time); TestTimer.clear(); if (Config.funbox === "memory") { Funbox.resetMemoryTimer(); $("#wordsWrapper").addClass("hidden"); } try { if (Config.paceCaret !== "off" || (Config.repeatedPace && isPaceRepeat)) PaceCaret.start(); } catch (e) {} //use a recursive self-adjusting timer to avoid time drift TestStats.setStart(performance.now()); TestTimer.start(); return true; } export function restart( withSameWordset = false, nosave = false, event, practiseMissed = false ) { if (TestUI.testRestarting || TestUI.resultCalculating) { try { event.preventDefault(); } catch {} return; } if (UI.getActivePage() == "pageTest" && !TestUI.resultVisible) { if (!ManualRestart.get()) { if (hasTab) { try { if (!event.shiftKey) return; } catch {} } try { if (Config.mode !== "zen") event.preventDefault(); } catch {} if ( !Misc.canQuickRestart( Config.mode, Config.words, Config.time, CustomText ) ) { let message = "Use your mouse to confirm."; if (Config.quickTab) message = "Press shift + tab or use your mouse to confirm."; Notifications.add("Quick restart disabled. " + message, 0, 3); return; } // }else{ // return; // } } } if (active) { TestStats.pushKeypressesToHistory(); let testSeconds = TestStats.calculateTestSeconds(performance.now()); let afkseconds = TestStats.calculateAfkSeconds(testSeconds); // incompleteTestSeconds += ; let tt = testSeconds - afkseconds; if (tt < 0) tt = 0; console.log( `increasing incomplete time by ${tt}s (${testSeconds}s - ${afkseconds}s afk)` ); TestStats.incrementIncompleteSeconds(tt); TestStats.incrementRestartCount(); if (tt > 600) { Notifications.add( `Your time typing just increased by ${Misc.roundTo2( tt / 60 )} minutes. If you think this is incorrect please contact Miodec and dont refresh the website.`, -1 ); } // restartCount++; } if (Config.mode == "zen") { $("#words").empty(); } if ( PractiseWords.before.mode !== null && !withSameWordset && !practiseMissed ) { Notifications.add("Reverting to previous settings.", 0); UpdateConfig.setPunctuation(PractiseWords.before.punctuation); UpdateConfig.setNumbers(PractiseWords.before.numbers); UpdateConfig.setMode(PractiseWords.before.mode); PractiseWords.resetBefore(); } let repeatWithPace = false; if (TestUI.resultVisible && Config.repeatedPace && withSameWordset) { repeatWithPace = true; } ManualRestart.reset(); TestTimer.clear(); TestStats.restart(); corrected.reset(); ShiftTracker.reset(); Caret.hide(); setActive(false); Replay.stopReplayRecording(); LiveWpm.hide(); LiveAcc.hide(); LiveBurst.hide(); TimerProgress.hide(); Replay.pauseReplay(); setBailout(false); PaceCaret.reset(); $("#showWordHistoryButton").removeClass("loaded"); $("#restartTestButton").blur(); Funbox.resetMemoryTimer(); RateQuotePopup.clearQuoteStats(); if (UI.getActivePage() == "pageTest" && window.scrollY > 0) window.scrollTo({ top: 0, behavior: "smooth" }); $("#wordsInput").val(" "); TestUI.reset(); $("#timerNumber").css("opacity", 0); let el = null; if (TestUI.resultVisible) { //results are being displayed el = $("#result"); } else { //words are being displayed el = $("#typingTest"); } if (TestUI.resultVisible) { if ( Config.randomTheme !== "off" && !UI.pageTransition && !Config.customTheme ) { ThemeController.randomizeTheme(); } } TestUI.setResultVisible(false); UI.setPageTransition(true); TestUI.setTestRestarting(true); el.stop(true, true).animate( { opacity: 0, }, 125, async () => { if (UI.getActivePage() == "pageTest") Focus.set(false); TestUI.focusWords(); $("#monkey .fast").stop(true, true).css("opacity", 0); $("#monkey").stop(true, true).css({ animationDuration: "0s" }); $("#typingTest").css("opacity", 0).removeClass("hidden"); $("#wordsInput").val(" "); let shouldQuoteRepeat = false; if ( Config.mode === "quote" && Config.repeatQuotes === "typing" && failReason !== "" ) { shouldQuoteRepeat = true; } if (Config.funbox === "arrows") { UpdateConfig.setPunctuation(false, true); UpdateConfig.setNumbers(false, true); } else if (Config.funbox === "58008") { UpdateConfig.setNumbers(false, true); } else if (Config.funbox === "specials") { UpdateConfig.setPunctuation(false, true); UpdateConfig.setNumbers(false, true); } else if (Config.funbox === "ascii") { UpdateConfig.setPunctuation(false, true); UpdateConfig.setNumbers(false, true); } if (!withSameWordset && !shouldQuoteRepeat) { setRepeated(false); setPaceRepeat(repeatWithPace); setHasTab(false); await init(); PaceCaret.init(nosave); } else { setRepeated(true); setPaceRepeat(repeatWithPace); setActive(false); Replay.stopReplayRecording(); words.resetCurrentIndex(); input.reset(); if (Config.funbox === "plus_one" || Config.funbox === "plus_two") { Notifications.add( "Sorry, this funbox won't work with repeated tests.", 0 ); await Funbox.activate("none"); } else { await Funbox.activate(); } TestUI.showWords(); PaceCaret.init(); } failReason = ""; if (Config.mode === "quote") { setRepeated(false); } if (Config.keymapMode !== "off") { Keymap.show(); } else { Keymap.hide(); } document.querySelector("#miniTimerAndLiveWpm .wpm").innerHTML = "0"; document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = "100%"; document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = "0"; document.querySelector("#liveWpm").innerHTML = "0"; document.querySelector("#liveAcc").innerHTML = "100%"; document.querySelector("#liveBurst").innerHTML = "0"; if (Config.funbox === "memory") { Funbox.startMemoryTimer(); if (Config.keymapMode === "next") { UpdateConfig.setKeymapMode("react"); } } let mode2 = Misc.getMode2(); let fbtext = ""; if (Config.funbox !== "none") { fbtext = " " + Config.funbox; } $(".pageTest #premidTestMode").text( `${Config.mode} ${mode2} ${Config.language.replace(/_/g, " ")}${fbtext}` ); $(".pageTest #premidSecondsLeft").text(Config.time); if (Config.funbox === "layoutfluid") { UpdateConfig.setLayout( Config.customLayoutfluid ? Config.customLayoutfluid.split("#")[0] : "qwerty", true ); UpdateConfig.setKeymapLayout( Config.customLayoutfluid ? Config.customLayoutfluid.split("#")[0] : "qwerty", true ); Keymap.highlightKey( words .getCurrent() .substring(input.current.length, input.current.length + 1) .toString() .toUpperCase() ); } $("#result").addClass("hidden"); $("#testModesNotice").removeClass("hidden").css({ opacity: 1, }); // resetPaceCaret(); $("#typingTest") .css("opacity", 0) .removeClass("hidden") .stop(true, true) .animate( { opacity: 1, }, 125, () => { TestUI.setTestRestarting(false); // resetPaceCaret(); PbCrown.hide(); TestTimer.clear(); if ($("#commandLineWrapper").hasClass("hidden")) TestUI.focusWords(); // ChartController.result.update(); TestUI.updateModesNotice(); UI.setPageTransition(false); // console.log(TestStats.incompleteSeconds); // console.log(TestStats.restartCount); } ); } ); } async function getNextWord(wordset, language, wordsBound) { let randomWord = wordset.randomWord(); const previousWord = words.get(words.length - 1, true); const previousWord2 = words.get(words.length - 2, true); if (Config.mode === "quote") { randomWord = randomQuote.textSplit[words.length]; } else if ( Config.mode == "custom" && !CustomText.isWordRandom && !CustomText.isTimeRandom ) { randomWord = CustomText.text[words.length]; } else if ( Config.mode == "custom" && (CustomText.isWordRandom || CustomText.isTimeRandom) && (wordset.length < 3 || PractiseWords.before.mode !== null) ) { randomWord = wordset.randomWord(); } else { let regenarationCount = 0; //infinite loop emergency stop button while ( regenarationCount < 100 && (previousWord == randomWord || previousWord2 == randomWord || (!Config.punctuation && randomWord == "I")) ) { regenarationCount++; randomWord = wordset.randomWord(); } } if (randomWord === undefined) { randomWord = wordset.randomWord(); } if (Config.lazyMode === true && !language.noLazyMode) { randomWord = LazyMode.replaceAccents(randomWord, language.accents); } randomWord = randomWord.replace(/ +/gm, " "); randomWord = randomWord.replace(/^ | $/gm, ""); if (Config.funbox === "rAnDoMcAsE") { let randomcaseword = ""; for (let i = 0; i < randomWord.length; i++) { if (i % 2 != 0) { randomcaseword += randomWord[i].toUpperCase(); } else { randomcaseword += randomWord[i]; } } randomWord = randomcaseword; } else if (Config.funbox === "capitals") { randomWord = Misc.capitalizeFirstLetter(randomWord); } else if (Config.funbox === "gibberish") { randomWord = Misc.getGibberish(); } else if (Config.funbox === "arrows") { randomWord = Misc.getArrows(); } else if (Config.funbox === "58008") { randomWord = Misc.getNumbers(7); } else if (Config.funbox === "specials") { randomWord = Misc.getSpecials(); } else if (Config.funbox === "ascii") { randomWord = Misc.getASCII(); } else if (Config.funbox === "weakspot") { randomWord = WeakSpot.getWord(wordset); } if (Config.punctuation) { randomWord = punctuateWord( words.get(words.length - 1), randomWord, words.length, wordsBound ); } if (Config.numbers) { if (Math.random() < 0.1) { randomWord = Misc.getNumbers(4); } } if (Config.britishEnglish && /english/.test(Config.language)) { randomWord = await BritishEnglish.replace(randomWord); } return randomWord; } export async function init() { setActive(false); Replay.stopReplayRecording(); words.reset(); TestUI.setCurrentWordElementIndex(0); // accuracy = { // correct: 0, // incorrect: 0, // }; input.resetHistory(); input.resetCurrent(); let language = await Misc.getLanguage(Config.language); if (language && language.name !== Config.language) { UpdateConfig.setLanguage("english"); } if (!language) { UpdateConfig.setLanguage("english"); language = await Misc.getLanguage(Config.language); } if (Config.lazyMode === true && language.noLazyMode) { Notifications.add("This language does not support lazy mode.", 0); UpdateConfig.setLazyMode(false); } let wordsBound = 100; if (Config.showAllLines) { if (Config.mode === "quote") { wordsBound = 100; } else if (Config.mode === "custom") { if (CustomText.isWordRandom) { wordsBound = CustomText.word; } else if (CustomText.isTimeRandom) { wordsBound = 100; } else { wordsBound = CustomText.text.length; } } else if (Config.mode != "time") { wordsBound = Config.words; } } else { if (Config.mode === "words" && Config.words < wordsBound) { wordsBound = Config.words; } if ( Config.mode == "custom" && CustomText.isWordRandom && CustomText.word < wordsBound ) { wordsBound = CustomText.word; } if (Config.mode == "custom" && CustomText.isTimeRandom) { wordsBound = 100; } if ( Config.mode == "custom" && !CustomText.isWordRandom && !CustomText.isTimeRandom && CustomText.text.length < wordsBound ) { wordsBound = CustomText.text.length; } } if ( (Config.mode === "custom" && CustomText.isWordRandom && CustomText.word == 0) || (Config.mode === "custom" && CustomText.isTimeRandom && CustomText.time == 0) ) { wordsBound = 100; } if (Config.mode === "words" && Config.words === 0) { wordsBound = 100; } if (Config.funbox === "plus_one") { wordsBound = 2; if (Config.mode === "words" && Config.words < wordsBound) { wordsBound = Config.words; } } if (Config.funbox === "plus_two") { wordsBound = 3; if (Config.mode === "words" && Config.words < wordsBound) { wordsBound = Config.words; } } if ( Config.mode == "time" || Config.mode == "words" || Config.mode == "custom" ) { let wordList = language.words; if (Config.mode == "custom") { wordList = CustomText.text; } const wordset = Wordset.withWords(wordList); if ( (Config.funbox == "wikipedia" || Config.funbox == "poetry") && Config.mode != "custom" ) { let wordCount = 0; // If mode is words, get as many sections as you need until the wordCount is fullfilled while ( (Config.mode == "words" && Config.words >= wordCount) || (Config.mode === "time" && wordCount < 100) ) { let section = Config.funbox == "wikipedia" ? await Wikipedia.getSection() : await Poetry.getPoem(); for (let word of section.words) { if (wordCount >= Config.words && Config.mode == "words") { wordCount++; break; } wordCount++; words.push(word); } } } else { for (let i = 0; i < wordsBound; i++) { let randomWord = await getNextWord(wordset, language, wordsBound); if (/t/g.test(randomWord)) { setHasTab(true); } if (/ +/.test(randomWord)) { let randomList = randomWord.split(" "); let id = 0; while (id < randomList.length) { words.push(randomList[id]); id++; if ( words.length == wordsBound && Config.mode == "custom" && CustomText.isWordRandom ) { break; } } if ( Config.mode == "custom" && !CustomText.isWordRandom && !CustomText.isTimeRandom ) { // } else { i = words.length - 1; } } else { words.push(randomWord); } } } } else if (Config.mode == "quote") { // setLanguage(Config.language.replace(/_\d*k$/g, ""), true); let quotes = await Misc.getQuotes(Config.language.replace(/_\d*k$/g, "")); if (quotes.length === 0) { TestUI.setTestRestarting(false); Notifications.add( `No ${Config.language.replace(/_\d*k$/g, "")} quotes found`, 0 ); if (firebase.auth().currentUser) { QuoteSubmitPopup.show(false); } UpdateConfig.setMode("words"); restart(); return; } let rq; if (Config.quoteLength != -2) { let quoteLengths = Config.quoteLength; let groupIndex; if (quoteLengths.length > 1) { groupIndex = quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; while (quotes.groups[groupIndex].length === 0) { groupIndex = quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; } } else { groupIndex = quoteLengths[0]; if (quotes.groups[groupIndex].length === 0) { Notifications.add("No quotes found for selected quote length", 0); TestUI.setTestRestarting(false); return; } } rq = quotes.groups[groupIndex][ Math.floor(Math.random() * quotes.groups[groupIndex].length) ]; if (randomQuote != null && rq.id === randomQuote.id) { rq = quotes.groups[groupIndex][ Math.floor(Math.random() * quotes.groups[groupIndex].length) ]; } } else { quotes.groups.forEach((group) => { let filtered = group.filter( (quote) => quote.id == QuoteSearchPopup.selectedId ); if (filtered.length > 0) { rq = filtered[0]; } }); if (rq == undefined) { rq = quotes.groups[0][0]; Notifications.add("Quote Id Does Not Exist", 0); } } rq.text = rq.text.replace(/ +/gm, " "); rq.text = rq.text.replace(/t/gm, "t"); rq.text = rq.text.replace(/\\\\n/gm, "\n"); rq.text = rq.text.replace(/t/gm, "t"); rq.text = rq.text.replace(/\\n/gm, "\n"); rq.text = rq.text.replace(/( *(\r\n|\r|\n) *)/g, "\n "); rq.text = rq.text.replace(/…/g, "..."); rq.text = rq.text.trim(); rq.textSplit = rq.text.split(" "); rq.language = Config.language.replace(/_\d*k$/g, ""); setRandomQuote(rq); let w = randomQuote.textSplit; wordsBound = Math.min(wordsBound, w.length); for (let i = 0; i < wordsBound; i++) { if (/t/g.test(w[i])) { setHasTab(true); } if ( Config.britishEnglish && Config.language.replace(/_\d*k$/g, "") === "english" ) { w[i] = await BritishEnglish.replace(w[i]); } if (Config.lazyMode === true && !language.noLazyMode) { w[i] = LazyMode.replaceAccents(w[i], language.accents); } words.push(w[i]); } } //handle right-to-left languages if (language.leftToRight) { TestUI.arrangeCharactersLeftToRight(); } else { TestUI.arrangeCharactersRightToLeft(); } if (language.ligatures) { $("#words").addClass("withLigatures"); $("#resultWordsHistory .words").addClass("withLigatures"); $("#resultReplay .words").addClass("withLigatures"); } else { $("#words").removeClass("withLigatures"); $("#resultWordsHistory .words").removeClass("withLigatures"); $("#resultReplay .words").removeClass("withLigatures"); } // if (Config.mode == "zen") { // // Creating an empty active word element for zen mode // $("#words").append('
'); // $("#words").css("height", "auto"); // $("#wordsWrapper").css("height", "auto"); // } else { if (UI.getActivePage() == "pageTest") { await Funbox.activate(); } TestUI.showWords(); // } } export function calculateWpmAndRaw() { let chars = 0; let correctWordChars = 0; let spaces = 0; //check input history for (let i = 0; i < input.history.length; i++) { let word = Config.mode == "zen" ? input.getHistory(i) : words.get(i); if (input.getHistory(i) == word) { //the word is correct //+1 for space correctWordChars += word.length; if ( i < input.history.length - 1 && Misc.getLastChar(input.getHistory(i)) !== "\n" ) { spaces++; } } chars += input.getHistory(i).length; } if (input.current !== "") { let word = Config.mode == "zen" ? input.current : words.getCurrent(); //check whats currently typed let toAdd = { correct: 0, incorrect: 0, missed: 0, }; for (let c = 0; c < word.length; c++) { if (c < input.current.length) { //on char that still has a word list pair if (input.current[c] == word[c]) { toAdd.correct++; } else { toAdd.incorrect++; } } else { //on char that is extra toAdd.missed++; } } chars += toAdd.correct; chars += toAdd.incorrect; chars += toAdd.missed; if (toAdd.incorrect == 0) { //word is correct so far, add chars correctWordChars += toAdd.correct; } } if (Config.funbox === "nospace" || Config.funbox === "arrows") { spaces = 0; } chars += input.current.length; let testSeconds = TestStats.calculateTestSeconds(performance.now()); let wpm = Math.round(((correctWordChars + spaces) * (60 / testSeconds)) / 5); let raw = Math.round(((chars + spaces) * (60 / testSeconds)) / 5); return { wpm: wpm, raw: raw, }; } export async function addWord() { let bound = 100; if (Config.funbox === "wikipedia" || Config.funbox == "poetry") { if (Config.mode == "time" && words.length - words.currentIndex < 20) { let section = Config.funbox == "wikipedia" ? await Wikipedia.getSection() : await Poetry.getPoem(); let wordCount = 0; for (let word of section.words) { if (wordCount >= Config.words && Config.mode == "words") { break; } wordCount++; words.push(word); TestUI.addWord(word); } } else { return; } } if (Config.funbox === "plus_one") bound = 1; if (Config.funbox === "plus_two") bound = 2; if ( words.length - input.history.length > bound || (Config.mode === "words" && words.length >= Config.words && Config.words > 0) || (Config.mode === "custom" && CustomText.isWordRandom && words.length >= CustomText.word && CustomText.word != 0) || (Config.mode === "custom" && !CustomText.isWordRandom && !CustomText.isTimeRandom && words.length >= CustomText.text.length) || (Config.mode === "quote" && words.length >= randomQuote.textSplit.length) ) return; const language = Config.mode !== "custom" ? await Misc.getCurrentLanguage() : { //borrow the direction of the current language leftToRight: await Misc.getCurrentLanguage().leftToRight, words: CustomText.text, }; const wordset = Wordset.withWords(language.words); let randomWord = await getNextWord(wordset, language, bound); let split = randomWord.split(" "); if (split.length > 1) { split.forEach((word) => { words.push(word); TestUI.addWord(word); }); } else { words.push(randomWord); TestUI.addWord(randomWord); } } var retrySaving = { completedEvent: null, canRetry: false, }; export function retrySavingResult() { if (!retrySaving.completedEvent) { Notifications.add( "Could not retry saving the result as the result no longer exists.", 0, -1 ); } if (!retrySaving.canRetry) { return; } retrySaving.canRetry = false; $("#retrySavingResultButton").addClass("hidden"); AccountButton.loading(true); Notifications.add("Retrying to save..."); var { completedEvent } = retrySaving; axiosInstance .post("/results/add", { result: completedEvent, }) .then((response) => { AccountButton.loading(false); Result.hideCrown(); if (response.status !== 200) { Notifications.add("Result not saved. " + response.data.message, -1); } else { completedEvent._id = response.data.insertedId; if (response.data.isPb) { completedEvent.isPb = true; } DB.saveLocalResult(completedEvent); DB.updateLocalStats({ time: completedEvent.testDuration + completedEvent.incompleteTestSeconds - completedEvent.afkDuration, started: TestStats.restartCount + 1, }); try { firebase.analytics().logEvent("testCompleted", completedEvent); } catch (e) { console.log("Analytics unavailable"); } if (response.data.isPb) { //new pb Result.showCrown(); Result.updateCrown(); DB.saveLocalPB( Config.mode, completedEvent.mode2, Config.punctuation, Config.language, Config.difficulty, Config.lazyMode, completedEvent.wpm, completedEvent.acc, completedEvent.rawWpm, completedEvent.consistency ); } } $("#retrySavingResultButton").addClass("hidden"); Notifications.add("Result saved", 1); }) .catch((e) => { AccountButton.loading(false); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to save result: " + msg, -1); $("#retrySavingResultButton").removeClass("hidden"); retrySaving.canRetry = true; }); } function buildCompletedEvent(difficultyFailed) { //build completed event object let completedEvent = { wpm: undefined, rawWpm: undefined, charStats: undefined, acc: undefined, mode: Config.mode, mode2: undefined, quoteLength: -1, punctuation: Config.punctuation, numbers: Config.numbers, lazyMode: Config.lazyMode, timestamp: Date.now(), language: Config.language, restartCount: TestStats.restartCount, incompleteTestSeconds: TestStats.incompleteSeconds < 0 ? 0 : Misc.roundTo2(TestStats.incompleteSeconds), difficulty: Config.difficulty, blindMode: Config.blindMode, tags: undefined, keySpacing: TestStats.keypressTimings.spacing.array, keyDuration: TestStats.keypressTimings.duration.array, consistency: undefined, keyConsistency: undefined, funbox: Config.funbox, bailedOut: bailout, chartData: { wpm: TestStats.wpmHistory, raw: undefined, err: undefined, }, customText: undefined, testDuration: undefined, afkDuration: undefined, }; // stats let stats = TestStats.calculateStats(); if (stats.time % 1 != 0 && Config.mode !== "time") { TestStats.setLastSecondNotRound(); } lastTestWpm = stats.wpm; completedEvent.wpm = stats.wpm; completedEvent.rawWpm = stats.wpmRaw; completedEvent.charStats = [ stats.correctChars + stats.correctSpaces, stats.incorrectChars, stats.extraChars, stats.missedChars, ]; completedEvent.acc = stats.acc; // if the last second was not rounded, add another data point to the history if (TestStats.lastSecondNotRound && !difficultyFailed) { let wpmAndRaw = calculateWpmAndRaw(); TestStats.pushToWpmHistory(wpmAndRaw.wpm); TestStats.pushToRawHistory(wpmAndRaw.raw); TestStats.pushKeypressesToHistory(); } //consistency let rawPerSecond = TestStats.keypressPerSecond.map((f) => Math.round((f.count / 5) * 60) ); let stddev = Misc.stdDev(rawPerSecond); let avg = Misc.mean(rawPerSecond); let consistency = Misc.roundTo2(Misc.kogasa(stddev / avg)); let keyconsistencyarray = TestStats.keypressTimings.spacing.array.slice(); keyconsistencyarray = keyconsistencyarray.splice( 0, keyconsistencyarray.length - 1 ); let keyConsistency = Misc.roundTo2( Misc.kogasa( Misc.stdDev(keyconsistencyarray) / Misc.mean(keyconsistencyarray) ) ); if (isNaN(consistency)) { consistency = 0; } completedEvent.keyConsistency = keyConsistency; completedEvent.consistency = consistency; let smoothedraw = Misc.smooth(rawPerSecond, 1); completedEvent.chartData.raw = smoothedraw; completedEvent.chartData.unsmoothedRaw = rawPerSecond; //smoothed consistency let stddev2 = Misc.stdDev(smoothedraw); let avg2 = Misc.mean(smoothedraw); let smoothConsistency = Misc.roundTo2(Misc.kogasa(stddev2 / avg2)); completedEvent.smoothConsistency = smoothConsistency; //wpm consistency let stddev3 = Misc.stdDev(completedEvent.chartData.wpm); let avg3 = Misc.mean(completedEvent.chartData.wpm); let wpmConsistency = Misc.roundTo2(Misc.kogasa(stddev3 / avg3)); completedEvent.wpmConsistency = wpmConsistency; completedEvent.testDuration = parseFloat(stats.time); completedEvent.afkDuration = TestStats.calculateAfkSeconds( completedEvent.testDuration ); completedEvent.chartData.err = []; for (let i = 0; i < TestStats.keypressPerSecond.length; i++) { completedEvent.chartData.err.push(TestStats.keypressPerSecond[i].errors); } if (Config.mode === "quote") { completedEvent.quoteLength = randomQuote.group; completedEvent.lang = Config.language.replace(/_\d*k$/g, ""); } completedEvent.mode2 = Misc.getMode2(); if (Config.mode === "custom") { completedEvent.customText = {}; completedEvent.customText.textLen = CustomText.text.length; completedEvent.customText.isWordRandom = CustomText.isWordRandom; completedEvent.customText.isTimeRandom = CustomText.isTimeRandom; completedEvent.customText.word = CustomText.word !== "" && !isNaN(CustomText.word) ? CustomText.word : null; completedEvent.customText.time = CustomText.time !== "" && !isNaN(CustomText.time) ? CustomText.time : null; } else { delete completedEvent.customText; } //tags let activeTagsIds = []; try { DB.getSnapshot().tags.forEach((tag) => { if (tag.active === true) { activeTagsIds.push(tag._id); } }); } catch (e) {} completedEvent.tags = activeTagsIds; if (completedEvent.mode != "custom") delete completedEvent.customText; return completedEvent; } export async function finish(difficultyFailed = false) { if (!active) return; if (Config.mode == "zen" && input.current.length != 0) { input.pushHistory(); corrected.pushHistory(); Replay.replayGetWordsList(input.history); } TestStats.recordKeypressSpacing(); //this is needed in case there is afk time at the end - to make sure test duration makes sense TestUI.setResultCalculating(true); TestUI.setResultVisible(true); TestStats.setEnd(performance.now()); setActive(false); Replay.stopReplayRecording(); Focus.set(false); Caret.hide(); LiveWpm.hide(); PbCrown.hide(); LiveAcc.hide(); LiveBurst.hide(); TimerProgress.hide(); OutOfFocus.hide(); TestTimer.clear(); Funbox.activate("none", null); //need one more calculation for the last word if test auto ended if (TestStats.burstHistory.length !== input.getHistory().length) { let burst = TestStats.calculateBurst(); TestStats.pushBurstToHistory(burst); } //remove afk from zen if (Config.mode == "zen" || bailout) { TestStats.removeAfkData(); } const completedEvent = buildCompletedEvent(difficultyFailed); //todo check if any fields are undefined ///////// completed event ready //afk check let kps = TestStats.keypressPerSecond.slice(-5); let afkDetected = kps.every((second) => second.afk); if (bailout) afkDetected = false; let tooShort = false; let dontSave = false; //fail checks if (difficultyFailed) { Notifications.add(`Test failed - ${failReason}`, 0, 1); dontSave = true; } else if (afkDetected) { Notifications.add("Test invalid - AFK detected", 0); dontSave = true; } else if (isRepeated) { Notifications.add("Test invalid - repeated", 0); dontSave = true; } else if ( (Config.mode === "time" && completedEvent.mode2 < 15 && completedEvent.mode2 > 0) || (Config.mode === "time" && completedEvent.mode2 == 0 && completedEvent.testDuration < 15) || (Config.mode === "words" && completedEvent.mode2 < 10 && completedEvent.mode2 > 0) || (Config.mode === "words" && completedEvent.mode2 == 0 && completedEvent.testDuration < 15) || (Config.mode === "custom" && !CustomText.isWordRandom && !CustomText.isTimeRandom && CustomText.text.length < 10) || (Config.mode === "custom" && CustomText.isWordRandom && !CustomText.isTimeRandom && CustomText.word < 10) || (Config.mode === "custom" && !CustomText.isWordRandom && CustomText.isTimeRandom && CustomText.time < 15) || (Config.mode === "zen" && completedEvent.testDuration < 15) ) { Notifications.add("Test invalid - too short", 0); tooShort = true; dontSave = true; } else if (completedEvent.wpm < 0 || completedEvent.wpm > 350) { Notifications.add("Test invalid - wpm", 0); TestStats.setInvalid(); dontSave = true; } else if (completedEvent.acc < 75 || completedEvent.acc > 100) { Notifications.add("Test invalid - accuracy", 0); TestStats.setInvalid(); dontSave = true; } // test is valid if (!dontSave) { TodayTracker.addSeconds( completedEvent.testDuration + (TestStats.incompleteSeconds < 0 ? 0 : Misc.roundTo2(TestStats.incompleteSeconds)) - completedEvent.afkDuration ); Result.updateTodayTracker(); } if (firebase.auth().currentUser == null) { $(".pageTest #result #rateQuoteButton").addClass("hidden"); try { firebase.analytics().logEvent("testCompletedNoLogin", completedEvent); } catch (e) { console.log("Analytics unavailable"); } notSignedInLastResult = completedEvent; dontSave = true; } Result.update( completedEvent, difficultyFailed, failReason, afkDetected, isRepeated, tooShort, randomQuote, dontSave ); delete completedEvent.chartData.unsmoothedRaw; if (completedEvent.testDuration > 122) { completedEvent.chartData = "toolong"; completedEvent.keySpacing = "toolong"; completedEvent.keyDuration = "toolong"; TestStats.setKeypressTimingsTooLong(); } if (dontSave) { try { firebase.analytics().logEvent("testCompletedInvalid", completedEvent); } catch (e) { console.log("Analytics unavailable"); } return; } // user is logged in if ( Config.difficulty == "normal" || ((Config.difficulty == "master" || Config.difficulty == "expert") && !difficultyFailed) ) { TestStats.resetIncomplete(); } completedEvent.uid = firebase.auth().currentUser.uid; Result.updateRateQuote(randomQuote); Result.updateGraphPBLine(); AccountButton.loading(true); completedEvent.challenge = ChallengeContoller.verify(completedEvent); if (!completedEvent.challenge) delete completedEvent.challenge; completedEvent.hash = objecthash(completedEvent); axiosInstance .post("/results/add", { result: completedEvent, }) .then((response) => { AccountButton.loading(false); Result.hideCrown(); if (response.status !== 200) { Notifications.add("Result not saved. " + response.data.message, -1); } else { completedEvent._id = response.data.insertedId; if (response.data.isPb) { completedEvent.isPb = true; } DB.saveLocalResult(completedEvent); DB.updateLocalStats({ time: completedEvent.testDuration + completedEvent.incompleteTestSeconds - completedEvent.afkDuration, started: TestStats.restartCount + 1, }); try { firebase.analytics().logEvent("testCompleted", completedEvent); } catch (e) { console.log("Analytics unavailable"); } if (response.data.isPb) { //new pb Result.showCrown(); Result.updateCrown(); DB.saveLocalPB( Config.mode, completedEvent.mode2, Config.punctuation, Config.language, Config.difficulty, Config.lazyMode, completedEvent.wpm, completedEvent.acc, completedEvent.rawWpm, completedEvent.consistency ); } } $("#retrySavingResultButton").addClass("hidden"); }) .catch((e) => { AccountButton.loading(false); let msg = e?.response?.data?.message ?? e.message; Notifications.add("Failed to save result: " + msg, -1); $("#retrySavingResultButton").removeClass("hidden"); retrySaving.completedEvent = completedEvent; retrySaving.canRetry = true; }); } export function fail(reason) { failReason = reason; // input.pushHistory(); // corrected.pushHistory(); TestStats.pushKeypressesToHistory(); finish(true); let testSeconds = TestStats.calculateTestSeconds(performance.now()); let afkseconds = TestStats.calculateAfkSeconds(testSeconds); let tt = testSeconds - afkseconds; if (tt < 0) tt = 0; TestStats.incrementIncompleteSeconds(tt); TestStats.incrementRestartCount(); } ==> ./monkeytype/src/js/test/test-ui.js <== import * as Notifications from "./notifications"; import * as ThemeColors from "./theme-colors"; import Config, * as UpdateConfig from "./config"; import * as DB from "./db"; import * as TestLogic from "./test-logic"; import * as Funbox from "./funbox"; import * as PaceCaret from "./pace-caret"; import * as CustomText from "./custom-text"; import * as Keymap from "./keymap"; import * as Caret from "./caret"; import * as CommandlineLists from "./commandline-lists"; import * as Commandline from "./commandline"; import * as OutOfFocus from "./out-of-focus"; import * as ManualRestart from "./manual-restart-tracker"; import * as PractiseWords from "./practise-words"; import * as Replay from "./replay"; import * as TestStats from "./test-stats"; import * as Misc from "./misc"; import * as TestUI from "./test-ui"; import * as ChallengeController from "./challenge-controller"; import * as RateQuotePopup from "./rate-quote-popup"; import * as UI from "./ui"; import * as TestTimer from "./test-timer"; export let currentWordElementIndex = 0; export let resultVisible = false; export let activeWordTop = 0; export let testRestarting = false; export let lineTransition = false; export let currentTestLine = 0; export let resultCalculating = false; export function setResultVisible(val) { resultVisible = val; } export function setCurrentWordElementIndex(val) { currentWordElementIndex = val; } export function setActiveWordTop(val) { activeWordTop = val; } export function setTestRestarting(val) { testRestarting = val; } export function setResultCalculating(val) { resultCalculating = val; } export function reset() { currentTestLine = 0; currentWordElementIndex = 0; } export function focusWords() { if (!$("#wordsWrapper").hasClass("hidden")) { $("#wordsInput").focus(); } } export function updateActiveElement(backspace) { let active = document.querySelector("#words .active"); if (Config.mode == "zen" && backspace) { active.remove(); } else if (active !== null) { if (Config.highlightMode == "word") { active.querySelectorAll("letter").forEach((e) => { e.classList.remove("correct"); }); } active.classList.remove("active"); } try { let activeWord = document.querySelectorAll("#words .word")[ currentWordElementIndex ]; activeWord.classList.add("active"); activeWord.classList.remove("error"); activeWordTop = document.querySelector("#words .active").offsetTop; if (Config.highlightMode == "word") { activeWord.querySelectorAll("letter").forEach((e) => { e.classList.add("correct"); }); } } catch (e) {} } function getWordHTML(word) { let newlineafter = false; let retval = `
`; for (let c = 0; c < word.length; c++) { if (Config.funbox === "arrows") { if (word.charAt(c) === "↑") { retval += ``; } if (word.charAt(c) === "↓") { retval += ``; } if (word.charAt(c) === "←") { retval += ``; } if (word.charAt(c) === "→") { retval += ``; } } else if (word.charAt(c) === "t") { retval += ``; } else if (word.charAt(c) === "\n") { newlineafter = true; retval += ``; } else { retval += "" + word.charAt(c) + ""; } } retval += "
"; if (newlineafter) retval += "
"; return retval; } export function showWords() { $("#words").empty(); let wordsHTML = ""; if (Config.mode !== "zen") { for (let i = 0; i < TestLogic.words.length; i++) { wordsHTML += getWordHTML(TestLogic.words.get(i)); } } else { wordsHTML = '
word height
'; } $("#words").html(wordsHTML); $("#wordsWrapper").removeClass("hidden"); const wordHeight = $(document.querySelector(".word")).outerHeight(true); const wordsHeight = $(document.querySelector("#words")).outerHeight(true); console.log( `Showing words. wordHeight: ${wordHeight}, wordsHeight: ${wordsHeight}` ); if ( Config.showAllLines && Config.mode != "time" && !(CustomText.isWordRandom && CustomText.word == 0) && !CustomText.isTimeRandom ) { $("#words").css("height", "auto"); $("#wordsWrapper").css("height", "auto"); let nh = wordHeight * 3; if (nh > wordsHeight) { nh = wordsHeight; } $(".outOfFocusWarning").css("line-height", nh + "px"); } else { $("#words") .css("height", wordHeight * 4 + "px") .css("overflow", "hidden"); $("#wordsWrapper") .css("height", wordHeight * 3 + "px") .css("overflow", "hidden"); $(".outOfFocusWarning").css("line-height", wordHeight * 3 + "px"); } if (Config.mode === "zen") { $(document.querySelector(".word")).remove(); } else { if (Config.keymapMode === "next") { Keymap.highlightKey( TestLogic.words .getCurrent() .substring( TestLogic.input.current.length, TestLogic.input.current.length + 1 ) .toString() .toUpperCase() ); } } updateActiveElement(); Funbox.toggleScript(TestLogic.words.getCurrent()); Caret.updatePosition(); } export function addWord(word) { $("#words").append(getWordHTML(word)); } export function flipColors(tf) { if (tf) { $("#words").addClass("flipped"); } else { $("#words").removeClass("flipped"); } } export function colorful(tc) { if (tc) { $("#words").addClass("colorfulMode"); } else { $("#words").removeClass("colorfulMode"); } } export async function screenshot() { let revealReplay = false; function revertScreenshot() { $("#notificationCenter").removeClass("hidden"); $("#commandLineMobileButton").removeClass("hidden"); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .ssWatermark").text("monkeytype.com"); $(".pageTest .buttons").removeClass("hidden"); if (revealReplay) $("#resultReplay").removeClass("hidden"); if (firebase.auth().currentUser == null) $(".pageTest .loginTip").removeClass("hidden"); } if (!$("#resultReplay").hasClass("hidden")) { revealReplay = true; Replay.pauseReplay(); } $("#resultReplay").addClass("hidden"); $(".pageTest .ssWatermark").removeClass("hidden"); $(".pageTest .ssWatermark").text( moment(Date.now()).format("DD MMM YYYY HH:mm") + " | monkeytype.com " ); if (firebase.auth().currentUser != null) { $(".pageTest .ssWatermark").text( DB.getSnapshot().name + " | " + moment(Date.now()).format("DD MMM YYYY HH:mm") + " | monkeytype.com " ); } $(".pageTest .buttons").addClass("hidden"); let src = $("#middle"); var sourceX = src.position().left; /*X position from div#target*/ var sourceY = src.position().top; /*Y position from div#target*/ var sourceWidth = src.outerWidth( true ); /*clientWidth/offsetWidth from div#target*/ var sourceHeight = src.outerHeight( true ); /*clientHeight/offsetHeight from div#target*/ $("#notificationCenter").addClass("hidden"); $("#commandLineMobileButton").addClass("hidden"); $(".pageTest .loginTip").addClass("hidden"); try { let paddingX = 50; let paddingY = 25; html2canvas(document.body, { backgroundColor: await ThemeColors.get("bg"), width: sourceWidth + paddingX * 2, height: sourceHeight + paddingY * 2, x: sourceX - paddingX, y: sourceY - paddingY, }).then(function (canvas) { canvas.toBlob(function (blob) { try { if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) { open(URL.createObjectURL(blob)); revertScreenshot(); } else { navigator.clipboard .write([ new ClipboardItem( Object.defineProperty({}, blob.type, { value: blob, enumerable: true, }) ), ]) .then(() => { Notifications.add("Copied to clipboard", 1, 2); revertScreenshot(); }); } } catch (e) { Notifications.add( "Error saving image to clipboard: " + e.message, -1 ); revertScreenshot(); } }); }); } catch (e) { Notifications.add("Error creating image: " + e.message, -1); revertScreenshot(); } setTimeout(() => { revertScreenshot(); }, 3000); } export function updateWordElement(showError = !Config.blindMode) { let input = TestLogic.input.current; let wordAtIndex; let currentWord; wordAtIndex = document.querySelector("#words .word.active"); currentWord = TestLogic.words.getCurrent(); let ret = ""; let newlineafter = false; if (Config.mode === "zen") { for (let i = 0; i < TestLogic.input.current.length; i++) { if (TestLogic.input.current[i] === "t") { ret += ``; } else if (TestLogic.input.current[i] === "\n") { newlineafter = true; ret += ``; } else { ret += `${TestLogic.input.current[i]}`; } } } else { let correctSoFar = false; // slice earlier if input has trailing compose characters const inputWithoutComposeLength = Misc.trailingComposeChars.test(input) ? input.search(Misc.trailingComposeChars) : input.length; if ( input.search(Misc.trailingComposeChars) < currentWord.length && currentWord.slice(0, inputWithoutComposeLength) === input.slice(0, inputWithoutComposeLength) ) { correctSoFar = true; } let wordHighlightClassString = correctSoFar ? "correct" : "incorrect"; if (Config.blindMode) { wordHighlightClassString = "correct"; } for (let i = 0; i < input.length; i++) { let charCorrect = currentWord[i] == input[i]; let correctClass = "correct"; if (Config.highlightMode == "off") { correctClass = ""; } let currentLetter = currentWord[i]; let tabChar = ""; let nlChar = ""; if (Config.funbox === "arrows") { if (currentLetter === "↑") { currentLetter = ``; } if (currentLetter === "↓") { currentLetter = ``; } if (currentLetter === "←") { currentLetter = ``; } if (currentLetter === "→") { currentLetter = ``; } } else if (currentLetter === "t") { tabChar = "tabChar"; currentLetter = ``; } else if (currentLetter === "\n") { nlChar = "nlChar"; currentLetter = ``; } if ( Misc.trailingComposeChars.test(input) && i > input.search(Misc.trailingComposeChars) ) continue; if (charCorrect) { ret += `${currentLetter}`; } else if ( currentLetter !== undefined && Misc.trailingComposeChars.test(input) && i === input.search(Misc.trailingComposeChars) ) { ret += `${currentLetter}`; } else if (!showError) { if (currentLetter !== undefined) { ret += `${currentLetter}`; } } else if (currentLetter === undefined) { if (!Config.hideExtraLetters) { let letter = input[i]; if (letter == " " || letter == "t" || letter == "\n") { letter = "_"; } ret += `${letter}`; } } else { ret += `` + currentLetter + (Config.indicateTypos ? `${input[i]}` : "") + ""; } } const inputWithSingleComposeLength = Misc.trailingComposeChars.test(input) ? input.search(Misc.trailingComposeChars) + 1 : input.length; if (inputWithSingleComposeLength < currentWord.length) { for (let i = inputWithSingleComposeLength; i < currentWord.length; i++) { if (Config.funbox === "arrows") { if (currentWord[i] === "↑") { ret += ``; } if (currentWord[i] === "↓") { ret += ``; } if (currentWord[i] === "←") { ret += ``; } if (currentWord[i] === "→") { ret += ``; } } else if (currentWord[i] === "t") { ret += ``; } else if (currentWord[i] === "\n") { ret += ``; } else { ret += `` + currentWord[i] + ""; } } } if (Config.highlightMode === "letter" && Config.hideExtraLetters) { if (input.length > currentWord.length && !Config.blindMode) { $(wordAtIndex).addClass("error"); } else if (input.length == currentWord.length) { $(wordAtIndex).removeClass("error"); } } } wordAtIndex.innerHTML = ret; if (newlineafter) $("#words").append("
"); } export function lineJump(currentTop) { //last word of the line if (currentTestLine > 0) { let hideBound = currentTop; let toHide = []; let wordElements = $("#words .word"); for (let i = 0; i < currentWordElementIndex; i++) { if ($(wordElements[i]).hasClass("hidden")) continue; let forWordTop = Math.floor(wordElements[i].offsetTop); if (forWordTop < hideBound - 10) { toHide.push($($("#words .word")[i])); } } const wordHeight = $(document.querySelector(".word")).outerHeight(true); if (Config.smoothLineScroll && toHide.length > 0) { lineTransition = true; $("#words").prepend( `
` ); $("#words .smoothScroller").animate( { height: 0, }, TestTimer.slowTimer ? 0 : 125, () => { $("#words .smoothScroller").remove(); } ); $("#paceCaret").animate( { top: document.querySelector("#paceCaret").offsetTop - wordHeight, }, TestTimer.slowTimer ? 0 : 125 ); $("#words").animate( { marginTop: `-${wordHeight}px`, }, TestTimer.slowTimer ? 0 : 125, () => { activeWordTop = document.querySelector("#words .active").offsetTop; currentWordElementIndex -= toHide.length; lineTransition = false; toHide.forEach((el) => el.remove()); $("#words").css("marginTop", "0"); } ); } else { toHide.forEach((el) => el.remove()); currentWordElementIndex -= toHide.length; $("#paceCaret").css({ top: document.querySelector("#paceCaret").offsetTop - wordHeight, }); } } currentTestLine++; } export function updateModesNotice() { let anim = false; if ($(".pageTest #testModesNotice").text() === "") anim = true; $(".pageTest #testModesNotice").empty(); if (TestLogic.isRepeated && Config.mode !== "quote") { $(".pageTest #testModesNotice").append( `
repeated
` ); } if (TestLogic.hasTab) { $(".pageTest #testModesNotice").append( `
shift + tab to restart
` ); } if (ChallengeController.active) { $(".pageTest #testModesNotice").append( `
${ChallengeController.active.display}
` ); } if (Config.mode === "zen") { $(".pageTest #testModesNotice").append( `
shift + enter to finish zen
` ); } // /^[0-9a-zA-Z_.-]+$/.test(name); if ( (/_\d+k$/g.test(Config.language) || /code_/g.test(Config.language) || Config.language == "english_commonly_misspelled") && Config.mode !== "quote" ) { $(".pageTest #testModesNotice").append( `
${Config.language.replace( /_/g, " " )}
` ); } if (Config.difficulty === "expert") { $(".pageTest #testModesNotice").append( `
expert
` ); } else if (Config.difficulty === "master") { $(".pageTest #testModesNotice").append( `
master
` ); } if (Config.blindMode) { $(".pageTest #testModesNotice").append( `
blind
` ); } if (Config.lazyMode) { $(".pageTest #testModesNotice").append( `
lazy
` ); } if ( Config.paceCaret !== "off" || (Config.repeatedPace && TestLogic.isPaceRepeat) ) { let speed = ""; try { speed = ` (${Math.round(PaceCaret.settings.wpm)} wpm)`; } catch {} $(".pageTest #testModesNotice").append( `
${ Config.paceCaret === "average" ? "average" : Config.paceCaret === "pb" ? "pb" : "custom" } pace${speed}
` ); } if (Config.minWpm !== "off") { $(".pageTest #testModesNotice").append( `
min ${Config.minWpmCustomSpeed} wpm
` ); } if (Config.minAcc !== "off") { $(".pageTest #testModesNotice").append( `
min ${Config.minAccCustom}% acc
` ); } if (Config.minBurst !== "off") { $(".pageTest #testModesNotice").append( `
min ${ Config.minBurstCustomSpeed } burst ${Config.minBurst === "flex" ? "(flex)" : ""}
` ); } if (Config.funbox !== "none") { $(".pageTest #testModesNotice").append( `
${Config.funbox.replace( /_/g, " " )}
` ); } if (Config.confidenceMode === "on") { $(".pageTest #testModesNotice").append( `
confidence
` ); } if (Config.confidenceMode === "max") { $(".pageTest #testModesNotice").append( `
max confidence
` ); } if (Config.stopOnError != "off") { $(".pageTest #testModesNotice").append( `
stop on ${Config.stopOnError}
` ); } if (Config.layout !== "default") { $(".pageTest #testModesNotice").append( `
emulating ${Config.layout.replace( /_/g, " " )}
` ); } if (Config.oppositeShiftMode !== "off") { $(".pageTest #testModesNotice").append( `
opposite shift${ Config.oppositeShiftMode === "keymap" ? " (keymap)" : "" }
` ); } let tagsString = ""; try { DB.getSnapshot().tags.forEach((tag) => { if (tag.active === true) { tagsString += tag.name + ", "; } }); if (tagsString !== "") { $(".pageTest #testModesNotice").append( `
${tagsString.substring( 0, tagsString.length - 2 )}
` ); } } catch {} if (anim) { $(".pageTest #testModesNotice") .css("transition", "none") .css("opacity", 0) .animate( { opacity: 1, }, 125, () => { $(".pageTest #testModesNotice").css("transition", ".125s"); } ); } } export function arrangeCharactersRightToLeft() { $("#words").addClass("rightToLeftTest"); $("#resultWordsHistory .words").addClass("rightToLeftTest"); $("#resultReplay .words").addClass("rightToLeftTest"); } export function arrangeCharactersLeftToRight() { $("#words").removeClass("rightToLeftTest"); $("#resultWordsHistory .words").removeClass("rightToLeftTest"); $("#resultReplay .words").removeClass("rightToLeftTest"); } async function loadWordsHistory() { $("#resultWordsHistory .words").empty(); let wordsHTML = ""; for (let i = 0; i < TestLogic.input.history.length + 2; i++) { let input = TestLogic.input.getHistory(i); let word = TestLogic.words.get(i); let wordEl = ""; try { if (input === "") throw new Error("empty input word"); if ( TestLogic.corrected.getHistory(i) !== undefined && TestLogic.corrected.getHistory(i) !== "" ) { wordEl = `
`; } else { wordEl = `
`; } if (i === TestLogic.input.history.length - 1) { //last word let wordstats = { correct: 0, incorrect: 0, missed: 0, }; let length = Config.mode == "zen" ? input.length : word.length; for (let c = 0; c < length; c++) { if (c < input.length) { //on char that still has a word list pair if (Config.mode == "zen" || input[c] == word[c]) { wordstats.correct++; } else { wordstats.incorrect++; } } else { //on char that is extra wordstats.missed++; } } if (wordstats.incorrect !== 0 || Config.mode !== "time") { if (Config.mode != "zen" && input !== word) { wordEl = `
`; } } } else { if (Config.mode != "zen" && input !== word) { wordEl = `
`; } } let loop; if (Config.mode == "zen" || input.length > word.length) { //input is longer - extra characters possible (loop over input) loop = input.length; } else { //input is shorter or equal (loop over word list) loop = word.length; } for (let c = 0; c < loop; c++) { let correctedChar; try { correctedChar = TestLogic.corrected.getHistory(i)[c]; } catch (e) { correctedChar = undefined; } let extraCorrected = ""; if ( c + 1 === loop && TestLogic.corrected.getHistory(i) !== undefined && TestLogic.corrected.getHistory(i).length > input.length ) { extraCorrected = "extraCorrected"; } if (Config.mode == "zen" || word[c] !== undefined) { if (Config.mode == "zen" || input[c] === word[c]) { if (correctedChar === input[c] || correctedChar === undefined) { wordEl += `${input[c]}`; } else { wordEl += `` + input[c] + ""; } } else { if (input[c] === TestLogic.input.current) { wordEl += `` + word[c] + ""; } else if (input[c] === undefined) { wordEl += "" + word[c] + ""; } else { wordEl += `` + word[c] + ""; } } } else { wordEl += '' + input[c] + ""; } } wordEl += "
"; } catch (e) { try { wordEl = "
"; for (let c = 0; c < word.length; c++) { wordEl += "" + word[c] + ""; } wordEl += "
"; } catch {} } wordsHTML += wordEl; } $("#resultWordsHistory .words").html(wordsHTML); $("#showWordHistoryButton").addClass("loaded"); return true; } export function toggleResultWords() { if (resultVisible) { if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) { //show if (!$("#showWordHistoryButton").hasClass("loaded")) { $("#words").html( `
` ); loadWordsHistory().then(() => { if (Config.burstHeatmap) { TestUI.applyBurstHeatmap(); } $("#resultWordsHistory") .removeClass("hidden") .css("display", "none") .slideDown(250, () => { if (Config.burstHeatmap) { TestUI.applyBurstHeatmap(); } }); }); } else { if (Config.burstHeatmap) { TestUI.applyBurstHeatmap(); } $("#resultWordsHistory") .removeClass("hidden") .css("display", "none") .slideDown(250); } } else { //hide $("#resultWordsHistory").slideUp(250, () => { $("#resultWordsHistory").addClass("hidden"); }); } } } export function applyBurstHeatmap() { if (Config.burstHeatmap) { $("#resultWordsHistory .heatmapLegend").removeClass("hidden"); let burstlist = [...TestStats.burstHistory]; burstlist = burstlist.filter((x) => x !== Infinity); burstlist = burstlist.filter((x) => x < 350); if ( TestLogic.input.getHistory(TestLogic.input.getHistory().length - 1) .length !== TestLogic.words.getCurrent()?.length ) { burstlist = burstlist.splice(0, burstlist.length - 1); } let median = Misc.median(burstlist); let adatm = []; burstlist.forEach((burst) => { adatm.push(Math.abs(median - burst)); }); let step = Misc.mean(adatm); let steps = [ { val: 0, class: "heatmap-0", }, { val: median - step * 1.5, class: "heatmap-1", }, { val: median - step * 0.5, class: "heatmap-2", }, { val: median + step * 0.5, class: "heatmap-3", }, { val: median + step * 1.5, class: "heatmap-4", }, ]; $("#resultWordsHistory .words .word").each((index, word) => { let wordBurstVal = parseInt($(word).attr("burst")); let cls = ""; steps.forEach((step) => { if (wordBurstVal > step.val) cls = step.class; }); $(word).addClass(cls); }); } else { $("#resultWordsHistory .heatmapLegend").addClass("hidden"); $("#resultWordsHistory .words .word").removeClass("heatmap-0"); $("#resultWordsHistory .words .word").removeClass("heatmap-1"); $("#resultWordsHistory .words .word").removeClass("heatmap-2"); $("#resultWordsHistory .words .word").removeClass("heatmap-3"); $("#resultWordsHistory .words .word").removeClass("heatmap-4"); } } export function highlightBadWord(index, showError) { if (!showError) return; $($("#words .word")[index]).addClass("error"); } $(document.body).on("click", "#saveScreenshotButton", () => { screenshot(); }); $(document).on("click", "#testModesNotice .text-button.restart", (event) => { TestLogic.restart(); }); $(document).on("click", "#testModesNotice .text-button.blind", (event) => { UpdateConfig.toggleBlindMode(); }); $(".pageTest #copyWordsListButton").click(async (event) => { try { let words; if (Config.mode == "zen") { words = TestLogic.input.history.join(" "); } else { words = TestLogic.words .get() .slice(0, TestLogic.input.history.length) .join(" "); } await navigator.clipboard.writeText(words); Notifications.add("Copied to clipboard", 0, 2); } catch (e) { Notifications.add("Could not copy to clipboard: " + e, -1); } }); $(".pageTest #rateQuoteButton").click(async (event) => { RateQuotePopup.show(TestLogic.randomQuote); }); $(".pageTest #toggleBurstHeatmap").click(async (event) => { UpdateConfig.setBurstHeatmap(!Config.burstHeatmap); }); $(".pageTest .loginTip .link").click(async (event) => { UI.changePage("login"); }); $(document).on("mouseleave", "#resultWordsHistory .words .word", (e) => { $(".wordInputAfter").remove(); }); $("#wpmChart").on("mouseleave", (e) => { $(".wordInputAfter").remove(); }); $(document).on("mouseenter", "#resultWordsHistory .words .word", (e) => { if (resultVisible) { let input = $(e.currentTarget).attr("input"); let burst = $(e.currentTarget).attr("burst"); if (input != undefined) $(e.currentTarget).append( `
${input .replace(/t/g, "_") .replace(/\n/g, "_") .replace(//g, ">")}
${Math.round(Config.alwaysShowCPM ? burst * 5 : burst)}${ Config.alwaysShowCPM ? "cpm" : "wpm" }
` ); } }); $(document).on("click", "#testModesNotice .text-button", (event) => { // console.log("CommandlineLists."+$(event.currentTarget).attr("commands")); let commands = CommandlineLists.getList( $(event.currentTarget).attr("commands") ); let func = $(event.currentTarget).attr("function"); if (commands !== undefined) { if ($(event.currentTarget).attr("commands") === "commandsTags") { CommandlineLists.updateTagCommands(); } CommandlineLists.pushCurrent(commands); Commandline.show(); } else if (func != undefined) { eval(func); } }); $("#wordsInput").on("focus", () => { if (!resultVisible && Config.showOutOfFocusWarning) { OutOfFocus.hide(); } Caret.show(TestLogic.input.current); }); $("#wordsInput").on("focusout", () => { if (!resultVisible && Config.showOutOfFocusWarning) { OutOfFocus.show(); } Caret.hide(); }); $(document).on("keypress", "#restartTestButton", (event) => { if (event.key == "Enter") { ManualRestart.reset(); if ( TestLogic.active && Config.repeatQuotes === "typing" && Config.mode === "quote" ) { TestLogic.restart(true); } else { TestLogic.restart(); } } }); $(document.body).on("click", "#restartTestButton", () => { ManualRestart.set(); if (resultCalculating) return; if ( TestLogic.active && Config.repeatQuotes === "typing" && Config.mode === "quote" ) { TestLogic.restart(true); } else { TestLogic.restart(); } }); $(document.body).on( "click", "#retrySavingResultButton", TestLogic.retrySavingResult ); $(document).on("keypress", "#practiseWordsButton", (event) => { if (event.keyCode == 13) { PractiseWords.showPopup(true); } }); $(document.body).on("click", "#practiseWordsButton", () => { // PractiseWords.init(); PractiseWords.showPopup(); }); $(document).on("keypress", "#nextTestButton", (event) => { if (event.keyCode == 13) { TestLogic.restart(); } }); $(document.body).on("click", "#nextTestButton", () => { ManualRestart.set(); TestLogic.restart(); }); $(document).on("keypress", "#showWordHistoryButton", (event) => { if (event.keyCode == 13) { toggleResultWords(); } }); $(document.body).on("click", "#showWordHistoryButton", () => { toggleResultWords(); }); $(document.body).on("click", "#restartTestButtonWithSameWordset", () => { if (Config.mode == "zen") { Notifications.add("Repeat test disabled in zen mode"); return; } ManualRestart.set(); TestLogic.restart(true); }); $(document).on("keypress", "#restartTestButtonWithSameWordset", (event) => { if (Config.mode == "zen") { Notifications.add("Repeat test disabled in zen mode"); return; } if (event.keyCode == 13) { TestLogic.restart(true); } }); $("#wordsWrapper").on("click", () => { focusWords(); }); ==> ./monkeytype/src/js/test/lazy-mode.js <== let accents = [ ["áàâäåãąą́āą̄ă", "a"], ["éèêëẽęę́ēę̄ėě", "e"], ["íìîïĩįį́īį̄", "i"], ["óòôöøõóōǫǫ́ǭő", "o"], ["úùûüŭũúūůű", "u"], ["ńň", "n"], ["çĉčć", "c"], ["ř", "r"], ["ď", "d"], ["ťț", "t"], ["æ", "ae"], ["œ", "oe"], ["ẅ", "w"], ["ĝğg̃", "g"], ["ĥ", "h"], ["ĵ", "j"], ["ń", "n"], ["ŝśšș", "s"], ["żźž", "z"], ["ÿỹýÿŷ", "y"], ["ł", "l"], ["أإآ", "ا"], ["َ", ""], ["ُ", ""], ["ِ", ""], ["ْ", ""], ["ً", ""], ["ٌ", ""], ["ٍ", ""], ["ّ", ""], ]; export function replaceAccents(word, accentsOverride) { let newWord = word; if (!accents && !accentsOverride) return newWord; let regex; let list = accentsOverride || accents; for (let i = 0; i < list.length; i++) { regex = new RegExp(`[${list[i][0]}]`, "gi"); newWord = newWord.replace(regex, list[i][1]); } return newWord; } ==> ./monkeytype/src/js/test/british-english.js <== import { capitalizeFirstLetter } from "./misc"; let list = null; export async function getList() { if (list == null) { return $.getJSON("languages/britishenglish.json", function (data) { list = data; return list; }); } else { return list; } } export async function replace(word) { let list = await getList(); let replacement = list.find((a) => word.match(RegExp(`^([\\W]*${a[0]}[\\W]*)$`, "gi")) ); return replacement ? word.replace( RegExp(`^(?:([\\W]*)(${replacement[0]})([\\W]*))$`, "gi"), (_, $1, $2, $3) => $1 + ($2.charAt(0) === $2.charAt(0).toUpperCase() ? $2 === $2.toUpperCase() ? replacement[1].toUpperCase() : capitalizeFirstLetter(replacement[1]) : replacement[1]) + $3 ) : word; } ==> ./monkeytype/src/js/test/custom-text.js <== export let text = "The quick brown fox jumps over the lazy dog".split(" "); export let isWordRandom = false; export let isTimeRandom = false; export let word = ""; export let time = ""; export let delimiter = " "; export function setText(txt) { text = txt; } export function setIsWordRandom(val) { isWordRandom = val; } export function setIsTimeRandom(val) { isTimeRandom = val; } export function setTime(val) { time = val; } export function setWord(val) { word = val; } export function setDelimiter(val) { delimiter = val; } ==> ./monkeytype/src/js/test/live-burst.js <== import Config from "./config"; import * as TestLogic from "./test-logic"; export function update(burst) { let number = burst; if (Config.blindMode) { number = 0; } document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = number; document.querySelector("#liveBurst").innerHTML = number; } export function show() { if (!Config.showLiveBurst) return; if (!TestLogic.active) return; if (Config.timerStyle === "mini") { if (!$("#miniTimerAndLiveWpm .burst").hasClass("hidden")) return; $("#miniTimerAndLiveWpm .burst") .removeClass("hidden") .css("opacity", 0) .animate( { opacity: Config.timerOpacity, }, 125 ); } else { if (!$("#liveBurst").hasClass("hidden")) return; $("#liveBurst").removeClass("hidden").css("opacity", 0).animate( { opacity: Config.timerOpacity, }, 125 ); } } export function hide() { $("#liveBurst").animate( { opacity: Config.timerOpacity, }, 125, () => { $("#liveBurst").addClass("hidden"); } ); $("#miniTimerAndLiveWpm .burst").animate( { opacity: Config.timerOpacity, }, 125, () => { $("#miniTimerAndLiveWpm .burst").addClass("hidden"); } ); } ==> ./monkeytype/src/js/test/live-wpm.js <== import Config from "./config"; import * as TestLogic from "./test-logic"; let liveWpmElement = document.querySelector("#liveWpm"); let miniLiveWpmElement = document.querySelector("#miniTimerAndLiveWpm .wpm"); export function update(wpm, raw) { // if (!TestLogic.active || !Config.showLiveWpm) { // hideLiveWpm(); // } else { // showLiveWpm(); // } let number = wpm; if (Config.blindMode) { number = raw; } if (Config.alwaysShowCPM) { number = Math.round(number * 5); } miniLiveWpmElement.innerHTML = number; liveWpmElement.innerHTML = number; } export function show() { if (!Config.showLiveWpm) return; if (!TestLogic.active) return; if (Config.timerStyle === "mini") { // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity); if (!$("#miniTimerAndLiveWpm .wpm").hasClass("hidden")) return; $("#miniTimerAndLiveWpm .wpm") .removeClass("hidden") .css("opacity", 0) .animate( { opacity: Config.timerOpacity, }, 125 ); } else { // $("#liveWpm").css("opacity", Config.timerOpacity); if (!$("#liveWpm").hasClass("hidden")) return; $("#liveWpm").removeClass("hidden").css("opacity", 0).animate( { opacity: Config.timerOpacity, }, 125 ); } } export function hide() { $("#liveWpm").animate( { opacity: Config.timerOpacity, }, 125, () => { $("#liveWpm").addClass("hidden"); } ); $("#miniTimerAndLiveWpm .wpm").animate( { opacity: Config.timerOpacity, }, 125, () => { $("#miniTimerAndLiveWpm .wpm").addClass("hidden"); } ); } ==> ./monkeytype/src/js/test/focus.js <== import * as Caret from "./caret"; import * as UI from "./ui"; let state = false; export function set(foc, withCursor = false) { if (foc && !state) { state = true; Caret.stopAnimation(); $("#top").addClass("focus"); $("#bottom").addClass("focus"); if (!withCursor) $("body").css("cursor", "none"); $("#middle").addClass("focus"); } else if (!foc && state) { state = false; Caret.startAnimation(); $("#top").removeClass("focus"); $("#bottom").removeClass("focus"); $("body").css("cursor", "default"); $("#middle").removeClass("focus"); } } $(document).mousemove(function (event) { if (!state) return; if (UI.getActivePage() == "pageLoading") return; if (UI.getActivePage() == "pageAccount" && state == true) return; if ( $("#top").hasClass("focus") && (event.originalEvent.movementX > 0 || event.originalEvent.movementY > 0) ) { set(false); } }); ==> ./monkeytype/src/js/test/today-tracker.js <== import * as Misc from "./misc"; import * as DB from "./db"; let seconds = 0; let addedAllToday = false; let dayToday = null; export function addSeconds(s) { if (addedAllToday) { let nowDate = new Date(); nowDate = nowDate.getDate(); if (nowDate > dayToday) { seconds = s; return; } } seconds += s; } export function getString() { let secString = Misc.secondsToString(Math.round(seconds), true, true); return secString + (addedAllToday === true ? " today" : " session"); } export async function addAllFromToday() { let todayDate = new Date(); todayDate.setSeconds(0); todayDate.setMinutes(0); todayDate.setHours(0); todayDate.setMilliseconds(0); dayToday = todayDate.getDate(); todayDate = todayDate.getTime(); seconds = 0; let results = await DB.getSnapshot().results; results.forEach((result) => { let resultDate = new Date(result.timestamp); resultDate.setSeconds(0); resultDate.setMinutes(0); resultDate.setHours(0); resultDate.setMilliseconds(0); resultDate = resultDate.getTime(); if (resultDate >= todayDate) { seconds += result.testDuration + result.incompleteTestSeconds - result.afkDuration; } }); addedAllToday = true; } ==> ./monkeytype/src/js/test/wikipedia.js <== import * as Loader from "./loader"; import Config from "./config"; import * as Misc from "./misc"; export class Section { constructor(title, author, words) { this.title = title; this.author = author; this.words = words; } } export async function getTLD(languageGroup) { // language group to tld switch (languageGroup.name) { case "english": return "en"; case "spanish": return "es"; case "french": return "fr"; case "german": return "de"; case "portuguese": return "pt"; case "italian": return "it"; case "dutch": return "nl"; default: return "en"; } } export async function getSection() { // console.log("Getting section"); Loader.show(); // get TLD for wikipedia according to language group let urlTLD = "en"; let currentLanguageGroup = await Misc.findCurrentGroup(Config.language); urlTLD = await getTLD(currentLanguageGroup); const randomPostURL = `https://${urlTLD}.wikipedia.org/api/rest_v1/page/random/summary`; var sectionObj = {}; var randomPostReq = await fetch(randomPostURL); var pageid = 0; if (randomPostReq.status == 200) { let postObj = await randomPostReq.json(); sectionObj.title = postObj.title; sectionObj.author = postObj.author; pageid = postObj.pageid; } return new Promise((res, rej) => { if (randomPostReq.status != 200) { Loader.hide(); rej(randomPostReq.status); } const sectionURL = `https://${urlTLD}.wikipedia.org/w/api.php?action=query&format=json&pageids=${pageid}&prop=extracts&exintro=true&origin=*`; var sectionReq = new XMLHttpRequest(); sectionReq.onload = () => { if (sectionReq.readyState == 4) { if (sectionReq.status == 200) { let sectionText = JSON.parse(sectionReq.responseText).query.pages[ pageid.toString() ].extract; let words = []; // Remove double whitespaces and finally trailing whitespaces. sectionText = sectionText.replace(/<\/p>

+/g, " "); sectionText = $("

").html(sectionText).text(); sectionText = sectionText.replace(/\s+/g, " "); sectionText = sectionText.trim(); // // Add spaces // sectionText = sectionText.replace(/[a-zA-Z0-9]{3,}\.[a-zA-Z]/g, (x) => // x.replace(/\./, ". ") // ); sectionText.split(" ").forEach((word) => { words.push(word); }); let section = new Section(sectionObj.title, sectionObj.author, words); Loader.hide(); res(section); } else { Loader.hide(); rej(sectionReq.status); } } }; sectionReq.open("GET", sectionURL); sectionReq.send(); }); } ==> ./monkeytype/src/js/test/timer-progress.js <== import Config from "./config"; import * as CustomText from "./custom-text"; import * as Misc from "./misc"; import * as TestLogic from "./test-logic"; import * as TestTimer from "./test-timer"; export function show() { let op = Config.showTimerProgress ? Config.timerOpacity : 0; if (Config.mode != "zen" && Config.timerStyle === "bar") { $("#timerWrapper").stop(true, true).removeClass("hidden").animate( { opacity: op, }, 125 ); } else if (Config.timerStyle === "text") { $("#timerNumber") .stop(true, true) .removeClass("hidden") .css("opacity", 0) .animate( { opacity: op, }, 125 ); } else if (Config.mode == "zen" || Config.timerStyle === "mini") { if (op > 0) { $("#miniTimerAndLiveWpm .time") .stop(true, true) .removeClass("hidden") .animate( { opacity: op, }, 125 ); } } } export function hide() { $("#timerWrapper").stop(true, true).animate( { opacity: 0, }, 125 ); $("#miniTimerAndLiveWpm .time") .stop(true, true) .animate( { opacity: 0, }, 125, () => { $("#miniTimerAndLiveWpm .time").addClass("hidden"); } ); $("#timerNumber").stop(true, true).animate( { opacity: 0, }, 125 ); } export function restart() { if (Config.timerStyle === "bar") { if (Config.mode === "time") { $("#timer").stop(true, true).animate( { width: "100vw", }, 0 ); } else if (Config.mode === "words" || Config.mode === "custom") { $("#timer").stop(true, true).animate( { width: "0vw", }, 0 ); } } } let timerNumberElement = document.querySelector("#timerNumber"); let miniTimerNumberElement = document.querySelector( "#miniTimerAndLiveWpm .time" ); export function update() { let time = TestTimer.time; if ( Config.mode === "time" || (Config.mode === "custom" && CustomText.isTimeRandom) ) { let maxtime = Config.time; if (Config.mode === "custom" && CustomText.isTimeRandom) { maxtime = CustomText.time; } if (Config.timerStyle === "bar") { let percent = 100 - ((time + 1) / maxtime) * 100; $("#timer") .stop(true, true) .animate( { width: percent + "vw", }, TestTimer.slowTimer ? 0 : 1000, "linear" ); } else if (Config.timerStyle === "text") { let displayTime = Misc.secondsToString(maxtime - time); if (maxtime === 0) { displayTime = Misc.secondsToString(time); } timerNumberElement.innerHTML = "
" + displayTime + "
"; } else if (Config.timerStyle === "mini") { let displayTime = Misc.secondsToString(maxtime - time); if (maxtime === 0) { displayTime = Misc.secondsToString(time); } miniTimerNumberElement.innerHTML = displayTime; } } else if ( Config.mode === "words" || Config.mode === "custom" || Config.mode === "quote" ) { let outof = TestLogic.words.length; if (Config.mode === "words") { outof = Config.words; } if (Config.mode === "custom") { if (CustomText.isWordRandom) { outof = CustomText.word; } else { outof = CustomText.text.length; } } if (Config.mode === "quote") { outof = TestLogic?.randomQuote?.textSplit?.length ?? 1; } if (Config.timerStyle === "bar") { let percent = Math.floor( ((TestLogic.words.currentIndex + 1) / outof) * 100 ); $("#timer") .stop(true, true) .animate( { width: percent + "vw", }, TestTimer.slowTimer ? 0 : 250 ); } else if (Config.timerStyle === "text") { if (outof === 0) { timerNumberElement.innerHTML = "
" + `${TestLogic.input.history.length}` + "
"; } else { timerNumberElement.innerHTML = "
" + `${TestLogic.input.history.length}/${outof}` + "
"; } } else if (Config.timerStyle === "mini") { if (Config.words === 0) { miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}`; } else { miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}/${outof}`; } } } else if (Config.mode == "zen") { if (Config.timerStyle === "text") { timerNumberElement.innerHTML = "
" + `${TestLogic.input.history.length}` + "
"; } else { miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}`; } } } export function updateStyle() { if (!TestLogic.active) return; hide(); update(); setTimeout(() => { show(); }, 125); } ==> ./monkeytype/src/js/test/pb-crown.js <== export function hide() { $("#result .stats .wpm .crown").css("opacity", 0).addClass("hidden"); } export function show() { $("#result .stats .wpm .crown") .removeClass("hidden") .css("opacity", "0") .animate( { opacity: 1, }, 250, "easeOutCubic" ); } ==> ./monkeytype/src/js/test/out-of-focus.js <== import * as Misc from "./misc"; let outOfFocusTimeouts = []; export function hide() { $("#words").css("transition", "none").removeClass("blurred"); $(".outOfFocusWarning").addClass("hidden"); Misc.clearTimeouts(outOfFocusTimeouts); } export function show() { outOfFocusTimeouts.push( setTimeout(() => { $("#words").css("transition", "0.25s").addClass("blurred"); $(".outOfFocusWarning").removeClass("hidden"); }, 1000) ); } ==> ./monkeytype/src/js/test/result.js <== import * as TestUI from "./test-ui"; import Config from "./config"; import * as Misc from "./misc"; import * as TestStats from "./test-stats"; import * as Keymap from "./keymap"; import * as ChartController from "./chart-controller"; import * as UI from "./ui"; import * as ThemeColors from "./theme-colors"; import * as DB from "./db"; import * as TodayTracker from "./today-tracker"; import * as PbCrown from "./pb-crown"; import * as RateQuotePopup from "./rate-quote-popup"; import * as TestLogic from "./test-logic"; import * as Notifications from "./notifications"; let result; let maxChartVal; let useUnsmoothedRaw = false; export function toggleUnsmoothedRaw() { useUnsmoothedRaw = !useUnsmoothedRaw; Notifications.add(useUnsmoothedRaw ? "on" : "off", 1); } async function updateGraph() { ChartController.result.options.annotation.annotations = []; let labels = []; for (let i = 1; i <= TestStats.wpmHistory.length; i++) { if (TestStats.lastSecondNotRound && i === TestStats.wpmHistory.length) { labels.push(Misc.roundTo2(result.testDuration).toString()); } else { labels.push(i.toString()); } } ChartController.result.updateColors(); ChartController.result.data.labels = labels; ChartController.result.options.scales.yAxes[0].scaleLabel.labelString = Config.alwaysShowCPM ? "Character per Minute" : "Words per Minute"; let chartData1 = Config.alwaysShowCPM ? TestStats.wpmHistory.map((a) => a * 5) : TestStats.wpmHistory; let chartData2; if (useUnsmoothedRaw) { chartData2 = Config.alwaysShowCPM ? result.chartData.unsmoothedRaw.map((a) => a * 5) : result.chartData.unsmoothedRaw; } else { chartData2 = Config.alwaysShowCPM ? result.chartData.raw.map((a) => a * 5) : result.chartData.raw; } ChartController.result.data.datasets[0].data = chartData1; ChartController.result.data.datasets[1].data = chartData2; ChartController.result.data.datasets[0].label = Config.alwaysShowCPM ? "cpm" : "wpm"; maxChartVal = Math.max(...[Math.max(...chartData2), Math.max(...chartData1)]); if (!Config.startGraphsAtZero) { let minChartVal = Math.min( ...[Math.min(...chartData2), Math.min(...chartData1)] ); ChartController.result.options.scales.yAxes[0].ticks.min = minChartVal; ChartController.result.options.scales.yAxes[1].ticks.min = minChartVal; } else { ChartController.result.options.scales.yAxes[0].ticks.min = 0; ChartController.result.options.scales.yAxes[1].ticks.min = 0; } ChartController.result.data.datasets[2].data = result.chartData.err; let fc = await ThemeColors.get("sub"); if (Config.funbox !== "none") { let content = Config.funbox; if (Config.funbox === "layoutfluid") { content += " " + Config.customLayoutfluid.replace(/#/g, " "); } ChartController.result.options.annotation.annotations.push({ enabled: false, type: "line", mode: "horizontal", scaleID: "wpm", value: 0, borderColor: "transparent", borderWidth: 1, borderDash: [2, 2], label: { backgroundColor: "transparent", fontFamily: Config.fontFamily.replace(/_/g, " "), fontSize: 11, fontStyle: "normal", fontColor: fc, xPadding: 6, yPadding: 6, cornerRadius: 3, position: "left", enabled: true, content: `${content}`, yAdjust: -11, }, }); } ChartController.result.options.scales.yAxes[0].ticks.max = maxChartVal; ChartController.result.options.scales.yAxes[1].ticks.max = maxChartVal; ChartController.result.update({ duration: 0 }); ChartController.result.resize(); } export async function updateGraphPBLine() { let themecolors = await ThemeColors.get(); let lpb = await DB.getLocalPB( result.mode, result.mode2, result.punctuation, result.language, result.difficulty, result.lazyMode, result.funbox ); if (lpb == 0) return; let chartlpb = Misc.roundTo2(Config.alwaysShowCPM ? lpb * 5 : lpb).toFixed(2); ChartController.result.options.annotation.annotations.push({ enabled: false, type: "line", mode: "horizontal", scaleID: "wpm", value: chartlpb, borderColor: themecolors["sub"], borderWidth: 1, borderDash: [2, 2], label: { backgroundColor: themecolors["sub"], fontFamily: Config.fontFamily.replace(/_/g, " "), fontSize: 11, fontStyle: "normal", fontColor: themecolors["bg"], xPadding: 6, yPadding: 6, cornerRadius: 3, position: "center", enabled: true, content: `PB: ${chartlpb}`, }, }); if ( maxChartVal >= parseFloat(chartlpb) - 20 && maxChartVal <= parseFloat(chartlpb) + 20 ) { maxChartVal = parseFloat(chartlpb) + 20; } ChartController.result.options.scales.yAxes[0].ticks.max = Math.round( maxChartVal ); ChartController.result.options.scales.yAxes[1].ticks.max = Math.round( maxChartVal ); ChartController.result.update({ duration: 0 }); } function updateWpmAndAcc() { let inf = false; if (result.wpm >= 1000) { inf = true; } if (Config.alwaysShowDecimalPlaces) { if (Config.alwaysShowCPM == false) { $("#result .stats .wpm .top .text").text("wpm"); if (inf) { $("#result .stats .wpm .bottom").text("Infinite"); } else { $("#result .stats .wpm .bottom").text( Misc.roundTo2(result.wpm).toFixed(2) ); } $("#result .stats .raw .bottom").text( Misc.roundTo2(result.rawWpm).toFixed(2) ); $("#result .stats .wpm .bottom").attr( "aria-label", Misc.roundTo2(result.wpm * 5).toFixed(2) + " cpm" ); } else { $("#result .stats .wpm .top .text").text("cpm"); if (inf) { $("#result .stats .wpm .bottom").text("Infinite"); } else { $("#result .stats .wpm .bottom").text( Misc.roundTo2(result.wpm * 5).toFixed(2) ); } $("#result .stats .raw .bottom").text( Misc.roundTo2(result.rawWpm * 5).toFixed(2) ); $("#result .stats .wpm .bottom").attr( "aria-label", Misc.roundTo2(result.wpm).toFixed(2) + " wpm" ); } $("#result .stats .acc .bottom").text( result.acc == 100 ? "100%" : Misc.roundTo2(result.acc).toFixed(2) + "%" ); let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; if (result.testDuration > 61) { time = Misc.secondsToString(Misc.roundTo2(result.testDuration)); } $("#result .stats .time .bottom .text").text(time); $("#result .stats .raw .bottom").removeAttr("aria-label"); $("#result .stats .acc .bottom").removeAttr("aria-label"); } else { //not showing decimal places if (Config.alwaysShowCPM == false) { $("#result .stats .wpm .top .text").text("wpm"); $("#result .stats .wpm .bottom").attr( "aria-label", result.wpm + ` (${Misc.roundTo2(result.wpm * 5)} cpm)` ); if (inf) { $("#result .stats .wpm .bottom").text("Infinite"); } else { $("#result .stats .wpm .bottom").text(Math.round(result.wpm)); } $("#result .stats .raw .bottom").text(Math.round(result.rawWpm)); $("#result .stats .raw .bottom").attr("aria-label", result.rawWpm); } else { $("#result .stats .wpm .top .text").text("cpm"); $("#result .stats .wpm .bottom").attr( "aria-label", Misc.roundTo2(result.wpm * 5) + ` (${Misc.roundTo2(result.wpm)} wpm)` ); if (inf) { $("#result .stats .wpm .bottom").text("Infinite"); } else { $("#result .stats .wpm .bottom").text(Math.round(result.wpm * 5)); } $("#result .stats .raw .bottom").text(Math.round(result.rawWpm * 5)); $("#result .stats .raw .bottom").attr("aria-label", result.rawWpm * 5); } $("#result .stats .acc .bottom").text(Math.floor(result.acc) + "%"); $("#result .stats .acc .bottom").attr("aria-label", result.acc + "%"); } } function updateConsistency() { if (Config.alwaysShowDecimalPlaces) { $("#result .stats .consistency .bottom").text( Misc.roundTo2(result.consistency).toFixed(2) + "%" ); $("#result .stats .consistency .bottom").attr( "aria-label", `${result.keyConsistency.toFixed(2)}% key` ); } else { $("#result .stats .consistency .bottom").text( Math.round(result.consistency) + "%" ); $("#result .stats .consistency .bottom").attr( "aria-label", `${result.consistency}% (${result.keyConsistency}% key)` ); } } function updateTime() { let afkSecondsPercent = Misc.roundTo2( (result.afkDuration / result.testDuration) * 100 ); $("#result .stats .time .bottom .afk").text(""); if (afkSecondsPercent > 0) { $("#result .stats .time .bottom .afk").text(afkSecondsPercent + "% afk"); } $("#result .stats .time .bottom").attr( "aria-label", `${result.afkDuration}s afk ${afkSecondsPercent}%` ); if (Config.alwaysShowDecimalPlaces) { let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; if (result.testDuration > 61) { time = Misc.secondsToString(Misc.roundTo2(result.testDuration)); } $("#result .stats .time .bottom .text").text(time); } else { let time = Math.round(result.testDuration) + "s"; if (result.testDuration > 61) { time = Misc.secondsToString(Math.round(result.testDuration)); } $("#result .stats .time .bottom .text").text(time); $("#result .stats .time .bottom").attr( "aria-label", `${Misc.roundTo2(result.testDuration)}s (${ result.afkDuration }s afk ${afkSecondsPercent}%)` ); } } export function updateTodayTracker() { $("#result .stats .time .bottom .timeToday").text(TodayTracker.getString()); } function updateKey() { $("#result .stats .key .bottom").text( result.charStats[0] + "/" + result.charStats[1] + "/" + result.charStats[2] + "/" + result.charStats[3] ); } export function showCrown() { PbCrown.show(); } export function hideCrown() { PbCrown.hide(); $("#result .stats .wpm .crown").attr("aria-label", ""); } export async function updateCrown() { let pbDiff = 0; const lpb = await DB.getLocalPB( Config.mode, result.mode2, Config.punctuation, Config.language, Config.difficulty, Config.lazyMode, Config.funbox ); pbDiff = Math.abs(result.wpm - lpb); $("#result .stats .wpm .crown").attr( "aria-label", "+" + Misc.roundTo2(pbDiff) ); } function updateTags(dontSave) { let activeTags = []; try { DB.getSnapshot().tags.forEach((tag) => { if (tag.active === true) { activeTags.push(tag); } }); } catch (e) {} $("#result .stats .tags").addClass("hidden"); if (activeTags.length == 0) { $("#result .stats .tags").addClass("hidden"); } else { $("#result .stats .tags").removeClass("hidden"); } $("#result .stats .tags .bottom").text(""); let annotationSide = "left"; let labelAdjust = 15; activeTags.forEach(async (tag) => { let tpb = await DB.getLocalTagPB( tag._id, Config.mode, result.mode2, Config.punctuation, Config.language, Config.difficulty, Config.lazyMode ); $("#result .stats .tags .bottom").append(`
${tag.name}
`); if (Config.mode != "quote" && !dontSave) { if (tpb < result.wpm) { //new pb for that tag DB.saveLocalTagPB( tag._id, Config.mode, result.mode2, Config.punctuation, Config.language, Config.difficulty, Config.lazyMode, result.wpm, result.acc, result.rawWpm, result.consistency ); $( `#result .stats .tags .bottom div[tagid="${tag._id}"] .fas` ).removeClass("hidden"); $(`#result .stats .tags .bottom div[tagid="${tag._id}"]`).attr( "aria-label", "+" + Misc.roundTo2(result.wpm - tpb) ); // console.log("new pb for tag " + tag.name); } else { let themecolors = await ThemeColors.get(); ChartController.result.options.annotation.annotations.push({ enabled: false, type: "line", mode: "horizontal", scaleID: "wpm", value: Config.alwaysShowCPM ? tpb * 5 : tpb, borderColor: themecolors["sub"], borderWidth: 1, borderDash: [2, 2], label: { backgroundColor: themecolors["sub"], fontFamily: Config.fontFamily.replace(/_/g, " "), fontSize: 11, fontStyle: "normal", fontColor: themecolors["bg"], xPadding: 6, yPadding: 6, cornerRadius: 3, position: annotationSide, xAdjust: labelAdjust, enabled: true, content: `${tag.name} PB: ${Misc.roundTo2( Config.alwaysShowCPM ? tpb * 5 : tpb ).toFixed(2)}`, }, }); if (annotationSide === "left") { annotationSide = "right"; labelAdjust = -15; } else { annotationSide = "left"; labelAdjust = 15; } } } }); } function updateTestType() { let testType = ""; if (Config.mode === "quote") { let qlen = ""; if (Config.quoteLength === 0) { qlen = "short "; } else if (Config.quoteLength === 1) { qlen = "medium "; } else if (Config.quoteLength === 2) { qlen = "long "; } else if (Config.quoteLength === 3) { qlen = "thicc "; } testType += qlen + Config.mode; } else { testType += Config.mode; } if (Config.mode == "time") { testType += " " + Config.time; } else if (Config.mode == "words") { testType += " " + Config.words; } if ( Config.mode != "custom" && Config.funbox !== "gibberish" && Config.funbox !== "ascii" && Config.funbox !== "58008" ) { testType += "
" + result.language.replace(/_/g, " "); } if (Config.punctuation) { testType += "
punctuation"; } if (Config.numbers) { testType += "
numbers"; } if (Config.blindMode) { testType += "
blind"; } if (Config.lazyMode) { testType += "
lazy"; } if (Config.funbox !== "none") { testType += "
" + Config.funbox.replace(/_/g, " "); } if (Config.difficulty == "expert") { testType += "
expert"; } else if (Config.difficulty == "master") { testType += "
master"; } $("#result .stats .testType .bottom").html(testType); } function updateOther( difficultyFailed, failReason, afkDetected, isRepeated, tooShort ) { let otherText = ""; if (difficultyFailed) { otherText += `
failed (${failReason})`; } if (afkDetected) { otherText += "
afk detected"; } if (TestStats.invalid) { otherText += "
invalid"; let extra = ""; if (result.wpm < 0 || result.wpm > 350) { extra += "wpm"; } if (result.acc < 75 || result.acc > 100) { if (extra.length > 0) { extra += ", "; } extra += "accuracy"; } if (extra.length > 0) { otherText += ` (${extra})`; } } if (isRepeated) { otherText += "
repeated"; } if (result.bailedOut) { otherText += "
bailed out"; } if (tooShort) { otherText += "
too short"; } if (otherText == "") { $("#result .stats .info").addClass("hidden"); } else { $("#result .stats .info").removeClass("hidden"); otherText = otherText.substring(4); $("#result .stats .info .bottom").html(otherText); } } export function updateRateQuote(randomQuote) { if (Config.mode === "quote") { let userqr = DB.getSnapshot().quoteRatings?.[randomQuote.language]?.[ randomQuote.id ]; if (userqr) { $(".pageTest #result #rateQuoteButton .icon") .removeClass("far") .addClass("fas"); } RateQuotePopup.getQuoteStats(randomQuote).then((quoteStats) => { if (quoteStats !== null) { $(".pageTest #result #rateQuoteButton .rating").text( quoteStats.average ); } $(".pageTest #result #rateQuoteButton") .css({ opacity: 0 }) .removeClass("hidden") .css({ opacity: 1 }); }); } } function updateQuoteSource(randomQuote) { if (Config.mode === "quote") { $("#result .stats .source").removeClass("hidden"); $("#result .stats .source .bottom").html(randomQuote.source); } else { $("#result .stats .source").addClass("hidden"); } } export function update( res, difficultyFailed, failReason, afkDetected, isRepeated, tooShort, randomQuote, dontSave ) { result = res; $("#result #resultWordsHistory").addClass("hidden"); $("#retrySavingResultButton").addClass("hidden"); $(".pageTest #result #rateQuoteButton .icon") .removeClass("fas") .addClass("far"); $(".pageTest #result #rateQuoteButton .rating").text(""); $(".pageTest #result #rateQuoteButton").addClass("hidden"); $("#testModesNotice").css("opacity", 0); $("#words").removeClass("blurred"); $("#wordsInput").blur(); $("#result .stats .time .bottom .afk").text(""); if (firebase.auth().currentUser != null) { $("#result .loginTip").addClass("hidden"); } else { $("#result .loginTip").removeClass("hidden"); } updateWpmAndAcc(); updateConsistency(); updateTime(); updateKey(); updateTestType(); updateQuoteSource(randomQuote); updateGraph(); updateGraphPBLine(); updateTags(dontSave); updateOther(difficultyFailed, failReason, afkDetected, isRepeated, tooShort); if ( $("#result .stats .tags").hasClass("hidden") && $("#result .stats .info").hasClass("hidden") ) { $("#result .stats .infoAndTags").addClass("hidden"); } else { $("#result .stats .infoAndTags").removeClass("hidden"); } if (TestLogic.glarsesMode) { $("#middle #result .noStressMessage").remove(); $("#middle #result").prepend(`
`); $("#middle #result .stats").addClass("hidden"); $("#middle #result .chart").addClass("hidden"); $("#middle #result #resultWordsHistory").addClass("hidden"); $("#middle #result #resultReplay").addClass("hidden"); $("#middle #result .loginTip").addClass("hidden"); $("#middle #result #showWordHistoryButton").addClass("hidden"); $("#middle #result #watchReplayButton").addClass("hidden"); $("#middle #result #saveScreenshotButton").addClass("hidden"); console.log( `Test Completed: ${result.wpm} wpm ${result.acc}% acc ${result.rawWpm} raw ${result.consistency}% consistency` ); } else { $("#middle #result .stats").removeClass("hidden"); $("#middle #result .chart").removeClass("hidden"); // $("#middle #result #resultWordsHistory").removeClass("hidden"); if (firebase.auth().currentUser == null) { $("#middle #result .loginTip").removeClass("hidden"); } $("#middle #result #showWordHistoryButton").removeClass("hidden"); $("#middle #result #watchReplayButton").removeClass("hidden"); $("#middle #result #saveScreenshotButton").removeClass("hidden"); } if (window.scrollY > 0) $([document.documentElement, document.body]) .stop() .animate({ scrollTop: 0 }, 250); UI.swapElements( $("#typingTest"), $("#result"), 250, () => { TestUI.setResultCalculating(false); $("#words").empty(); ChartController.result.resize(); if (Config.alwaysShowWordsHistory && Config.burstHeatmap) { TestUI.applyBurstHeatmap(); } $("#result").focus(); window.scrollTo({ top: 0 }); $("#testModesNotice").addClass("hidden"); }, () => { $("#resultExtraButtons").removeClass("hidden").css("opacity", 0).animate( { opacity: 1, }, 125 ); if (Config.alwaysShowWordsHistory && !TestLogic.glarsesMode) { TestUI.toggleResultWords(); } Keymap.hide(); } ); } ==> ./monkeytype/src/js/test/test-config.js <== import * as CustomWordAmountPopup from "./custom-word-amount-popup"; import * as CustomTestDurationPopup from "./custom-test-duration-popup"; import * as UpdateConfig from "./config"; import * as ManualRestart from "./manual-restart-tracker"; import * as TestLogic from "./test-logic"; import * as QuoteSearchPopup from "./quote-search-popup"; import * as CustomTextPopup from "./custom-text-popup"; import * as UI from "./ui"; // export function show() { // $("#top .config").removeClass("hidden").css("opacity", 1); // } // export function hide() { // $("#top .config").css("opacity", 0).addClass("hidden"); // } export function show() { $("#top .config") .stop(true, true) .removeClass("hidden") .css("opacity", 0) .animate( { opacity: 1, }, 125 ); } export function hide() { $("#top .config") .stop(true, true) .css("opacity", 1) .animate( { opacity: 0, }, 125, () => { $("#top .config").addClass("hidden"); } ); } export function update(previous, current) { if (previous == current) return; $("#top .config .mode .text-button").removeClass("active"); $("#top .config .mode .text-button[mode='" + current + "']").addClass( "active" ); if (current == "time") { // $("#top .config .wordCount").addClass("hidden"); // $("#top .config .time").removeClass("hidden"); // $("#top .config .customText").addClass("hidden"); $("#top .config .punctuationMode").removeClass("disabled"); $("#top .config .numbersMode").removeClass("disabled"); // $("#top .config .puncAndNum").removeClass("disabled"); // $("#top .config .punctuationMode").removeClass("hidden"); // $("#top .config .numbersMode").removeClass("hidden"); // $("#top .config .quoteLength").addClass("hidden"); } else if (current == "words") { // $("#top .config .wordCount").removeClass("hidden"); // $("#top .config .time").addClass("hidden"); // $("#top .config .customText").addClass("hidden"); $("#top .config .punctuationMode").removeClass("disabled"); $("#top .config .numbersMode").removeClass("disabled"); // $("#top .config .puncAndNum").removeClass("disabled"); // $("#top .config .punctuationMode").removeClass("hidden"); // $("#top .config .numbersMode").removeClass("hidden"); // $("#top .config .quoteLength").addClass("hidden"); } else if (current == "custom") { // $("#top .config .wordCount").addClass("hidden"); // $("#top .config .time").addClass("hidden"); // $("#top .config .customText").removeClass("hidden"); $("#top .config .punctuationMode").removeClass("disabled"); $("#top .config .numbersMode").removeClass("disabled"); // $("#top .config .puncAndNum").removeClass("disabled"); // $("#top .config .punctuationMode").removeClass("hidden"); // $("#top .config .numbersMode").removeClass("hidden"); // $("#top .config .quoteLength").addClass("hidden"); } else if (current == "quote") { // $("#top .config .wordCount").addClass("hidden"); // $("#top .config .time").addClass("hidden"); // $("#top .config .customText").addClass("hidden"); $("#top .config .punctuationMode").addClass("disabled"); $("#top .config .numbersMode").addClass("disabled"); // $("#top .config .puncAndNum").addClass("disabled"); // $("#top .config .punctuationMode").removeClass("hidden"); // $("#top .config .numbersMode").removeClass("hidden"); // $("#result .stats .source").removeClass("hidden"); // $("#top .config .quoteLength").removeClass("hidden"); } else if (current == "zen") { // $("#top .config .wordCount").addClass("hidden"); // $("#top .config .time").addClass("hidden"); // $("#top .config .customText").addClass("hidden"); // $("#top .config .punctuationMode").addClass("hidden"); // $("#top .config .numbersMode").addClass("hidden"); // $("#top .config .quoteLength").addClass("hidden"); } let submenu = { time: "time", words: "wordCount", custom: "customText", quote: "quoteLength", zen: "", }; let animTime = 250; if (current == "zen") { $(`#top .config .${submenu[previous]}`).animate( { opacity: 0, }, animTime / 2, () => { $(`#top .config .${submenu[previous]}`).addClass("hidden"); } ); $(`#top .config .puncAndNum`).animate( { opacity: 0, }, animTime / 2, () => { $(`#top .config .puncAndNum`).addClass("invisible"); } ); return; } if (previous == "zen") { setTimeout(() => { $(`#top .config .${submenu[current]}`).removeClass("hidden"); $(`#top .config .${submenu[current]}`) .css({ opacity: 0 }) .animate( { opacity: 1, }, animTime / 2 ); $(`#top .config .puncAndNum`).removeClass("invisible"); $(`#top .config .puncAndNum`) .css({ opacity: 0 }) .animate( { opacity: 1, }, animTime / 2 ); }, animTime / 2); return; } UI.swapElements( $("#top .config ." + submenu[previous]), $("#top .config ." + submenu[current]), animTime ); } $(document).on("click", "#top .config .wordCount .text-button", (e) => { const wrd = $(e.currentTarget).attr("wordCount"); if (wrd == "custom") { CustomWordAmountPopup.show(); } else { UpdateConfig.setWordCount(wrd); ManualRestart.set(); TestLogic.restart(); } }); $(document).on("click", "#top .config .time .text-button", (e) => { let mode = $(e.currentTarget).attr("timeConfig"); if (mode == "custom") { CustomTestDurationPopup.show(); } else { UpdateConfig.setTimeConfig(mode); ManualRestart.set(); TestLogic.restart(); } }); $(document).on("click", "#top .config .quoteLength .text-button", (e) => { let len = $(e.currentTarget).attr("quoteLength"); if (len == -2) { // UpdateConfig.setQuoteLength(-2, false, e.shiftKey); QuoteSearchPopup.show(); } else { if (len == -1) { len = [0, 1, 2, 3]; } UpdateConfig.setQuoteLength(len, false, e.shiftKey); ManualRestart.set(); TestLogic.restart(); } }); $(document).on("click", "#top .config .customText .text-button", () => { CustomTextPopup.show(); }); $(document).on("click", "#top .config .punctuationMode .text-button", () => { UpdateConfig.togglePunctuation(); ManualRestart.set(); TestLogic.restart(); }); $(document).on("click", "#top .config .numbersMode .text-button", () => { UpdateConfig.toggleNumbers(); ManualRestart.set(); TestLogic.restart(); }); $(document).on("click", "#top .config .mode .text-button", (e) => { if ($(e.currentTarget).hasClass("active")) return; const mode = $(e.currentTarget).attr("mode"); UpdateConfig.setMode(mode); ManualRestart.set(); TestLogic.restart(); }); ==> ./monkeytype/src/js/test/practise-words.js <== import * as TestStats from "./test-stats"; import * as Notifications from "./notifications"; import Config, * as UpdateConfig from "./config"; import * as CustomText from "./custom-text"; import * as TestLogic from "./test-logic"; export let before = { mode: null, punctuation: null, numbers: null, }; export function init(missed, slow) { if (Config.mode === "zen") return; let limit; if ((missed && !slow) || (!missed && slow)) { limit = 20; } else if (missed && slow) { limit = 10; } let sortableMissedWords = []; if (missed) { Object.keys(TestStats.missedWords).forEach((missedWord) => { sortableMissedWords.push([missedWord, TestStats.missedWords[missedWord]]); }); sortableMissedWords.sort((a, b) => { return b[1] - a[1]; }); sortableMissedWords = sortableMissedWords.slice(0, limit); } if (missed && !slow && sortableMissedWords.length == 0) { Notifications.add("You haven't missed any words", 0); return; } let sortableSlowWords = []; if (slow) { sortableSlowWords = TestLogic.words.get().map(function (e, i) { return [e, TestStats.burstHistory[i]]; }); sortableSlowWords.sort((a, b) => { return a[1] - b[1]; }); sortableSlowWords = sortableSlowWords.slice( 0, Math.min(limit, Math.round(TestLogic.words.length * 0.2)) ); } // console.log(sortableMissedWords); // console.log(sortableSlowWords); if (sortableMissedWords.length == 0 && sortableSlowWords.length == 0) { Notifications.add("Could not start a new custom test", 0); return; } let newCustomText = []; sortableMissedWords.forEach((missed, index) => { for (let i = 0; i < missed[1]; i++) { newCustomText.push(missed[0]); } }); sortableSlowWords.forEach((slow, index) => { for (let i = 0; i < sortableSlowWords.length - index; i++) { newCustomText.push(slow[0]); } }); // console.log(newCustomText); let mode = before.mode === null ? Config.mode : before.mode; let punctuation = before.punctuation === null ? Config.punctuation : before.punctuation; let numbers = before.numbers === null ? Config.numbers : before.numbers; UpdateConfig.setMode("custom"); CustomText.setText(newCustomText); CustomText.setIsWordRandom(true); CustomText.setWord( (sortableSlowWords.length + sortableMissedWords.length) * 5 ); TestLogic.restart(false, false, false, true); before.mode = mode; before.punctuation = punctuation; before.numbers = numbers; } export function resetBefore() { before.mode = null; before.punctuation = null; before.numbers = null; } export function showPopup(focus = false) { if ($("#practiseWordsPopupWrapper").hasClass("hidden")) { if (Config.mode === "zen") { Notifications.add("Practice words is unsupported in zen mode", 0); return; } $("#practiseWordsPopupWrapper") .stop(true, true) .css("opacity", 0) .removeClass("hidden") .animate({ opacity: 1 }, 100, () => { if (focus) { console.log("focusing"); $("#practiseWordsPopup .missed").focus(); } }); } } function hidePopup() { if (!$("#practiseWordsPopupWrapper").hasClass("hidden")) { $("#practiseWordsPopupWrapper") .stop(true, true) .css("opacity", 1) .animate( { opacity: 0, }, 100, (e) => { $("#practiseWordsPopupWrapper").addClass("hidden"); } ); } } $("#practiseWordsPopupWrapper").click((e) => { if ($(e.target).attr("id") === "practiseWordsPopupWrapper") { hidePopup(); } }); $("#practiseWordsPopup .button.missed").click(() => { hidePopup(); init(true, false); }); $("#practiseWordsPopup .button.slow").click(() => { hidePopup(); init(false, true); }); $("#practiseWordsPopup .button.both").click(() => { hidePopup(); init(true, true); }); $("#practiseWordsPopup .button").keypress((e) => { if (e.key == "Enter") { $(e.currentTarget).click(); } }); $("#practiseWordsPopup .button.both").on("focusout", (e) => { e.preventDefault(); $("#practiseWordsPopup .missed").focus(); }); ==> ./monkeytype/src/js/test/test-stats.js <== import * as TestLogic from "./test-logic"; import Config from "./config"; import * as Misc from "./misc"; import * as TestStats from "./test-stats"; export let invalid = false; export let start, end; export let start2, end2; export let wpmHistory = []; export let rawHistory = []; export let burstHistory = []; export let keypressPerSecond = []; export let currentKeypress = { count: 0, errors: 0, words: [], afk: true, }; export let lastKeypress; export let currentBurstStart = 0; // export let errorsPerSecond = []; // export let currentError = { // count: 0, // words: [], // }; export let lastSecondNotRound = false; export let missedWords = {}; export let accuracy = { correct: 0, incorrect: 0, }; export let keypressTimings = { spacing: { current: -1, array: [], }, duration: { current: -1, array: [], }, }; export function getStats() { let ret = { start, end, wpmHistory, rawHistory, burstHistory, keypressPerSecond, currentKeypress, lastKeypress, currentBurstStart, lastSecondNotRound, missedWords, accuracy, keypressTimings, }; try { ret.keySpacingStats = { average: keypressTimings.spacing.array.reduce( (previous, current) => (current += previous) ) / keypressTimings.spacing.array.length, sd: Misc.stdDev(keypressTimings.spacing.array), }; } catch (e) { // } try { ret.keyDurationStats = { average: keypressTimings.duration.array.reduce( (previous, current) => (current += previous) ) / keypressTimings.duration.array.length, sd: Misc.stdDev(keypressTimings.duration.array), }; } catch (e) { // } return ret; } export function restart() { start = 0; end = 0; invalid = false; wpmHistory = []; rawHistory = []; burstHistory = []; keypressPerSecond = []; currentKeypress = { count: 0, errors: 0, words: [], afk: true, }; currentBurstStart = 0; // errorsPerSecond = []; // currentError = { // count: 0, // words: [], // }; lastSecondNotRound = false; missedWords = {}; accuracy = { correct: 0, incorrect: 0, }; keypressTimings = { spacing: { current: -1, array: [], }, duration: { current: -1, array: [], }, }; } export let restartCount = 0; export let incompleteSeconds = 0; export function incrementRestartCount() { restartCount++; } export function incrementIncompleteSeconds(val) { incompleteSeconds += val; } export function resetIncomplete() { restartCount = 0; incompleteSeconds = 0; } export function setInvalid() { invalid = true; } export function calculateTestSeconds(now) { if (now === undefined) { let endAfkSeconds = (end - lastKeypress) / 1000; if ((Config.mode == "zen" || TestLogic.bailout) && endAfkSeconds < 7) { return (lastKeypress - start) / 1000; } else { return (end - start) / 1000; } } else { return (now - start) / 1000; } } export function setEnd(e) { end = e; end2 = Date.now(); } export function setStart(s) { start = s; start2 = Date.now(); } export function updateLastKeypress() { lastKeypress = performance.now(); } export function pushToWpmHistory(word) { wpmHistory.push(word); } export function pushToRawHistory(word) { rawHistory.push(word); } export function incrementKeypressCount() { currentKeypress.count++; } export function setKeypressNotAfk() { currentKeypress.afk = false; } export function incrementKeypressErrors() { currentKeypress.errors++; } export function pushKeypressWord(word) { currentKeypress.words.push(word); } export function pushKeypressesToHistory() { keypressPerSecond.push(currentKeypress); currentKeypress = { count: 0, errors: 0, words: [], afk: true, }; } export function calculateAfkSeconds(testSeconds) { let extraAfk = 0; if (testSeconds !== undefined) { if (Config.mode === "time") { extraAfk = Math.round(testSeconds) - keypressPerSecond.length; } else { extraAfk = Math.ceil(testSeconds) - keypressPerSecond.length; } if (extraAfk < 0) extraAfk = 0; // console.log("-- extra afk debug"); // console.log("should be " + Math.ceil(testSeconds)); // console.log(keypressPerSecond.length); // console.log( // `gonna add extra ${extraAfk} seconds of afk because of no keypress data` // ); } let ret = keypressPerSecond.filter((x) => x.afk).length; return ret + extraAfk; } export function setLastSecondNotRound() { lastSecondNotRound = true; } export function setBurstStart(time) { currentBurstStart = time; } export function calculateBurst() { let timeToWrite = (performance.now() - currentBurstStart) / 1000; let wordLength; if (Config.mode === "zen") { wordLength = TestLogic.input.current.length; if (wordLength == 0) { wordLength = TestLogic.input.getHistoryLast().length; } } else { wordLength = TestLogic.words.getCurrent().length; } let speed = Misc.roundTo2((wordLength * (60 / timeToWrite)) / 5); return Math.round(speed); } export function pushBurstToHistory(speed) { if (burstHistory[TestLogic.words.currentIndex] === undefined) { burstHistory.push(speed); } else { //repeated word - override burstHistory[TestLogic.words.currentIndex] = speed; } } export function calculateAccuracy() { let acc = (accuracy.correct / (accuracy.correct + accuracy.incorrect)) * 100; return isNaN(acc) ? 100 : acc; } export function incrementAccuracy(correctincorrect) { if (correctincorrect) { accuracy.correct++; } else { accuracy.incorrect++; } } export function setKeypressTimingsTooLong() { keypressTimings.spacing.array = "toolong"; keypressTimings.duration.array = "toolong"; } export function pushKeypressDuration(val) { keypressTimings.duration.array.push(val); } export function setKeypressDuration(val) { keypressTimings.duration.current = val; } export function pushKeypressSpacing(val) { keypressTimings.spacing.array.push(val); } export function setKeypressSpacing(val) { keypressTimings.spacing.current = val; } export function recordKeypressSpacing() { let now = performance.now(); let diff = Math.abs(keypressTimings.spacing.current - now); if (keypressTimings.spacing.current !== -1) { pushKeypressSpacing(diff); } setKeypressSpacing(now); } export function resetKeypressTimings() { keypressTimings = { spacing: { current: performance.now(), array: [], }, duration: { current: performance.now(), array: [], }, }; } export function pushMissedWord(word) { if (!Object.keys(missedWords).includes(word)) { missedWords[word] = 1; } else { missedWords[word]++; } } export function removeAfkData() { let testSeconds = calculateTestSeconds(); keypressPerSecond.splice(testSeconds); keypressTimings.duration.array.splice(testSeconds); keypressTimings.spacing.array.splice(testSeconds); wpmHistory.splice(testSeconds); } function countChars() { let correctWordChars = 0; let correctChars = 0; let incorrectChars = 0; let extraChars = 0; let missedChars = 0; let spaces = 0; let correctspaces = 0; for (let i = 0; i < TestLogic.input.history.length; i++) { let word = Config.mode == "zen" ? TestLogic.input.getHistory(i) : TestLogic.words.get(i); if (TestLogic.input.getHistory(i) === "") { //last word that was not started continue; } if (TestLogic.input.getHistory(i) == word) { //the word is correct correctWordChars += word.length; correctChars += word.length; if ( i < TestLogic.input.history.length - 1 && Misc.getLastChar(TestLogic.input.getHistory(i)) !== "\n" ) { correctspaces++; } } else if (TestLogic.input.getHistory(i).length >= word.length) { //too many chars for (let c = 0; c < TestLogic.input.getHistory(i).length; c++) { if (c < word.length) { //on char that still has a word list pair if (TestLogic.input.getHistory(i)[c] == word[c]) { correctChars++; } else { incorrectChars++; } } else { //on char that is extra extraChars++; } } } else { //not enough chars let toAdd = { correct: 0, incorrect: 0, missed: 0, }; for (let c = 0; c < word.length; c++) { if (c < TestLogic.input.getHistory(i).length) { //on char that still has a word list pair if (TestLogic.input.getHistory(i)[c] == word[c]) { toAdd.correct++; } else { toAdd.incorrect++; } } else { //on char that is extra toAdd.missed++; } } correctChars += toAdd.correct; incorrectChars += toAdd.incorrect; if (i === TestLogic.input.history.length - 1 && Config.mode == "time") { //last word - check if it was all correct - add to correct word chars if (toAdd.incorrect === 0) correctWordChars += toAdd.correct; } else { missedChars += toAdd.missed; } } if (i < TestLogic.input.history.length - 1) { spaces++; } } if (Config.funbox === "nospace" || Config.funbox === "arrows") { spaces = 0; correctspaces = 0; } return { spaces: spaces, correctWordChars: correctWordChars, allCorrectChars: correctChars, incorrectChars: Config.mode == "zen" ? TestStats.accuracy.incorrect : incorrectChars, extraChars: extraChars, missedChars: missedChars, correctSpaces: correctspaces, }; } export function calculateStats() { let testSeconds = TestStats.calculateTestSeconds(); console.log((TestStats.end2 - TestStats.start2) / 1000); console.log(testSeconds); if (Config.mode != "custom") { testSeconds = Misc.roundTo2(testSeconds); } let chars = countChars(); let wpm = Misc.roundTo2( ((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5 ); let wpmraw = Misc.roundTo2( ((chars.allCorrectChars + chars.spaces + chars.incorrectChars + chars.extraChars) * (60 / testSeconds)) / 5 ); let acc = Misc.roundTo2(TestStats.calculateAccuracy()); return { wpm: isNaN(wpm) ? 0 : wpm, wpmRaw: isNaN(wpmraw) ? 0 : wpmraw, acc: acc, correctChars: chars.correctWordChars, incorrectChars: chars.incorrectChars, missedChars: chars.missedChars, extraChars: chars.extraChars, allChars: chars.allCorrectChars + chars.spaces + chars.incorrectChars + chars.extraChars, time: testSeconds, spaces: chars.spaces, correctSpaces: chars.correctSpaces, }; }