==> ./monkeytype/README.md <== [](https://monkeytype.com/) <br /> <img align="left" alt="JavaScript" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/javascript/javascript.png" /> <img align="left" alt="HTML5" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/html/html.png" /> <img align="left" alt="CSS3" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/css/css.png" /> <img align="left" alt="CSS3" width="26px" src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/sass/sass.png" /> <br /> # 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 <== <!DOCTYPE html> <html lang="en"> <head> <script> document.addEventListener("keydown", function (e) { if (e.key == "Escape") e.preventDefault(); }); </script> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Monkeytype</title> <!-- <link rel="stylesheet" href="css/fa.css" /> --> <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" crossorigin="anonymous" /> <link rel="stylesheet" href="css/balloon.css" /> <link rel="stylesheet" href="css/style.css?v=1" /> <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" /> <link rel="stylesheet" href="" id="funBoxTheme" /> <link id="favicon" rel="shortcut icon" href="/images/favicon/favicon.ico" /> <link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-touch-icon.png" /> <link rel="mask-icon" href="/images/favicon/safari-pinned-tab.svg" color="#eee" /> <meta name="msapplication-TileColor" content="#e2b714" /> <meta name="msapplication-config" content="/images/favicon/browserconfig.xml" /> <meta id="metaThemeColor" name="theme-color" content="#e2b714" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" onerror="this.onerror=null;this.href='css/fa.min.css';" /> <link rel="manifest" href="manifest.json" /> <meta name="name" content="Monkeytype" /> <meta name="image" content="https://monkeytype.com/images/mtsocial.png" /> <meta name="description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta name="keywords" content="typing speed test, typing speedtest, typing test, speetest, speed test, typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test" /> <meta name="author" content="Miodec" /> <meta property="og:title" content="Monkeytype" /> <meta property="og:url" content="https://monkeytype.com/" /> <meta property="og:type" content="website" /> <meta property="og:image" content="https://monkeytype.com/images/mtsocial.png" /> <meta property="og:description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta name="twitter:title" content="Monkeytype" /> <meta name="twitter:image" content="https://monkeytype.com/images/mtsocial.png" /> <meta name="twitter:card" content="summary_large_image" /> <!-- <script type="text/javascript"> window["nitroAds"] = window["nitroAds"] || { createAd: function () { window.nitroAds.queue.push(["createAd", arguments]); }, queue: [], }; </script> --> <!-- <script async src="https://s.nitropay.com/ads-693.js"></script> --> <script src="https://hb.vntsm.com/v3/live/ad-manager.min.js" type="text/javascript" data-site-id="60b78af12119122b8958910f" data-mode="scan" id="adScript" async ></script> <script> if ("serviceWorker" in navigator) { navigator.serviceWorker.register("sw.js"); } </script> </head> <div class="customBackground"></div> <body> <noscript> <div style=" background: #333; width: 100vw; height: 100vh; left: 0; top: 0; position: fixed; display: grid; justify-content: center; align-content: center; color: #646669; font-size: 1rem; font-family: Arial Rounded MT Bold; z-index: 99999; padding: 6rem; box-sizing: border-box; " > <div style=" display: grid; justify-content: center; align-content: center; max-width: 800px; gap: 1rem; " > <span style="font-size: 6rem; color: #e2b714">:(</span> <span style="font-size: 2rem; color: #d1d0c5"> Looks like JavaScript is disabled. Please enable JavaScript in order to use this website. </span> </div> </div> </noscript> <div id="nocss" style=" background: #333; width: 100vw; height: 100vh; left: 0; top: 0; position: fixed; display: grid; justify-content: center; align-content: center; color: #646669; font-size: 1rem; font-family: Arial Rounded MT Bold; z-index: 99999; padding: 6rem; box-sizing: border-box; " > <div style=" display: grid; justify-content: center; align-content: center; max-width: 800px; gap: 1rem; " > <span style="font-size: 6rem; color: #e2b714">:(</span> <span style="font-size: 2rem; color: #d1d0c5"> It seems like the CSS failed to load. Please clear your cache to redownload the styles. If that doesn't help contact support. <br /> (jack@monkeytype.com or discord.gg/monkeytype) </span> <span>(ctrl/cmd + shift + r on Chromium browsers)</span> <span> 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". </span> </div> </div> <div id="ad_rich_media" class="hidden"></div> <div id="backgroundLoader" style="display: none"></div> <div id="bannerCenter"></div> <div id="notificationCenter"> <div class="history"></div> </div> <div class="nameChangeMessage" style=" background-color: var(--main-color); padding: 0.5rem; text-align: center; color: var(--bg-color); display: none; " > Important information about your account. Please click this message. </div> <div id="simplePopupWrapper" class="popupWrapper hidden"> <div id="simplePopup" popupId=""></div> </div> <div id="mobileTestConfigPopupWrapper" class="popupWrapper hidden"> <div id="mobileTestConfigPopup"> <div class="group"> <div class="button punctuation">punctuation</div> <div class="button numbers">numbers</div> </div> <div class="spacer"></div> <div class="group modeGroup"> <div class="button" mode="time">time</div> <div class="button active" mode="words">words</div> <div class="button" mode="quote">quote</div> <div class="button" mode="zen">zen</div> <div class="button" mode="custom">custom</div> </div> <div class="spacer"></div> <div class="group timeGroup"> <div class="button" time="15">15</div> <div class="button active" time="30">30</div> <div class="button" time="60">60</div> <div class="button" time="120">120</div> <div class="button" time="custom">custom</div> </div> <div class="group wordsGroup hidden"> <div class="button" words="10">10</div> <div class="button" words="25">25</div> <div class="button" words="50">50</div> <div class="button" words="100">100</div> <div class="button" words="custom">custom</div> </div> <div class="group quoteGroup hidden"> <div class="button" quote="-1">all</div> <div class="button" quote="0">short</div> <div class="button" quote="1">medium</div> <div class="button" quote="2">long</div> <div class="button" quote="3">thicc</div> <div class="button" quote="-2">search</div> </div> <div class="group customGroup hidden"> <div class="button customChange">change</div> </div> </div> </div> <div id="pbTablesPopupWrapper" class="popupWrapper hidden"> <div id="pbTablesPopup"> <!-- <div class="title">All words personal bests</div> --> <table> <thead> <tr> <td width="1%">words</td> <td> wpm <br /> <span class="sub">accuracy</span> </td> <td> raw <br /> <span class="sub">consistency</span> </td> <td>difficulty</td> <td>language</td> <td>punctuation</td> <td>lazy mode</td> <td>date</td> </tr> </thead> <tbody></tbody> </table> </div> </div> <div id="practiseWordsPopupWrapper" class="popupWrapper hidden"> <div id="practiseWordsPopup" action=""> <div class="title">Practice words</div> <div class="text"> This will start a new test in custom mode. Words that you mistyped more often or words that you typed much slower will be weighted higher and appear more often. </div> <div class="button missed" tabindex="1">Practice missed</div> <div class="button slow" tabindex="1">Practice slow</div> <div class="button both" tabindex="1">Practice both</div> </div> </div> <div id="rateQuotePopupWrapper" class="popupWrapper hidden"> <div id="rateQuotePopup" quoteId=""> <div class="quote"> <div class="text">-</div> <div class="id"> <div class="top">id</div> <div class="val">-</div> </div> <div class="length"> <div class="top">length</div> <div class="val">-</div> </div> <div class="source"> <div class="top">source</div> <div class="val">-</div> </div> </div> <div class="spacer"></div> <div class="ratingStats"> <div class="ratingCount"> <div class="top">ratings</div> <div class="val">-</div> </div> <div class="ratingAverage"> <div class="top">average</div> <div class="val">-</div> </div> <div class="starsWrapper"> <div class="top">your rating</div> <div class="stars"> <div class="star active" rating="1"> <i class="fas fa-fw fa-star"></i> </div> <div class="star active" rating="2"> <i class="fas fa-fw fa-star"></i> </div> <div class="star active" rating="3"> <i class="fas fa-fw fa-star"></i> </div> <div class="star" rating="4"> <i class="fas fa-fw fa-star"></i> </div> <div class="star" rating="5"> <i class="fas fa-fw fa-star"></i> </div> </div> </div> </div> <!-- <div class="button rate1"> <i class="fas fa-star"></i> <i class="far fa-star"></i> <i class="far fa-star"></i> </div> <div class="button rate2"> <i class="fas fa-star"></i> <i class="fas fa-star"></i> <i class="far fa-star"></i> </div> <div class="button rate3"> <i class="fas fa-star"></i> <i class="fas fa-star"></i> <i class="fas fa-star"></i> </div> --> <span aria-label="Submit review" data-balloon-pos="up"> <div class="icon-button submitButton"> <i class="fas fa-chevron-right"></i> </div> </span> </div> </div> <div id="settingsImportWrapper" class="popupWrapper hidden"> <div id="settingsImport" action=""> <input type="text" /> <div class="button">import settings</div> </div> </div> <div id="customThemeShareWrapper" class="popupWrapper hidden"> <div id="customThemeShare" action=""> <input type="text" /> <div class="button">ok</div> </div> </div> <div id="customTextPopupWrapper" class="popupWrapper hidden"> <div id="customTextPopup" action=""> <div class="wordfilter button"> <i class="fas fa-filter"></i> Words filter </div> <textarea class="textarea" placeholder="Custom text"></textarea> <div class="inputs"> <label class="checkbox"> <input type="checkbox" /> <div class="customTextCheckbox"></div> Random <span> randomize the above words, and control how many words are generated. </span> </label> <div class="randomInputFields hidden"> <label class="wordcount"> Word count <input type="number" value="" min="1" max="10000" /> </label> <div style="color: var(--sub-color)">or</div> <label class="time"> Time <input type="number" value="" min="1" max="10000" /> </label> </div> <label class="checkbox typographyCheck"> <input type="checkbox" checked /> <div class="customTextCheckbox customTextTypographyCheckbox"></div> Remove Fancy Typography <span> Standardises typography symbols (for example “ and †become ") </span> </label> <label class="checkbox delimiterCheck"> <input type="checkbox" /> <div class="customTextCheckbox customTextDelimiterCheckbox"></div> Pipe Delimiter <span> Change how words are separated. Using the pipe delimiter allows you to randomize groups of words. </span> </label> </div> <div class="button apply">ok</div> </div> </div> <div id="wordFilterPopupWrapper" class="popupWrapper hidden"> <div id="wordFilterPopup"> <div class="group"> <div class="title">language</div> <select class="languageInput" class=""></select> </div> <div class="group lengthgrid"> <div class="title">min length</div> <div class="title">max length</div> <input class="wordLength wordMinInput" autocomplete="off" type="number" /> <input class="wordLength wordMaxInput" autocomplete="off" type="number" /> </div> <div class="group"> <div class="title">include</div> <input class="wordIncludeInput" autocomplete="off" /> </div> <div class="group"> <div class="title">exclude</div> <input class="wordExcludeInput" autocomplete="off" /> </div> <div class="tip"> Use the above filters to include and exclude words or characters (separated by spaces) </div> <div class="tip"> "Set" replaces the current custom word list with the filter result, "Add" appends the filter result to the current custom word list </div> <i class="fas fa-fw fa-spin fa-circle-notch hidden loadingIndicator" ></i> <div class="button" id="set">set</div> <div class="button">add</div> </div> </div> <div id="customWordAmountPopupWrapper" class="popupWrapper hidden"> <div id="customWordAmountPopup"> <div class="title">Word amount</div> <input type="number" value="1" min="1" max="10000" /> <div class="tip"> You can start an infinite test by inputting 0. Then, to stop the test, use the Bail Out feature (esc or ctrl/cmd + shift + p > Bail Out) </div> <div class="button">ok</div> </div> </div> <div id="customTestDurationPopupWrapper" class="popupWrapper hidden"> <div id="customTestDurationPopup"> <div class="title">Test duration</div> <div class="preview"></div> <input value="1" /> <div class="tip"> You can start an infinite test by inputting 0. Then, to stop the test, use the Bail Out feature (esc or ctrl/cmd + shift + p > Bail Out) </div> <div class="button">ok</div> </div> </div> <div id="quoteSearchPopupWrapper" class="popupWrapper hidden"> <div id="quoteSearchPopup" mode=""> <div id="quoteSearchTop"> <div class="title">Quote Search</div> <div class="buttons"> <div id="gotoSubmitQuoteButton" class="button"> <i class="fas fa-plus"></i> Submit a quote </div> <div id="goToApproveQuotes" class="button"> <i class="fas fa-check"></i> Approve quotes </div> </div> </div> <input id="searchBox" class="searchBox" type="text" maxlength="200" autocomplete="off" /> <div id="extraResults">No search results</div> <div id="quoteSearchResults" class="quoteSearchResults"></div> </div> </div> <div id="quoteSubmitPopupWrapper" class="popupWrapper hidden"> <div id="quoteSubmitPopup" mode=""> <div class="title">Submit a Quote</div> <ul> <li> Do not include content that contains any libelous or otherwise unlawful, abusive or obscene text. </li> <li>Verify quotes added aren't duplicates of any already present</li> <li> Please do not add extremely short quotes (less than 60 characters) </li> <li> <b> Submitting low quality quotes or misusing this form will cause you to lose access to this feature </b> </li> </ul> <label>Quote</label> <div style="position: relative"> <textarea id="submitQuoteText" class="" type="text" autocomplete="off" ></textarea> <div class="characterCount">-</div> </div> <label>Source</label> <input id="submitQuoteSource" class="" type="text" autocomplete="off" /> <label>Language</label> <select name="language" id="submitQuoteLanguage"></select> <div class="g-recaptcha" data-sitekey="6Lc-V8McAAAAAJ7s6LGNe7MBZnRiwbsbiWts87aj" ></div> <div id="submitQuoteButton" class="button">Submit</div> </div> </div> <div id="quoteApprovePopupWrapper" class="popupWrapper hidden"> <div id="quoteApprovePopup" mode=""> <div class="top"> <div class="title">Approve Quotes</div> <div class="button refreshList"> <i class="fas fa-sync-alt"></i> Refresh list </div> </div> <div class="quotes"></div> </div> </div> <div id="tagsWrapper" class="popupWrapper hidden"> <div id="tagsEdit" action="" tagid=""> <div class="title"></div> <input type="text" /> <div class="button"><i class="fas fa-plus"></i></div> </div> </div> <div id="resultEditTagsPanelWrapper" class="popupWrapper hidden"> <div id="resultEditTagsPanel" resultid=""> <div class="buttons"></div> <div class="button confirmButton"><i class="fas fa-check"></i></div> </div> </div> <div id="presetWrapper" class="popupWrapper hidden"> <div id="presetEdit" action="" presetid=""> <div class="title"></div> <input type="text" class="text" /> <label class="checkbox"> <input type="checkbox" /> <div class="customTextCheckbox"></div> Change preset to current settings </label> <div class="button"><i class="fas fa-plus"></i></div> </div> </div> <div id="leaderboardsWrapper" class="hidden"> <div id="leaderboards"> <div class="leaderboardsTop"> <div class="mainTitle">Leaderboards</div> <div class="subTitle">Next update in: --:--</div> <!-- <div class="buttons"> <div class="buttonGroup"> <div class="button active" board="time_15">time 15</div> <div class="button" board="time_60">time 60</div> </div> </div> --> </div> <div class="tables"> <div class="titleAndTable"> <div class="titleAndButtons"> <div class="title"> English Time 15 <i class="hidden leftTableLoader fas fa-fw fa-spin fa-circle-notch" ></i> </div> <div class="buttons"> Jump to: <div class="button leftTableJumpToTop"> <i class="fas fa-fw fas fa-crown"></i> <!-- Top --> </div> <div class="button leftTableJumpToMe"> <i class="fas fa-fw fas fa-user"></i> <!-- Me --> </div> </div> </div> <div class="leftTableWrapper invisible"> <table class="left"> <thead> <tr> <td width="1%">#</td> <td>name</td> <td class="alignRight" width="15%"> wpm <br /> <div class="sub">accuracy</div> </td> <td class="alignRight" width="15%"> raw <br /> <div class="sub">consistency</div> </td> <td class="alignRight" width="13%">test</td> <td class="alignRight" width="22%">date</td> </tr> </thead> <tbody></tbody> <tfoot></tfoot> </table> </div> </div> <div class="titleAndTable"> <div class="titleAndButtons"> <div class="title"> English Time 60 <i class="hidden rightTableLoader fas fa-fw fa-spin fa-circle-notch" ></i> </div> <div class="buttons"> Jump to: <div class="button rightTableJumpToTop"> <i class="fas fa-fw fas fa-crown"></i> <!-- Top --> </div> <div class="button rightTableJumpToMe"> <i class="fas fa-fw fas fa-user"></i> <!-- Me --> </div> </div> </div> <div class="rightTableWrapper invisible"> <table class="right"> <thead> <tr> <td width="1%">#</td> <td>name</td> <td class="alignRight" width="15%"> wpm <br /> <div class="sub">accuracy</div> </td> <td class="alignRight" width="15%"> raw <br /> <div class="sub">consistency</div> </td> <td class="alignRight" width="13%">test</td> <td class="alignRight" width="22%">date</td> </tr> </thead> <tbody></tbody> <tfoot></tfoot> </table> </div> </div> </div> </div> </div> <div id="versionHistoryWrapper" class="hidden"> <div id="versionHistory"> <!-- <div class="tip">Click anywhere to dismiss</div> --> <div class="releases"> <div class="release"> <div class="title">v1</div> <div class="date">010101</div> <div class="body">test</div> </div> <div class="release"> <div class="title">v2</div> <div class="date">010101</div> <div class="body">test</div> </div> </div> </div> </div> <div id="supportMeWrapper" class="popupWrapper hidden"> <div id="supportMe"> <div class="title">Support Monkeytype</div> <div class="text"> Thank you so much for thinking about supporting this project. It would not be possible without you and your continued support. <i class="fas fa-heart"></i> </div> <div class="buttons"> <div class="button ads"> <div class="icon"><i class="fas fa-ad"></i></div> <div class="text">Enable Ads</div> </div> <a class="button" href="https://ko-fi.com/monkeytype" target="_blank" rel="noreferrer" > <div class="icon"><i class="fas fa-donate"></i></div> <div class="text">Donate</div> </a> <a class="button" href="https://www.patreon.com/monkeytype" target="_blank" rel="noreferrer" > <div class="icon"><i class="fab fa-patreon"></i></div> <div class="text"> Become <br /> a Patron </div> </a> <a class="button" href="https://monkeytype.store" target="_blank" rel="noreferrer" > <div class="icon"><i class="fas fa-tshirt"></i></div> <div class="text">Buy Merch</div> </a> </div> </div> </div> <div id="contactPopupWrapper" class="popupWrapper hidden"> <div id="contactPopup"> <div class="title">Contact</div> <div class="text"> Feel free to send an email to jack@monkeytype.com (the buttons below will open the default mail client). <br /> <br /> Please <span>do not send</span> requests to delete account, update email, update name or clear personal bests - you can do that in the settings page. </div> <div class="buttons"> <a class="button" target="_blank" href="mailto:jack@monkeytype.com?subject=[Question] " > <div class="icon"><i class="fas fa-question-circle"></i></div> <div class="textGroup"> <div class="text">Question</div> <!-- <div class="subtext">Confused about something?</div> --> </div> </a> <a class="button" target="_blank" href="mailto:jack@monkeytype.com?subject=[Feedback] " > <div class="icon"><i class="fas fa-comment-dots"></i></div> <div class="textGroup"> <div class="text">Feedback</div> <!-- <div class="subtext">Feel like the website could be improved?</div> --> </div> </a> <a class="button" target="_blank" href="mailto:jack@monkeytype.com?subject=[Bug] " > <div class="icon"><i class="fas fa-bug"></i></div> <div class="textGroup"> <div class="text">Bug Report</div> <!-- <div class="subtext">Report any bugs you found. You can also open a new issue on GitHub.</div> --> </div> </a> <a class="button" target="_blank" href="mailto:jack@monkeytype.com?subject=[Account] " > <div class="icon"><i class="fas fa-user-circle"></i></div> <div class="textGroup"> <div class="text">Account Help</div> <!-- <div class="subtext">Report any problems with your account like login issues.</div> --> </div> </a> <a class="button" target="_blank" href="mailto:jack@monkeytype.com?subject=[Business] " > <div class="icon"><i class="fas fa-briefcase"></i></div> <div class="textGroup"> <div class="text">Business Inquiry</div> <!-- <div class="subtext">Let's work together.</div> --> </div> </a> <a class="button" target="_blank" href="mailto:jack@monkeytype.com?subject=[Other] " > <div class="icon"><i class="fas fa-ellipsis-h"></i></div> <div class="textGroup"> <div class="text">Other</div> <!-- <div class="subtext">We will try to help.</div> --> </div> </a> </div> </div> </div> <div id="commandLineWrapper" class="hidden"> <div id="commandLine"> <div style=" display: grid; grid-template-columns: auto 1fr; align-items: center; " > <div class="searchicon"> <i class="fas fa-search"></i> </div> <input type="text" class="input" placeholder="Type to search" /> </div> <div class="separator hidden"></div> <div class="listTitle">Title</div> <div class="suggestions"></div> </div> <div id="commandInput" class="hidden"> <input type="text" class="input" placeholder="input" /> </div> </div> <div id="timerWrapper"> <div id="timer" class="timerMain"></div> </div> <div style="display: flex; justify-content: space-around"> <div id="centerContent" class="hidden"> <div id="top"> <div class="logo"> <div class="icon"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation: isolate" viewBox="-680 -1030 300 180" > <g> <path d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z" /> <path d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z " /> <path d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z" /> <path d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z " /> <path d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z " /> <path d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z" /> <path d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z " /> <path d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z " /> <path d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z " /> </g> </svg> </div> <div class="text"> <div class="top">monkey see</div> monkeytype </div> </div> <div id="menu"> <a id="startTestButton" class="icon-button view-start" tabindex="2" href="/" onclick="this.blur();" > <div class="icon"> <i class="fas fa-fw fa-keyboard"></i> </div> </a> <div class="icon-button leaderboards view-leaderboards" tabindex="2" onclick="this.blur();" > <div class="icon"> <i class="fas fa-fw fa-crown"></i> </div> </div> <a class="icon-button view-about" tabindex="2" href="/about" onclick="this.blur();" > <div class="icon"> <i class="fas fa-fw fa-info"></i> </div> </a> <!-- <a class="icon-button discord" tabindex="2" href="https://discord.gg/yENzqcB" onclick="this.blur();" target="_blank" style="text-decoration: none;" > <div class="icon"> <i class="fab fa-discord"></i> </div> </a> --> <a class="icon-button view-settings" tabindex="2" href="/settings" onclick="this.blur();" > <div class="icon"> <i class="fas fa-fw fa-cog"></i> </div> </a> <a class="icon-button hidden account view-account" tabindex="2" href="/account" onclick="this.blur();" > <div class="icon"> <i class="fas fa-fw fa-user"></i> </div> <div class="text"></div> </a> <a class="icon-button login view-login" tabindex="2" href="/login" onclick="this.blur();" > <div class="icon"> <i class="far fa-fw fa-user"></i> </div> </a> </div> <div class="config hidden"> <div class="mobileConfig"> <div class="icon-button"> <i class="fas fa-bars"></i> </div> </div> <div class="desktopConfig"> <div class="puncAndNum" style=" display: grid; grid-auto-flow: column; padding-top: 0.1rem; " > <div class="group punctuationMode"> <!-- <div class="title">time</div> --> <div class="buttons"> <div class="text-button toggleButton" tabindex="2"> punctuation </div> </div> </div> <div class="group numbersMode"> <!-- <div class="title">time</div> --> <div class="buttons"> <div class="text-button toggleButton" tabindex="2"> numbers </div> </div> </div> </div> <div class="group mode"> <!-- <div class="title">mode</div> --> <div class="buttons"> <div class="text-button active" mode="time" tabindex="2"> time </div> <div class="text-button" mode="words" tabindex="2">words</div> <div class="text-button" mode="quote" tabindex="2">quote</div> <div class="text-button" mode="zen" tabindex="2">zen</div> <div class="text-button" mode="custom" tabindex="2"> custom </div> </div> </div> <div class="group wordCount hidden"> <!-- <div class="title">words</div> --> <div class="buttons"> <div class="text-button" wordCount="10" tabindex="2">10</div> <div class="text-button" wordCount="25" tabindex="2">25</div> <div class="text-button active" wordCount="50" tabindex="2"> 50 </div> <div class="text-button" wordCount="100" tabindex="2"> 100 </div> <div class="text-button" wordCount="custom" tabindex="2"> <i class="fas fa-tools"></i> </div> </div> </div> <div class="group time"> <!-- <div class="title">time</div> --> <div class="buttons"> <div class="text-button" timeConfig="15" tabindex="2">15</div> <div class="text-button active" timeConfig="30" tabindex="2"> 30 </div> <div class="text-button" timeConfig="60" tabindex="2">60</div> <div class="text-button" timeConfig="120" tabindex="2"> 120 </div> <div class="text-button" timeConfig="custom" tabindex="2"> <i class="fas fa-tools"></i> </div> </div> </div> <div class="group quoteLength hidden"> <!-- <div class="title">time</div> --> <div class="buttons"> <div class="text-button" quoteLength="-1" tabindex="2"> all </div> <div class="text-button" quoteLength="0" tabindex="2"> short </div> <div class="text-button active" quoteLength="1" tabindex="2"> medium </div> <div class="text-button" quoteLength="2" tabindex="2"> long </div> <div class="text-button" quoteLength="3" tabindex="2"> thicc </div> <div class="text-button" quoteLength="-2" tabindex="2"> <i class="fas fa-search"></i> </div> </div> </div> <div class="group customText hidden"> <!-- <div class="title">time</div> --> <div class="buttons"> <div class="text-button">change</div> </div> </div> </div> </div> <div class="signOut hidden" style="grid-column: 3/4; grid-row: 1/2" tabindex="0" > <i class="fas fa-sign-out-alt"></i> sign out </div> </div> <div id="middle"> <div class="page pageLoading hidden"> <div class="preloader"> <div class="icon"> <i class="fas fa-fw fa-spin fa-circle-notch"></i> </div> <div class="barWrapper hidden"> <div class="bar"> <div class="fill"></div> </div> <div class="text"></div> </div> </div> </div> <div class="page pageTest hidden"> <div id="typingTest"> <div id="capsWarning" class="hidden"> <i class="fas fa-lock"></i> Caps Lock </div> <div id="memoryTimer">Time left to memorise all words: 0s</div> <div id="testModesNotice"></div> <input id="wordsInput" class="" tabindex="0" type="text" autocomplete="off" autocapitalize="off" autocorrect="off" /> <div id="timerNumber" class="timerMain"> <div>60</div> </div> <div id="miniTimerAndLiveWpm" class="timerMain size15"> <div class="time hidden">1:00</div> <div class="wpm">60</div> <div class="acc">100%</div> <div class="burst">1</div> </div> <div class="outOfFocusWarning hidden"> <i class="fas fa-mouse-pointer"></i> Click or press any key to focus </div> <div id="wordsWrapper" translate="no"> <div id="paceCaret" class="default size15 hidden"></div> <div id="caret" class="default size15"></div> <div id="words" class="size15"></div> </div> <div class="keymap hidden"> <div class="row r1"> <div></div> <div class="keymap-key" id="Key1"> <span class="letter">1</span> </div> <div class="keymap-key" id="Key2"> <span class="letter">2</span> </div> <div class="keymap-key" id="Key3"> <span class="letter">3</span> </div> <div class="keymap-key" id="Key4"> <span class="letter">4</span> </div> <div class="keymap-key" id="Key5"> <span class="letter">5</span> </div> <div class="keymap-key" id="Key6"> <span class="letter">6</span> </div> <div class="keymap-split-spacer"></div> <div class="keymap-key" id="Key7"> <span class="letter">7</span> </div> <div class="keymap-key" id="Key8"> <span class="letter">8</span> </div> <div class="keymap-key" id="Key9"> <span class="letter">9</span> </div> <div class="keymap-key" id="Key0"> <span class="letter">0</span> </div> <div class="keymap-key" id="Key-"> <span class="letter">-</span> </div> <div class="keymap-key" id="Key="> <span class="letter">=</span> </div> </div> <div class="row r2"> <div></div> <div class="keymap-key" id="KeyQ"> <span class="letter">q</span> </div> <div class="keymap-key" id="KeyW"> <span class="letter">w</span> </div> <div class="keymap-key" id="KeyE"> <span class="letter">e</span> </div> <div class="keymap-key" id="KeyR"> <span class="letter">r</span> </div> <div class="keymap-key" id="KeyT"> <span class="letter">t</span> </div> <div class="keymap-split-spacer"></div> <div class="keymap-key" id="KeyY"> <span class="letter">y</span> </div> <div class="keymap-key" id="KeyU"> <span class="letter">u</span> </div> <div class="keymap-key" id="KeyI"> <span class="letter">i</span> </div> <div class="keymap-key" id="KeyO"> <span class="letter">o</span> </div> <div class="keymap-key" id="KeyP"> <span class="letter">p</span> </div> <div class="keymap-key" id="KeyLeftBracket"> <span class="letter">[</span> </div> <div class="keymap-key" id="KeyRightBracket"> <span class="letter">]</span> </div> <div class="keymap-key hidden-key" id="Backslash"> <span class="letter">\</span> </div> </div> <div class="row r3"> <div></div> <div class="keymap-key" id="KeyA"> <span class="letter">a</span> </div> <div class="keymap-key" id="KeyS"> <span class="letter">s</span> </div> <div class="keymap-key" id="KeyD"> <span class="letter">d</span> </div> <div class="keymap-key" id="KeyF"> <span class="letter">f</span> <div class="bump"></div> </div> <div class="keymap-key" id="KeyG"> <span class="letter">g</span> </div> <div class="keymap-split-spacer"></div> <div class="keymap-key" id="KeyH"> <span class="letter">h</span> </div> <div class="keymap-key" id="KeyJ"> <span class="letter">j</span> <div class="bump"></div> </div> <div class="keymap-key" id="KeyK"> <span class="letter">k</span> </div> <div class="keymap-key" id="KeyL"> <span class="letter">l</span> </div> <div class="keymap-key" id="KeySemicolon"> <span class="letter">;</span> </div> <div class="keymap-key" id="KeyQuote"> <span class="letter">'</span> </div> </div> <div class="row r4"> <div></div> <div class="keymap-key first" id="KeyZ"> <span class="letter">z</span> </div> <div class="keymap-key" id="KeyX"> <span class="letter">x</span> </div> <div class="keymap-key" id="KeyC"> <span class="letter">c</span> </div> <div class="keymap-key" id="KeyV"> <span class="letter">v</span> </div> <div class="keymap-key" id="KeyB"> <span class="letter">b</span> </div> <div class="keymap-key" id="KeyN"> <span class="letter">n</span> </div> <div class="keymap-split-spacer"></div> <div class="extraKey"> <span class="letter"></span> </div> <div class="keymap-key" id="KeyM"> <span class="letter">m</span> </div> <div class="keymap-key" id="KeyComma"> <span class="letter">,</span> </div> <div class="keymap-key" id="KeyPeriod"> <span class="letter">.</span> </div> <div class="keymap-key" id="KeySlash"> <span class="letter">/</span> </div> <div class="keymap-key last"> <span class="letter"></span> </div> </div> <div class="row r5"> <div></div> <div class="keymap-key key-split-space" id="KeySpace"> <span class="letter"></span> </div> <div class="keymap-split-spacer"></div> <div class="keymap-key key-split-space" id="KeySpace2"> <span class="letter"></span> </div> <div class="keymap-key hidden-key"> <span class="letter"></span> </div> </div> </div> <div id="largeLiveWpmAndAcc" class="timerMain"> <div id="liveWpm" class="hidden">123</div> <div id="liveAcc" class="hidden">100%%</div> <div id="liveBurst" class="hidden">1</div> </div> <div id="restartTestButton" aria-label="Restart Test" data-balloon-pos="down" class="" tabindex="0" onclick="this.blur();" > <i class="fas fa-fw fa-redo-alt"></i> </div> <div id="monkey" class="hidden"> <div class="up"></div> <div class="left hidden"></div> <div class="right hidden"></div> <div class="both hidden"></div> <div class="fast"> <div class="up"></div> <div class="left hidden"></div> <div class="right hidden"></div> <div class="both hidden"></div> </div> </div> <div id="premidTestMode" class="hidden"></div> <div id="premidSecondsLeft" class="hidden"></div> </div> <div id="result" tabindex="0" class="hidden"> <div class="stats"> <!-- <div class="info">words 10<br>punctuation</div> --> <div class="group wpm"> <div class="top"> <div class="text">wpm</div> <div class="crown hidden" aria-label="" data-balloon-pos="up" > <i class="fas fa-crown"></i> </div> </div> <div class="bottom" aria-label="" data-balloon-pos="up"> - </div> </div> <div class="group acc"> <div class="top">acc</div> <div class="bottom" aria-label="" data-balloon-pos="up"> - </div> </div> </div> <div class="stats morestats"> <div class="group testType"> <div class="top">test type</div> <div class="bottom">-</div> <div class="tags hidden" style="margin-top: 0.5rem"> <div class="top">tags</div> <div class="bottom">-</div> </div> </div> <!-- <div class="group infoAndTags"> --> <div class="group info"> <div class="top">other</div> <div class="bottom">-</div> </div> <!-- </div> --> <!-- <div class="subgroup"> --> <div class="group raw"> <div class="top">raw</div> <div class="bottom" aria-label="" data-balloon-pos="up"> - </div> </div> <div class="group key"> <div class="top">characters</div> <div class="bottom" aria-label="correct, incorrect, extra, and missed" data-balloon-break="" data-balloon-pos="up" > - </div> </div> <!-- </div> --> <!-- <div class="subgroup"> --> <div class="group flat consistency"> <div class="top">consistency</div> <div class="bottom" aria-label="" data-balloon-pos="up"> 2 - </div> </div> <div class="group time"> <div class="top">time</div> <div class="bottom" aria-label="" data-balloon-pos="up"> <div class="text">-</div> <div class="afk"></div> <div class="timeToday"></div> </div> </div> <!-- </div> --> <div class="group source hidden"> <div class="top"> source <span id="rateQuoteButton" class="icon-button hidden" aria-label="Rate quote" data-balloon-pos="up" style="display: inline-block" > <i class="icon far fa-star"></i> <span class="rating"></span> </span> </div> <div class="bottom">-</div> </div> </div> <div class="chart"> <!-- <div class="title">wpm over time</div> --> <canvas id="wpmChart"></canvas> </div> <div class="loginTip"> <span class="link">Sign in</span> to save your result </div> <div class="bottom" style="grid-column: 1/3"> <div id="resultWordsHistory" class="hidden"> <div class="title"> input history <span id="copyWordsListButton" class="icon-button" aria-label="Copy words list" data-balloon-pos="up" style="display: inline-block" > <i class="fas fa-fw fa-copy"></i> </span> <span id="toggleBurstHeatmap" class="icon-button" aria-label="Toggle burst heatmap" data-balloon-pos="up" style="display: inline-block" > <i class="fas fa-fw fa-fire-alt"></i> </span> <div class="heatmapLegend hidden"> <div>slow</div> <div class="boxes"> <div class="box"></div> <div class="box"></div> <div class="box"></div> <div class="box"></div> <div class="box"></div> </div> <div>fast</div> </div> </div> <div class="words"></div> </div> <div id="resultReplay" class="hidden"> <div class="title"> watch replay <span id="playpauseReplayButton" class="icon-button" aria-label="Start replay" data-balloon-pos="up" style="display: inline-block" > <i class="fas fa-play"></i> </span> <p id="replayStopwatch">0s</p> </div> <div id="wordsWrapper"> <div id="replayWords" class="words"></div> </div> </div> <div id="retrySavingResultButton" class="hidden" tabindex="0" onclick="this.blur();" > <i class="fas fa-redo"></i> Retry saving result </div> <div class="buttons"> <div id="nextTestButton" aria-label="Next test" data-balloon-pos="down" tabindex="0" onclick="this.blur();" > <i class="fas fa-fw fa-chevron-right"></i> </div> <div id="restartTestButtonWithSameWordset" aria-label="Repeat test" data-balloon-pos="down" tabindex="0" onclick="this.blur();" > <i class="fas fa-fw fa-sync-alt"></i> </div> <div id="practiseWordsButton" aria-label="Practice words" data-balloon-pos="down" tabindex="0" onclick="this.blur();" > <i class="fas fa-fw fa-exclamation-triangle"></i> </div> <div id="showWordHistoryButton" aria-label="Toggle words history" data-balloon-pos="down" tabindex="0" onclick="this.blur();" > <i class="fas fa-fw fa-align-left"></i> </div> <div id="watchReplayButton" aria-label="Watch replay" data-balloon-pos="down" tabindex="0" onclick="this.blur();" > <i class="fas fa-fw fa-backward"></i> </div> <div id="saveScreenshotButton" aria-label="Save screenshot" data-balloon-pos="down" tabindex="0" onclick="this.blur();" > <i class="far fa-fw fa-image"></i> </div> </div> </div> <div class="ssWatermark hidden">monkeytype.com</div> </div> </div> <div class="page pageAbout hidden"> <div class="scrollToTopButton invisible"> <i class="fas fa-angle-double-up"></i> </div> <div class="created"> Created with love by Miodec. <a href="#supporters_title">Supported</a> and <a href="#contributors_title">expanded</a> by many awesome people. Launched on 15th of May, 2020. </div> <div class="section"> <div class="title">about</div> <!-- <h1>about</h1> --> <p> Monkeytype is a minimalistic typing test, featuring many test modes, an account system to save your typing speed history and user configurable features like themes, a smooth caret and more. </p> </div> <div class="section"> <h1>word set</h1> <p> By default, this website uses the most common 200 words in the English language to generate its tests. You can change to an expanded set (1000 most common words) in the options, or change the language entirely. </p> </div> <div class="section"> <h1>keybinds</h1> <p> You can use <key>tab</key> and <key>enter</key> (or just <key>tab</key> if you have quick tab mode enabled) to restart the typing test. Open the command line by pressing <key>ctrl/cmd</key> + <key>shift</key> + <key>p</key> or <key>esc</key> - there you can access all the functionality you need without touching your mouse </p> </div> <div class="section"> <h1>stats</h1> <p> wpm - total amount of characters in the correctly typed words (including spaces), divided by 5 and normalised to 60 seconds. </p> <p> raw wpm - calculated just like wpm, but also includes incorrect words. </p> <p>acc - percentage of correctly pressed keys.</p> <p> char - correct characters / incorrect characters. Calculated after the test has ended. </p> <p> consistency - based on the variance of your raw wpm. Closer to 100% is better. Calculated using the coefficient of variation of raw wpm and mapped onto a scale from 0 to 100. </p> </div> <div id="ad_about1" class="hidden"></div> <div class="section"> <h1>results screen</h1> <p> After completing a test you will be able to see your wpm, raw wpm, accuracy, character stats, test length, leaderboards info and test info. (you can hover over some values to get floating point numbers). You can also see a graph of your wpm and raw over the duration of the test. Remember that the wpm line is a global average, while the raw wpm line is a local, momentary value. (meaning if you stop, the value is 0) </p> </div> <div class="section"> <h1>bug report or feature request</h1> <p> If you encounter a bug, or have a feature request - join the Discord server, send me an email, a direct message on Twitter or create an issue on GitHub. </p> </div> <div class="section"> <div class="title">support</div> <p> Thanks to everyone who has supported this project. It would not be possible without you and your continued support. </p> <div class="supportButtons"> <a class="button ads"> <div class="fas fa-ad"></div> Ads </a> <a class="button" href="https://ko-fi.com/monkeytype" target="_blank" rel="noreferrer" > <div class="fas fa-donate"></div> Ko-Fi </a> <a class="button" href="https://www.patreon.com/monkeytype" target="_blank" rel="noreferrer" > <div class="fab fa-patreon"></div> Patreon </a> <a class="button" href="https://monkeytype.store" target="_blank" rel="noreferrer" > <div class="fas fa-tshirt"></div> Merch </a> </div> </div> <div class="section"> <div class="title">contact</div> <p> If you encounter a bug, have a feature request or just want to say hi - here are the different ways you can contact me directly. </p> <div class="contactButtons"> <a class="button" href="mailto:jack@monkeytype.com" target="_blank" > <div class="fas fa-envelope"></div> Mail </a> <a class="button" href="https://twitter.com/monkeytypegame" target="_blank" rel="noreferrer" > <div class="fab fa-twitter"></div> Twitter </a> <a class="button" href="https://discord.gg/monkeytype" target="_blank" rel="noreferrer" > <div class="fab fa-discord"></div> Discord </a> <a class="button" href="https://github.com/Miodec/monkeytype" target="_blank" rel="noreferrer" > <div class="fab fa-github"></div> GitHub </a> </div> </div> <div class="section"> <div class="title">credits</div> <p> <a href="https://www.reddit.com/user/montydrei" target="_blank" rel="noreferrer" > Montydrei </a> for the name suggestion </p> <p> <a href="https://www.reddit.com/r/MechanicalKeyboards/comments/gc6wx3/experimenting_with_a_completely_new_type_of/" target="_blank" rel="noreferrer" > Everyone </a> who provided valuable feedback on the original reddit post for the prototype of this website </p> <p> <a href="#supporters_title">Supporters</a> who helped financially by donating, enabling optional ads or buying merch </p> <p> <a href="https://github.com/Miodec/monkeytype/graphs/contributors" target="_blank" rel="noreferrer" > Contributors </a> on GitHub that have helped with implementing various features, adding themes and more </p> </div> <div id="ad_about2" class="hidden"></div> <div class="section"> <h1 id="supporters_title">supporters</h1> <div class="supporters"></div> </div> <div class="section"> <h1 id="contributors_title">contributors</h1> <div class="contributors"></div> </div> </div> <div class="page pageSettings hidden"> <div class="scrollToTopButton"> <i class="fas fa-angle-double-up"></i> </div> <div class="tip"> tip: You can also change all these settings quickly using the command line ( <key>ctrl/cmd</key> + <key>shift</key> + <key>p</key> or <key>esc</key> ) </div> <!-- <div class="sectionGroupTitle">quick navigation</div> --> <div class="settingsGroup quickNav"> <div class="links"> <a href="#group_account">account</a> <a href="#group_behavior">behavior</a> <a href="#group_input">input</a> <a href="#group_sound">sound</a> <a href="#group_caret">caret</a> <a href="#group_appearance">appearance</a> <a href="#group_theme">theme</a> <a href="#group_hideElements">hide elements</a> <a href="#group_dangerZone">danger zone</a> </div> </div> <div id="ad_settings0" class="hidden"></div> <div id="group_account" class="sectionGroupTitle hidden" group="account" > account <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup account hidden"> <div class="section discordIntegration"> <h1>discord integration</h1> <div class="text"> When you connect your monkeytype account to your Discord account, you will be automatically assigned a new role every time you achieve a new personal best in a 60 second test. If you pair your accounts before joining the Discord server the bot <i>will not</i> give you a role. </div> <div class="buttons"> <a class="button" href="https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=https%3A%2F%2Fmonkeytype.com%2Fverify&response_type=token&scope=identify" style="text-decoration: none" > Link with Discord </a> </div> <div class="info hidden"> <div> <i class="fas fa-check"></i> Your accounts are paired! </div> <div id="unlinkDiscordButton" class="text-button" aria-label="Unlink" data-balloon-pos="up" > <i class="fas fa-unlink" aria-hidden="true"></i> </div> </div> </div> <div class="section tags"> <h1>tags</h1> <div class="text"> With tags, you can compare how fast you're typing in different situations. You can see your active tags above the test words. They will remain active until you deactivate them, or refresh the page. </div> <div class="tagsListAndButton"> <div class="tagsList"> <div class="tag" id="0"> <div class="active"> <i class="fas fa-check-square"></i> </div> <div class="title">staggered</div> <div class="editButton"><i class="fas fa-pen"></i></div> <div class="removeButton"> <i class="fas fa-trash"></i> </div> </div> </div> <div class="addTagButton"><i class="fas fa-plus"></i></div> </div> </div> <div class="section presets"> <h1>presets</h1> <div class="text"> Create settings presets that can be applied with one click. Remember to edit your preset if you make any changes - they don't save on their own. </div> <div class="presetsListAndButton"> <div class="presetsList"> <div class="buttons preset" id="0"> <div class="presetButton button"> <div class="title">staggered</div> </div> <div class="editButton button"> <i class="fas fa-pen"></i> </div> <div class="removeButton button"> <i class="fas fa-trash"></i> </div> </div> </div> <div class="addPresetButton"><i class="fas fa-plus"></i></div> </div> </div> <div class="sectionSpacer"></div> </div> <div id="group_behavior" class="sectionGroupTitle" group="behavior"> behavior <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup behavior"> <div class="section difficulty"> <h1>test difficulty</h1> <div class="text"> Normal is the classic type test experience. Expert fails the test if you submit (press space) an incorrect word. Master fails if you press a single incorrect key (meaning you have to achieve 100% accuracy). </div> <div class="buttons"> <div class="button" difficulty="normal" tabindex="0" onclick="this.blur();" > normal </div> <div class="button" difficulty="expert" tabindex="0" onclick="this.blur();" > expert </div> <div class="button" difficulty="master" tabindex="0" onclick="this.blur();" > master </div> </div> </div> <div class="section quickTab" id="quickTab"> <h1>quick tab mode</h1> <div class="text"> Press <key>tab</key> to quickly restart the test, or to quickly jump to the test page. This function disables tab navigation on the website. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section repeatQuotes" id="repeatQuotes"> <h1>repeat quotes</h1> <div class="text"> This setting changes the restarting behavior when typing in quote mode. Changing it to 'typing' will repeat the quote if you restart while typing. </div> <!-- , and 'always' will always repeat the quote (you will need to press <key>shift + tab</key> to move on to the next quote). --> <div class="buttons"> <div class="button" repeatQuotes="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" repeatQuotes="typing" tabindex="0" onclick="this.blur();" > typing </div> <!-- <div class="button" repeatQuotes="always" tabindex="0" onclick="this.blur();"> always </div> --> </div> </div> <div class="section blindMode"> <h1>blind mode</h1> <div class="text"> No errors or incorrect words are highlighted. Helps you to focus on raw speed. If enabled, quick end is recommended. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> </div> </div> </div> <!-- <div class="section readAheadMode"> <h1>read ahead mode</h1> <div class="text"> When enabled, the active and immediately following test words will be hidden. Helps you practice reading ahead. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> --> <div class="section alwaysShowWordsHistory"> <h1>always show words history</h1> <div class="text"> This option will automatically show the words history at the end of the test. Can cause slight lag with a lot of words. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section singleListCommandLine"> <h1>single list command line</h1> <div class="text"> When enabled, it will show the command line with all commands in a single list instead of submenu arrangements. Selecting 'manual' will expose all commands only after typing <key>></key> . </div> <div class="buttons"> <div class="button" singleListCommandLine="manual" tabindex="0" onclick="this.blur();" > manual </div> <div class="button" singleListCommandLine="on" tabindex="0" onclick="this.blur();" > on </div> </div> </div> <div class="section minWpm" section=""> <h1>min wpm</h1> <div class="text"> Automatically fails a test if your WPM falls below a threshold. </div> <div> <div class="inputAndButton"> <input type="number" placeholder="min wpm" class="input customMinWpmSpeed" tabindex="0" min="0" step="1" value="" /> <div class="button save" tabindex="0" onclick="this.blur();" > <i class="fas fa-save fa-fw"></i> </div> </div> <div class="buttons"> <div class="button" minWpm="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" minWpm="custom" tabindex="0" onclick="this.blur();" > custom </div> </div> </div> </div> <div class="section minAcc" section=""> <h1>min accuracy</h1> <div class="text"> Automatically fails a test if your accuracy falls below a threshold. </div> <div> <div class="inputAndButton"> <input type="number" placeholder="min accuracy" class="input customMinAcc" tabindex="0" min="0" step="1" value="" /> <div class="button save" tabindex="0" onclick="this.blur();" > <i class="fas fa-save fa-fw"></i> </div> </div> <div class="buttons"> <div class="button" minAcc="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" minAcc="custom" tabindex="0" onclick="this.blur();" > custom </div> </div> </div> </div> <div class="section minBurst" section=""> <h1>min burst</h1> <div class="text"> Automatically fails a test if your raw for a single word falls below this threshold. Selecting 'flex' allows for this threshold to automatically decrease for longer words. </div> <div> <div class="inputAndButton"> <input type="number" placeholder="min burst" class="input customMinBurst" tabindex="0" min="0" step="1" value="" /> <div class="button save" tabindex="0" onclick="this.blur();" > <i class="fas fa-save fa-fw"></i> </div> </div> <div class="buttons"> <div class="button" minBurst="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" minBurst="fixed" tabindex="0" onclick="this.blur();" > fixed </div> <div class="button" minBurst="flex" tabindex="0" onclick="this.blur();" > flex </div> </div> </div> </div> <div class="section britishEnglish" section=""> <h1>british english</h1> <div class="text"> When enabled, the website will use the British spelling instead of American. Note that this might not replace all words correctly. If you find any issues, please let us know. </div> <div> <div class="buttons"> <div class="button off active" tabindex="0" onclick="this.blur();" > off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> </div> <div class="section languageGroups fullWidth"> <h1>language groups</h1> <div class="buttons"></div> </div> <div class="section language fullWidth"> <h1>language</h1> <div class="buttons"></div> </div> <div class="section funbox fullWidth"> <h1>funbox</h1> <div class="text"> These are special modes that change the website in some special way (by altering the word generation, behavior of the website or the looks). Give each one of them a try! </div> <div class="buttons"></div> </div> <div class="sectionSpacer"></div> <div class="section customLayoutfluid"> <h1>custom layoutfluid</h1> <div class="text"> Select which layouts you want the layoutfluid funbox to cycle through. </div> <div class="inputAndButton"> <input type="text" placeholder="layouts (separated by space)" class="input" tabindex="0" /> <div class="button save" tabindex="0" onclick="this.blur();"> <i class="fas fa-save fa-fw"></i> </div> </div> </div> <div class="sectionSpacer"></div> </div> <div id="group_input" class="sectionGroupTitle" group="input"> input <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup input"> <div class="section freedomMode"> <h1>freedom mode</h1> <div class="text"> Allows you to delete any word, even if it was typed correctly. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section strictSpace"> <h1>strict space</h1> <div class="text"> Pressing space at the beginning of a word will insert a space character when this mode is enabled. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section oppositeShiftMode"> <h1>opposite shift mode</h1> <div class="text"> This mode will force you to use opposite <key>shift</key> keys for shifting. Using an incorrect one will count as an error. This feature ignores keys in locations <key>B</key> , <key>Y</key> , and <key>^</key> because many people use the other hand for those keys. If you're using external software to emulate your layout (including QMK), you should use the "keymap" mode - the standard "on" will not work. This will enforce opposite shift based on the "keymap layout" setting. </div> <div class="buttons"> <div class="button" oppositeShiftMode="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" oppositeShiftMode="on" tabindex="0" onclick="this.blur();" > on </div> <div class="button" oppositeShiftMode="keymap" tabindex="0" onclick="this.blur();" > keymap </div> </div> </div> <div class="section stopOnError"> <h1>stop on error</h1> <div class="text"> Letter mode will stop input when pressing any incorrect letters. Word mode will not allow you to continue to the next word until you correct all mistakes. </div> <div class="buttons"> <div class="button" stopOnError="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" stopOnError="word" tabindex="0" onclick="this.blur();" > word </div> <div class="button" stopOnError="letter" tabindex="0" onclick="this.blur();" > letter </div> </div> </div> <div class="section confidenceMode"> <h1>confidence mode</h1> <div class="text"> When enabled, you will not be able to go back to previous words to fix mistakes. When turned up to the max, you won't be able to backspace at all. </div> <div class="buttons"> <div class="button" confidenceMode="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" confidenceMode="on" tabindex="0" onclick="this.blur();" > on </div> <div class="button" confidenceMode="max" tabindex="0" onclick="this.blur();" > max </div> </div> </div> <div class="section quickEnd"> <h1>quick end</h1> <div class="text"> This only applies to the words mode - when enabled, the test will end as soon as the last word has been typed, even if it's incorrect. When disabled, you need to manually confirm the last incorrect entry with a space. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section indicateTypos" section=""> <h1>indicate typos</h1> <div class="text">Shows typos underneath the letters.</div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section hideExtraLetters" section=""> <h1>hide extra letters</h1> <div class="text"> Hides extra letters. This will completely avoid words jumping lines (due to changing width), but might feel a bit confusing when you press a key and nothing happens. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section swapEscAndTab" section=""> <h1>swap esc and tab</h1> <div class="text"> Swap the behavior of tab and escape keys. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section lazyMode"> <h1>lazy mode</h1> <div class="text"> Replaces accented letters with their normal equivalents. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section layout fullWidth"> <h1>layout emulator</h1> <div class="text"> With this setting you can emulate other layouts. This setting is best kept off, as it can break things like dead keys and alt layers. </div> <div class="buttons"></div> </div> <div class="sectionSpacer"></div> </div> <div id="ad_settings1" class="hidden"></div> <div id="group_sound" class="sectionGroupTitle" group="sound"> sound <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup sound"> <div class="section soundVolume"> <h1>sound volume</h1> <div class="text">Change the volume of the sound effects.</div> <div class="buttons"> <div class="button" soundVolume="0.1" tabindex="0" onclick="this.blur();" > quiet </div> <div class="button" soundVolume="0.5" tabindex="0" onclick="this.blur();" > medium </div> <div class="button" soundVolume="1.0" tabindex="0" onclick="this.blur();" > loud </div> </div> </div> <div class="section playSoundOnClick fullWidth"> <h1>play sound on click</h1> <div class="text"> Plays a short sound when you press a key. </div> <div class="buttons"> <div class="button" playSoundOnClick="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" playSoundOnClick="1" tabindex="0" onclick="this.blur();" > click </div> <div class="button" playSoundOnClick="2" tabindex="0" onclick="this.blur();" > beep </div> <div class="button" playSoundOnClick="3" tabindex="0" onclick="this.blur();" > pop </div> <div class="button" playSoundOnClick="4" tabindex="0" onclick="this.blur();" > nk creams </div> <div class="button" playSoundOnClick="5" tabindex="0" onclick="this.blur();" > typewriter </div> <div class="button" playSoundOnClick="6" tabindex="0" onclick="this.blur();" > osu </div> <div class="button" playSoundOnClick="7" tabindex="0" onclick="this.blur();" > hitmarker </div> </div> </div> <div class="section playSoundOnError"> <h1>play sound on error</h1> <div class="text"> Plays a short sound if you press an incorrect key or press space too early. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="sectionSpacer"></div> </div> <div id="group_caret" class="sectionGroupTitle" group="caret"> caret <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup caret"> <div class="section smoothCaret" section=""> <h1>smooth caret</h1> <div class="text"> The caret will move smoothly between letters and words. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section caretStyle" section=""> <h1>caret style</h1> <div class="text"> Change the style of the caret during the test. </div> <div class="buttons"> <div class="button" caretStyle="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" caretStyle="default" tabindex="0" onclick="this.blur();" > | </div> <div class="button" caretStyle="block" tabindex="0" onclick="this.blur();" > </div> <div class="button" caretStyle="outline" tabindex="0" onclick="this.blur();" > </div> <div class="button" caretStyle="underline" tabindex="0" onclick="this.blur();" > _ </div> </div> </div> <div class="section paceCaret" section=""> <h1>pace caret</h1> <div class="text"> Displays a second caret that moves at constant speed. The 'average' option averages the speed of last 10 results. </div> <div> <div class="inputAndButton"> <input type="number" placeholder="wpm" class="input customPaceCaretSpeed" tabindex="0" min="0" step="1" value="" /> <div class="button save" tabindex="0" onclick="this.blur();" > <i class="fas fa-save fa-fw"></i> </div> </div> <div class="buttons"> <div class="button" paceCaret="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" paceCaret="average" tabindex="0" onclick="this.blur();" > average </div> <div class="button" paceCaret="pb" tabindex="0" onclick="this.blur();" > pb </div> <div class="button" paceCaret="custom" tabindex="0" onclick="this.blur();" > custom </div> </div> </div> </div> <div class="section repeatedPace" section=""> <h1>repeated pace</h1> <div class="text"> When repeating a test, a pace caret will automatically be enabled for one test with the speed of your previous test. It does not override the pace caret if it's already enabled. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section paceCaretStyle" section=""> <h1>pace caret style</h1> <div class="text"> Change the style of the pace caret during the test. </div> <div class="buttons"> <div class="button" paceCaretStyle="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" paceCaretStyle="default" tabindex="0" onclick="this.blur();" > | </div> <div class="button" paceCaretStyle="block" tabindex="0" onclick="this.blur();" > </div> <div class="button" paceCaretStyle="outline" tabindex="0" onclick="this.blur();" > </div> <div class="button" paceCaretStyle="underline" tabindex="0" onclick="this.blur();" > _ </div> </div> </div> <div class="sectionSpacer"></div> </div> <div id="group_appearance" class="sectionGroupTitle" group="appearance" > appearance <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup appearance"> <div class="section timerStyle" section=""> <h1>timer/progress style</h1> <div class="text"> Change the style of the timer/progress during a timed test. </div> <div class="buttons"> <div class="button" timerStyle="bar" tabindex="0" onclick="this.blur();" > bar </div> <div class="button" timerStyle="text" tabindex="0" onclick="this.blur();" > text </div> <div class="button" timerStyle="mini" tabindex="0" onclick="this.blur();" > mini </div> </div> </div> <div class="section timerColor" section=""> <h1>timer/progress color</h1> <div class="text"> Change the color of the timer/progress number/bar and live wpm number. </div> <div class="buttons"> <div class="button" timerColor="black" tabindex="0" onclick="this.blur();" > black </div> <div class="button" timerColor="sub" tabindex="0" onclick="this.blur();" > sub </div> <div class="button" timerColor="text" tabindex="0" onclick="this.blur();" > text </div> <div class="button" timerColor="main" tabindex="0" onclick="this.blur();" > main </div> </div> </div> <div class="section timerOpacity" section=""> <h1>timer/progress opacity</h1> <div class="text"> Change the opacity of the timer/progress number/bar and live wpm number. </div> <div class="buttons"> <div class="button" timerOpacity="0.25" tabindex="0" onclick="this.blur();" > 0.25 </div> <div class="button" timerOpacity="0.5" tabindex="0" onclick="this.blur();" > 0.5 </div> <div class="button" timerOpacity="0.75" tabindex="0" onclick="this.blur();" > 0.75 </div> <div class="button" timerOpacity="1" tabindex="0" onclick="this.blur();" > 1 </div> </div> </div> <div class="section highlightMode" section=""> <h1>highlight mode</h1> <div class="text"> Change what is highlighted during the test. </div> <div class="buttons"> <div class="button" highlightMode="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" highlightMode="letter" tabindex="0" onclick="this.blur();" > letter </div> <div class="button" highlightMode="word" tabindex="0" onclick="this.blur();" > word </div> </div> </div> <div class="section smoothLineScroll"> <h1>smooth line scroll</h1> <div class="text"> When enabled, the line transition will be animated. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section showAllLines"> <h1>show all lines</h1> <div class="text"> When enabled, the website will show all lines for word, custom and quote mode tests - otherwise the lines will be limited to 3, and will automatically scroll. Using this could cause the timer text and live wpm to not be visible. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section alwaysShowDecimalPlaces"> <h1>always show decimal places</h1> <div class="text"> Always shows decimal places for values on the result page, without the need to hover over the stats. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section alwaysShowCPM"> <h1>always show cpm</h1> <div class="text"> Always shows characters per minute calculation instead of the default words per minute calculation. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section startGraphsAtZero"> <h1>start graphs at zero</h1> <div class="text"> Force graph axis to always start at zero, no matter what the data is. Turning this off may exaggerate the value changes. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section fontSize"> <h1>font size</h1> <div class="text">Change the font size of the test words.</div> <div class="buttons"> <div class="button" fontsize="1" tabindex="0" onclick="this.blur();" > 1 </div> <div class="button" fontsize="125" tabindex="0" onclick="this.blur();" > 1.25 </div> <div class="button" fontsize="15" tabindex="0" onclick="this.blur();" > 1.5 </div> <div class="button" fontsize="2" tabindex="0" onclick="this.blur();" > 2 </div> <div class="button" fontsize="3" tabindex="0" onclick="this.blur();" > 3 </div> <div class="button" fontsize="4" tabindex="0" onclick="this.blur();" > 4 </div> </div> </div> <div class="section fontFamily fullWidth"> <h1>font family</h1> <!-- <div class="text">Change the font family for the site</div> --> <div class="buttons"></div> </div> <div class="section pageWidth"> <h1>page width</h1> <div class="text">Control the width of the content.</div> <div class="buttons"> <div class="button" pageWidth="100" tabindex="0" onclick="this.blur();" > 100% </div> <div class="button" pageWidth="125" tabindex="0" onclick="this.blur();" > 125% </div> <div class="button" pageWidth="150" tabindex="0" onclick="this.blur();" > 150% </div> <div class="button" pageWidth="200" tabindex="0" onclick="this.blur();" > 200% </div> <div class="button" pageWidth="max" tabindex="0" onclick="this.blur();" > Max </div> </div> </div> <div class="section keymapMode"> <h1>keymap</h1> <div class="text"> Displays your current layout while taking a test. React shows what you pressed and Next shows what you need to press next. </div> <div class="buttons"> <div class="button" keymapMode="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" keymapMode="static" tabindex="0" onclick="this.blur();" > static </div> <div class="button" keymapMode="react" tabindex="0" onclick="this.blur();" > react </div> <div class="button" keymapMode="next" tabindex="0" onclick="this.blur();" > next </div> </div> </div> <div class="section keymapStyle fullWidth"> <h1>keymap style</h1> <!-- <div class="text">Displays the keymap in a different style.</div> --> <div class="buttons"> <div class="button" keymapStyle="staggered" tabindex="0" onclick="this.blur();" > staggered </div> <div class="button" keymapStyle="alice" tabindex="0" onclick="this.blur();" > alice </div> <div class="button" keymapStyle="matrix" tabindex="0" onclick="this.blur();" > matrix </div> <div class="button" keymapStyle="split" tabindex="0" onclick="this.blur();" > split </div> <div class="button" keymapStyle="split_matrix" tabindex="0" onclick="this.blur();" > split matrix </div> </div> </div> <div class="section keymapLegendStyle fullWidth"> <h1>keymap legend style</h1> <div class="buttons"> <div class="button" keymapLegendStyle="lowercase" tabindex="0" onclick="this.blur();" > lowercase </div> <div class="button" keymapLegendStyle="uppercase" tabindex="0" onclick="this.blur();" > uppercase </div> <div class="button" keymapLegendStyle="blank" tabindex="0" onclick="this.blur();" > blank </div> </div> </div> <div class="section keymapLayout fullWidth"> <h1>keymap layout</h1> <div class="buttons"></div> </div> <div class="sectionSpacer"></div> </div> <div id="ad_settings2" class="hidden"></div> <div id="group_theme" class="sectionGroupTitle" group="theme"> theme <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup theme"> <div class="section flipTestColors"> <h1>flip test colors</h1> <div class="text"> By default, typed text is brighter than the future text. When enabled, the colors will be flipped and the future text will be brighter than the already typed text. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section colorfulMode"> <h1>colorful mode</h1> <div class="text"> When enabled, the test words will use the main color, instead of the text color, making the website more colorful. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> off </div> <div class="button on" tabindex="0" onclick="this.blur();"> on </div> </div> </div> <div class="section customBackgroundSize"> <h1>custom background</h1> <div class="text"> Set an image url to be a custom background image. Cover fits the image to cover the screen. Contain fits the image to be fully visible. Max fits the image corner to corner. </div> <div> <div class="inputAndButton"> <input type="text" placeholder="image url" class="input" tabindex="0" onClick="this.select();" /> <div class="button save" tabindex="0" onclick="this.blur();" > <i class="fas fa-save fa-fw"></i> </div> </div> <div class="buttons"> <div class="button" customBackgroundSize="cover" tabindex="0" onclick="this.blur();" > cover </div> <div class="button" customBackgroundSize="contain" tabindex="0" onclick="this.blur();" > contain </div> <div class="button" customBackGroundSize="max" tabindex="0" onclick="this.blur();" > max </div> </div> </div> </div> <div class="section customBackgroundFilter fullWidth"> <h1>custom background filter</h1> <div class="text"> Apply various effects to the custom background. </div> <div class="groups"> <div class="group blur"> <div class="title">blur</div> <div class="value"></div> <input type="range" min="0" max="5" value="0" step="0.1" /> </div> <div class="group brightness"> <div class="title">brightness</div> <div class="value"></div> <input type="range" min="0" max="2" value="1" step="0.1" /> </div> <div class="group saturate"> <div class="title">saturate</div> <div class="value"></div> <input type="range" min="0" max="2" value="1" step="0.1" /> </div> <div class="group opacity"> <div class="title">opacity</div> <div class="value"></div> <input type="range" min="0" max="1" value="1" step="0.1" /> </div> <div class="saveContainer"> <div class="save button" style="grid-column: 3"> <!-- <i class="fas fa-save fa-fw"></i> --> save </div> </div> </div> </div> <div class="section randomTheme fullWidth"> <h1>randomize theme</h1> <div class="text"> After completing a test, the theme will be set to a random one. The random themes are not saved to your config. If set to 'fav' only favourite themes will be randomized. If set to 'light' or 'dark', only presets with light or dark background colors will be randomized, respectively. </div> <div class="buttons"> <div class="button" randomTheme="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" randomTheme="on" tabindex="0" onclick="this.blur();" > on </div> <div class="button" randomTheme="fav" tabindex="0" onclick="this.blur();" > favorite </div> <div class="button" randomTheme="light" tabindex="0" onclick="this.blur();" > light </div> <div class="button" randomTheme="dark" tabindex="0" onclick="this.blur();" > dark </div> </div> </div> <div class="section themes fullWidth"> <h1>theme</h1> <div class="tabs"> <div class="button" tab="preset" tabindex="0" onclick="this.blur();" > preset </div> <div class="button" tab="custom" tabindex="0" onclick="this.blur();" > custom </div> </div> <!-- <div class='tabs'> <button tab="preset" class="tab">preset</button> <button tab="custom" class="tab">custom</button> </div> --> <!-- Used to convert colors to hex --> <div class="colorConverter"></div> <div class="tabContainer"> <div tabContent="custom" class="tabContent section customTheme hidden" > <label class="colorText">background</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--bg-color-txt" /> <label for="--bg-color" class="button" style=" color: var(--text-color); background: rgba(0, 0, 0, 0.1); " > <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--bg-color" /> </div> <label class="colorText">main</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--main-color-txt" /> <label for="--main-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--main-color" /> </div> <label class="colorText">caret</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--caret-color-txt" /> <label for="--caret-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--caret-color" /> </div> <label class="colorText">sub</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--sub-color-txt" /> <label for="--sub-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--sub-color" /> </div> <label class="colorText">text</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--text-color-txt" /> <label for="--text-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--text-color" /> </div> <span class="spacer"></span> <label class="colorText">error</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--error-color-txt" /> <label for="--error-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--error-color" /> </div> <label class="colorText">extra error</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--error-extra-color-txt" /> <label for="--error-extra-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--error-extra-color" /> </div> <p>colorful mode</p> <label class="colorText">error</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--colorful-error-color-txt" /> <label for="--colorful-error-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--colorful-error-color" /> </div> <label class="colorText">extra error</label> <div class="colorPicker inputAndButton"> <input type="text" value="#000000" class="input" id="--colorful-error-extra-color-txt" /> <label for="--colorful-error-extra-color" class="button"> <i class="fas fa-fw fa-palette"></i> </label> <input type="color" class="color" value="#000000" id="--colorful-error-extra-color" /> </div> <!-- <div style=" display: grid; gap: 2rem; grid-auto-flow: column; grid-column: 1/5; " > --> <div class="button" id="loadCustomColorsFromPreset" style="grid-column: 1/3" > load from preset </div> <div class="button" id="shareCustomThemeButton">share</div> <div class="button saveCustomThemeButton">save</div> <!-- </div> --> </div> <div tabContent="preset" class="tabContent"> <div class="favThemes buttons"></div> <div class="allThemes buttons"></div> </div> </div> </div> <div class="sectionSpacer"></div> </div> <div id="group_hideElements" class="sectionGroupTitle" group="hideElements" > hide elements <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup hideElements"> <div class="section showLiveWpm"> <h1>live wpm</h1> <div class="text"> Displays a live WPM speed during the test. Updates once every second. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> hide </div> <div class="button on" tabindex="0" onclick="this.blur();"> show </div> </div> </div> <div class="section showLiveAcc"> <h1>live accuracy</h1> <div class="text">Displays live accuracy during the test.</div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> hide </div> <div class="button on" tabindex="0" onclick="this.blur();"> show </div> </div> </div> <div class="section showLiveBurst"> <h1>live burst</h1> <div class="text"> Displays live burst during the test of the last word you typed. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> hide </div> <div class="button on" tabindex="0" onclick="this.blur();"> show </div> </div> </div> <div class="section showTimerProgress"> <h1>timer/progress</h1> <div class="text"> Displays a live timer for timed tests and progress for words/custom tests. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> hide </div> <div class="button on" tabindex="0" onclick="this.blur();"> show </div> </div> </div> <div class="section showKeyTips"> <h1>key tips</h1> <div class="text"> Shows the keybind tips at the bottom of the page. </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> hide </div> <div class="button on" tabindex="0" onclick="this.blur();"> show </div> </div> </div> <div class="section showOutOfFocusWarning"> <h1>out of focus warning</h1> <div class="text"> Shows an out of focus reminder after 1 second of being 'out of focus' (not being able to type). </div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> hide </div> <div class="button on" tabindex="0" onclick="this.blur();"> show </div> </div> </div> <div class="section capsLockWarning"> <h1>caps lock warning</h1> <div class="text">Displays a warning when caps lock is on.</div> <div class="buttons"> <div class="button off" tabindex="0" onclick="this.blur();"> hide </div> <div class="button on" tabindex="0" onclick="this.blur();"> show </div> </div> </div> <div class="sectionSpacer"></div> </div> <div id="group_dangerZone" class="sectionGroupTitle" group="dangerZone" > danger zone <i class="fas fa-chevron-down"></i> </div> <div class="settingsGroup dangerZone"> <div class="section importexportSettings"> <h1>import/export settings</h1> <div class="text">Import or export the settings as JSON.</div> <div class="buttons"> <div class="button off" id="importSettingsButton" tabindex="0" onclick="this.blur();" > import </div> <div class="button off" id="exportSettingsButton" tabindex="0" onclick="this.blur();" > export </div> </div> </div> <div class="section enableAds"> <h1>enable ads</h1> <div class="text"> If you wish to support me without directly donating you can enable ads that will be visible at the bottom of the screen. Sellout mode also shows ads on both sides of the screen. <br /> <br /> (changes will take effect after a refresh). </div> <div class="buttons"> <div class="button" enableAds="off" tabindex="0" onclick="this.blur();" > off </div> <div class="button" enableAds="on" tabindex="0" onclick="this.blur();" > on </div> <div class="button" enableAds="max" tabindex="0" onclick="this.blur();" > sellout </div> </div> </div> <div class="section resetSettings"> <h1>reset settings</h1> <div class="text"> Resets settings to the default (but doesn't touch your tags). Warning: you can't undo this action! </div> <div class="buttons"> <div class="button off danger" id="resetSettingsButton" tabindex="0" onclick="this.blur();" > reset settings </div> </div> </div> <div class="section resetPersonalBests needsAccount hidden"> <h1>reset personal bests</h1> <div class="text"> Resets all your personal bests (but doesn't delete any tests from your history). Warning: you can't undo this action! </div> <div class="buttons"> <div class="button off danger" id="resetPersonalBestsButton" tabindex="0" onclick="this.blur();" > reset personal bests </div> </div> </div> <div class="section updateAccountName needsAccount hidden"> <h1>update account name</h1> <div class="text"> Change the name of your account. You can only do this once every 30 days. </div> <div class="buttons"> <div class="button off danger" id="updateAccountName" tabindex="0" onclick="this.blur();" > update name </div> </div> </div> <div class="section passwordAuthSettings needsAccount hidden"> <h1>password authentication settings</h1> <div class="text"> Add password authentication, update your password or email. </div> <div class="buttons vertical"> <div class="button danger" id="addPasswordAuth" tabindex="0" onclick="this.blur();" > add password authentication </div> <div class="button danger" id="emailPasswordAuth" tabindex="0" onclick="this.blur();" > update email </div> <div class="button danger" id="passPasswordAuth" tabindex="0" onclick="this.blur();" > update password </div> </div> </div> <div class="section googleAuthSettings needsAccount hidden"> <h1>google authentication settings</h1> <div class="text">Add or remove Google authentication.</div> <div class="buttons vertical"> <div class="button danger" id="addGoogleAuth" tabindex="0" onclick="this.blur();" > add google authentication </div> <div class="button danger" id="removeGoogleAuth" tabindex="0" onclick="this.blur();" > remove google authentication </div> </div> </div> <!-- <div class="section updateAccountEmail needsAccount hidden"> <h1>update account email</h1> <div class="text"> In case you misspell it or get a new address. </div> <div class="buttons"> <div class="button off danger" id="updateAccountEmail" tabindex="0" onclick="this.blur();" > update email </div> </div> </div> <div class="section updateAccountPassword needsAccount hidden"> <h1>update account password</h1> <div class="text">Change the password you use to sign in.</div> <div class="buttons"> <div class="button off danger" id="updateAccountPassword" tabindex="0" onclick="this.blur();" > update password </div> </div> </div> --> <div class="section deleteAccount needsAccount hidden"> <h1>delete account</h1> <div class="text"> Deletes your account and all data connected to it. </div> <div class="buttons"> <div class="button off danger" id="deleteAccount" tabindex="0" onclick="this.blur();" > delete account </div> </div> </div> <div class="sectionSpacer"></div> </div> </div> <div class="page pageLogin hidden"> <div class="preloader hidden"> <i class="fas fa-fw fa-spin fa-circle-notch"></i> </div> <div class="register side"> <div class="title">register</div> <form action="" autocomplete="nope"> <input type="text" placeholder="username" autocomplete="new-username" /> <input type="email" placeholder="email" autocomplete="new-email" /> <input type="email" placeholder="verify email" autocomplete="verify-email" /> <input type="password" placeholder="password" autocomplete="new-password" name="new-password" /> <input type="password" placeholder="verify password" autocomplete="verify-password" name="verify-password" /> <div class="button"> <i class="fas fa-user-plus"></i> Sign Up </div> </form> </div> <div class="login side"> <div class="title">login</div> <div id="forgotPasswordButton">Forgot password?</div> <form action=""> <input name="current-email" type="email" placeholder="email" autocomplete="current-username" /> <input name="current-password" type="password" placeholder="password" autocomplete="current-password" /> <div> <label id="rememberMe" class="checkbox"> <input type="checkbox" checked /> <div class="customTextCheckbox"></div> Remember me </label> </div> <div class="button signIn"> <i class="fas fa-sign-in-alt"></i> Sign In </div> <div style="font-size: 0.75rem; text-align: center">or</div> <div class="button signInWithGoogle"> <i class="fab fa-google"></i> Google Sign In </div> <!-- <div class="button signInWithGitHub"> <i class="fab fa-github"></i> GitHub Sign In </div> --> </form> </div> </div> <div class="page pageAccount hidden"> <div class="scrollToTopButton"> <i class="fas fa-angle-double-up"></i> </div> <div class="preloader"> <div class="icon"> <i class="fas fa-fw fa-spin fa-circle-notch"></i> </div> <div class="barWrapper hidden"> <div class="bar"> <div class="fill"></div> </div> <div class="text"></div> </div> </div> <div class="content hidden"> <div class="miniResultChartWrapper"> <canvas id="miniResultChart"></canvas> </div> <div class="miniResultChartBg"></div> <div class="triplegroup"> <div class="group globalTestsStarted"> <div class="title">tests started</div> <div class="val">-</div> </div> <div class="group globalTestsCompleted"> <div class="title">tests completed</div> <div class="val">-</div> </div> <div class="group globalTimeTyping"> <div class="title">time typing</div> <div class="val">-</div> </div> </div> <div class="group createdDate">Account created on -</div> <div class="group personalBestTables"> <div class="title">personal bests</div> <div class="tables"> <div class="titleAndTable timePbTable"> <table width="100%"> <thead> <tr> <td width="1%">time</td> <td width="33%"> wpm <br /> <span class="sub">accuracy</span> </td> <td width="33%"> raw <br /> <span class="sub">consistency</span> </td> <td width="33%">date</td> </tr> </thead> <tbody> <tr> <td>15</td> <td>-</td> <td>-</td> <td>-</td> </tr> <tr> <td>30</td> <td>-</td> <td>-</td> <td>-</td> </tr> <tr> <td>60</td> <td>-</td> <td>-</td> <td>-</td> </tr> <tr> <td>120</td> <td>-</td> <td>-</td> <td>-</td> </tr> </tbody> </table> <div class="button showAllTimePbs">show all</div> </div> <div class="titleAndTable wordsPbTable"> <table width="100%"> <thead> <tr> <td width="1%">words</td> <td width="33%"> wpm <br /> <span class="sub">accuracy</span> </td> <td width="33%"> raw <br /> <span class="sub">consistency</span> </td> <td width="33%">date</td> </tr> </thead> <tbody> <tr> <td>10</td> <td>-</td> <td>-</td> <td>-</td> </tr> <tr> <td>25</td> <td>-</td> <td>-</td> <td>-</td> </tr> <tr> <td>50</td> <td>-</td> <td>-</td> <td>-</td> </tr> <tr> <td>100</td> <td>-</td> <td>-</td> <td>-</td> </tr> </tbody> </table> <div class="button showAllWordsPbs">show all</div> </div> </div> </div> <div id="ad_account" class="hidden"></div> <div class="group topFilters"> <!-- <div class="button" id="currentConfigFilter" style="grid-column: 1/3;" > set filters to current settings </div> --> <div class="buttonsAndTitle" style="grid-column: 1/3"> <div class="title">filters</div> <div class="buttons"> <div class="button allFilters">all</div> <div class="button currentConfigFilter"> current settings </div> <div class="button toggleAdvancedFilters">advanced</div> </div> </div> <div class="buttonsAndTitle testDate" style="grid-column: 1/3; margin-top: 1rem" > <!-- <div class="title">date</div> --> <div class="buttons filterGroup" group="date"> <div class="button" filter="last_day">last day</div> <div class="button" filter="last_week">last week</div> <div class="button" filter="last_month">last month</div> <div class="button" filter="last_3months"> last 3 months </div> <div class="button" filter="all">all time</div> </div> </div> </div> <div class="group filterButtons" style="display: none"> <div class="buttonsAndTitle" style="grid-column: 1/3"> <div class="title">advanced filters</div> <div class="buttons"> <div class="button noFilters">clear filters</div> </div> </div> <div class="buttonsAndTitle" style="grid-column: 1/3"> <div class="title">difficulty</div> <div class="buttons filterGroup" group="difficulty"> <div class="button" filter="normal">normal</div> <div class="button" filter="expert">expert</div> <div class="button" filter="master">master</div> </div> </div> <div class="buttonsAndTitle"> <div class="title">mode</div> <div class="buttons filterGroup" group="mode"> <div class="button" filter="words">words</div> <div class="button" filter="time">time</div> <div class="button" filter="quote">quote</div> <div class="button" filter="zen">zen</div> <div class="button" filter="custom">custom</div> </div> </div> <div class="buttonsAndTitle"> <div class="title">quote length</div> <div class="buttons filterGroup" group="quoteLength"> <div class="button" filter="short">short</div> <div class="button" filter="medium">medium</div> <div class="button" filter="long">long</div> <div class="button" filter="thicc">thicc</div> </div> </div> <div class="buttonsAndTitle"> <div class="title">words</div> <div class="buttons filterGroup" group="words"> <div class="button" filter="10">10</div> <div class="button" filter="25">25</div> <div class="button" filter="50">50</div> <div class="button" filter="100">100</div> <div class="button" filter="custom">custom</div> </div> </div> <div class="buttonsAndTitle"> <div class="title">time</div> <div class="buttons filterGroup" group="time"> <div class="button" filter="15">15</div> <div class="button" filter="30">30</div> <div class="button" filter="60">60</div> <div class="button" filter="120">120</div> <div class="button" filter="custom">custom</div> </div> </div> <div class="buttonsAndTitle"> <div class="title">punctuation</div> <div class="buttons filterGroup" group="punctuation"> <div class="button" filter="on">on</div> <div class="button" filter="off">off</div> </div> </div> <div class="buttonsAndTitle"> <div class="title">numbers</div> <div class="buttons filterGroup" group="numbers"> <div class="button" filter="on">on</div> <div class="button" filter="off">off</div> </div> </div> <div class="buttonsAndTitle tags" style="grid-column: 1/3"> <div class="title">tags</div> <div class="buttons filterGroup" group="tags"></div> </div> <div class="buttonsAndTitle languages" style="grid-column: 1/3"> <div class="title">language</div> <div class="buttons filterGroup" group="language"></div> </div> <div class="buttonsAndTitle funbox" style="grid-column: 1/3"> <div class="title">funbox</div> <div class="buttons filterGroup" group="funbox"></div> </div> </div> <div class="group noDataError hidden"> No data found. Check your filters. </div> <div class="group chart"> <!-- <div class="chartPreloader"> <i class="fas fa-fw fa-spin fa-circle-notch"></i> </div> --> <div class="above"></div> <div class="chart"> <canvas id="accountHistoryChart"></canvas> </div> <div class="below"> <div class="text"></div> <div class="buttons"> <div class="toggleAccuracyOnChart button"> <i class="fas fa-bullseye"></i> Toggle Accuracy </div> <div class="toggleChartStyle button"> <i class="fas fa-chart-line"></i> Toggle Chart Style </div> </div> </div> </div> <div class="group dailyActivityChart"> <div class="chart" style="height: 200px"> <canvas id="accountActivityChart"></canvas> </div> </div> <div class="triplegroup stats"> <div class="group testsStarted"> <div class="title">tests started</div> <div class="val">-</div> </div> <div class="group testsCompleted"> <div class="title"> tests completed <span data-balloon-length="xlarge" aria-label="Due to the increasing number of results in the database, you can now only see your last 1000 results in detail. Total time spent typing, started and completed tests stats will still be up to date at the top of the page, above the filters." data-balloon-pos="up" > <i class="fas fa-question-circle"></i> </span> </div> <div class="val">-</div> <div class="avgres">-</div> </div> <div class="group timeTotalFiltered"> <div class="title">time typing</div> <div class="val">-</div> </div> <div class="group highestWpm"> <div class="title">highest wpm</div> <div class="val">-</div> <div class="mode"></div> </div> <div class="group averageWpm"> <div class="title">average wpm</div> <div class="val">-</div> </div> <div class="group averageWpm10"> <div class="title"> average wpm <br /> (last 10 tests) </div> <div class="val">-</div> </div> <div class="group highestRaw"> <div class="title">highest raw wpm</div> <div class="val">-</div> <div class="mode"></div> </div> <div class="group averageRaw"> <div class="title">average raw wpm</div> <div class="val">-</div> </div> <div class="group averageRaw10"> <div class="title"> average raw wpm <br /> (last 10 tests) </div> <div class="val">-</div> </div> <div class="emptygroup"></div> <div class="group avgAcc"> <div class="title">avg accuracy</div> <div class="val">-</div> </div> <div class="group avgAcc10"> <div class="title"> avg accuracy <br /> (last 10 tests) </div> <div class="val">-</div> </div> <div class="emptygroup"></div> <div class="group avgCons"> <div class="title">avg consistency</div> <div class="val">-</div> </div> <div class="group avgCons10"> <div class="title"> avg consistency <br /> (last 10 tests) </div> <div class="val">-</div> </div> <!-- <div class="group favouriteTest"> <div class="title">favourite test</div> <div class="val">words 10</div> </div> --> </div> <div class="group history"> <!-- <div class="title">result history</div> --> <table width="100%"> <thead> <tr> <td></td> <td type="button" class="sortable history-wpm-header"> wpm </td> <td type="button" class="sortable history-raw-header"> raw </td> <td type="button" class="sortable history-acc-header"> accuracy </td> <td type="button" class="sortable history-consistency-header" > consistency </td> <td aria-label="correct/incorrect/extra/missed" data-balloon-pos="up" > chars </td> <td>mode</td> <!-- <td>punctuation</td> --> <td>info</td> <td>tags</td> <td type="button" class="sortable history-date-header"> date <i class="fas fa-sort-down" aria-hidden="true"></i> </td> </tr> </thead> <tbody></tbody> </table> <div class="loadMoreButton">load more</div> </div> </div> </div> </div> <div id="bottom"> <div id="commandLineMobileButton"> <i class="fas fa-terminal"></i> </div> <div class="keyTips"> <key>tab</key> and <key>enter</key> - Restart Test <br /> <key>ctrl/cmd</key> + <key>shift</key> + <key>p</key> or <key>esc</key> - Command Line </div> <div class="leftright"> <div class="left"> <a id="contactPopupButton"> <i class="fas fa-fw fa-envelope"></i> <div class="text">Contact</div> </a> <a href="https://github.com/Miodec/monkeytype" target="_blank" rel="noreferrer" > <i class="fas fa-fw fa-code"></i> <div class="text">GitHub</div> </a> <a href="https://www.discord.gg/monkeytype" target="_blank" rel="noreferrer" class="discordLink" > <i class="fab fa-fw fa-discord"></i> <div class="text">Discord</div> </a> <a href="https://twitter.com/monkeytypegame" target="_blank" rel="noreferrer" > <i class="fab fa-fw fa-twitter"></i> <div class="text">Twitter</div> </a> <a href="terms-of-service.html" target="_blank"> <i class="fas fa-fw fa-file-contract"></i> <div class="text">Terms</div> </a> <a href="security-policy.html" target="_blank"> <i class="fas fa-fw fa-shield-alt"></i> <div class="text">Security</div> </a> <a href="privacy-policy.html" target="_blank"> <i class="fas fa-fw fa-lock"></i> <div class="text">Privacy</div> </a> <a id="supportMeButton"> <i class="fas fa-fw fa-donate"></i> <div class="text">Donate</div> </a> </div> <div class="right"> <a class="current-theme" aria-label="Shift-click to toggle custom theme" data-balloon-pos="left" > <i class="fas fa-fw fa-palette"></i> <div class="text">serika dark</div> </a> <a class="version"> <i class="fas fa-fw fa-code-branch"></i> <div class="text">version</div> </a> <!-- <div> <i class="fas fa-file"></i> Terms & Conditions </div> --> </div> </div> </div> <div class="footerads"> <div id="ad_footer" style="display: flex; justify-content: center; justify-self: center" ></div> <div id="ad_footer2" class="hidden" style="display: flex; justify-content: center; justify-self: center" ></div> <!-- <div id="ad_footer3" class="hidden" style="display: flex; justify-content: center; justify-self: center" ></div> --> </div> </div> </div> </body> <!-- Global site tag (gtag.js) - Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-165993088-1" ></script> <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag("js", new Date()); gtag("config", "UA-165993088-1"); </script> <!-- The core Firebase JS SDK is always required and must be listed first --> <script src="/__/firebase/8.4.2/firebase-app.js"></script> <!-- TODO: Add SDKs for Firebase products that you want to use https://firebase.google.com/docs/web/setup#available-libraries --> <script src="/__/firebase/8.4.2/firebase-analytics.js"></script> <script src="/__/firebase/8.4.2/firebase-auth.js"></script> <!-- <script src="/__/firebase/8.4.2/firebase-firestore.js"></script> --> <!-- <script src="/__/firebase/8.4.2/firebase-functions.js"></script> --> <!-- Initialize Firebase --> <script src="/__/firebase/init.js"></script> <script src="js/jquery-3.5.1.min.js"></script> <script src="js/jquery.color.min.js"></script> <script src="js/easing.min.js"></script> <script src="js/jquery.cookie-1.4.1.min.js"></script> <script src="js/moment.min.js"></script> <script src="js/html2canvas.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js" async defer ></script> <script src="https://www.google.com/recaptcha/api.js" async defer></script> <script src="js/monkeytype.js"></script> </html> ==> ./monkeytype/static/privacy-policy.html <== <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Privacy Policy | Monkeytype</title> <!-- <link rel="stylesheet" href="css/fa.css" /> --> <link rel="stylesheet" href="css/balloon.css" /> <link rel="stylesheet" href="css/style.css" /> <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" /> <link rel="stylesheet" href="" id="funBoxTheme" /> <link id="favicon" rel="shortcut icon" href="images/fav.png" /> <link rel="shortcut icon" href="images/fav.png" /> <meta name="name" content="Monkeytype" /> <meta name="image" content="https://monkeytype.com/mtsocial.png" /> <meta name="description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta name="keywords" content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test" /> <meta name="author" content="Miodec" /> <meta property="og:title" content="Monkeytype" /> <meta property="og:url" content="https://monkeytype.com/" /> <meta property="og:type" content="website" /> <meta property="og:description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta property="og:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="theme-color" content="#e2b714" id="metaThemeColor" /> <meta name="twitter:title" content="Monkeytype" /> <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="twitter:card" content="summary_large_image" /> <style> #top { font-size: 2.5rem; } #middle { color: var(--text-color); } h1 { font-weight: unset; color: var(--main-color); font-size: 2rem; margin-top: 3rem; } body { justify-content: center; display: flex; } </style> </head> <body> <div id="centerContent"> <div id="top"> <div class="logo"> <div class="icon"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation: isolate" viewBox="-680 -1030 300 180" > <g> <path d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z" /> <path d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z " /> <path d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z" /> <path d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z " /> <path d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z " /> <path d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z" /> <path d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z " /> <path d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z " /> <path d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z " /> </g> </svg> </div> <div class="text"> <div class="top">monkey see</div> monkeytype <span style="color: var(--main-color)">Privacy Policy</span> </div> </div> </div> <div id="middle"> <p> <!-- make sure to update this date every time the policy is changed --> </p> <p>Effective date: September 8, 2021</p> <p> 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. </p> <p> 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. </p> <p> 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. </p> <p>Table of Contents</p> <!-- The last three internal links are redunant because the anchor stops at the same location but gives more context to the user when they are viewing the table of contents --> <ul> <li><a href="#Data_Collection">What data do we collect?</a></li> <li> <a href="#How_is_Data_Collected">How do we collect your data?</a> </li> <li><a href="#Data_Usage">How will we use your data?</a></li> <li><a href="#Data_Storage">How do we store your data?</a></li> <li> <a href="#Data_Protection_Rights"> What are your data protection rights? </a> </li> <li><a href="#Log_Data">What log data do we collect?</a></li> <li><a href="#Cookies">What are cookies?</a></li> <li><a href="#Usage_of_Cookies">How do we use cookies?</a></li> <li> <a href="#Types_of_Cookies_Used"> What types of cookies do we use? </a> </li> <li><a href="#Managing_Cookies">How to manage your cookies</a></li> <li> <a href="#External_Websites">Privacy policies of other websites</a> </li> <li> <a href="#Privacy_Policy_Modifications"> Changes to our privacy policy </a> </li> <li><a href="#Contact_Info">How to contact us</a></li> </ul> <h1 id="Data_Collection">What data do we collect?</h1> <p>Monkeytype collects the following data:</p> <ul> <li>Email</li> <li>Username</li> <li>Information about each typing test</li> <li>Your currently active settings</li> <li>How many typing tests you've started and completed</li> <li>How long you've been typing on the website</li> </ul> <h1 id="How_is_Data_Collected">How do we collect your data?</h1> <p> You directly provide most of the data we collect. We collect data and process data when you: </p> <ul> <li>Create an account</li> <li>Complete a typing test</li> <li>Change settings on the website</li> </ul> <h1 id="Data_Usage">How will we use your data?</h1> <p>Monkeytype collects your data so that we can:</p> <ul> <li> Allow you to view result history of previous tests you completed </li> <li> Save results from tests you take and show you statistics based on them </li> <li>Remember your settings</li> <li>Display leaderboards</li> </ul> <h1 id="Data_Storage">How do we store your data?</h1> <p>Monkeytype securely stores your data using Firebase Firestore.</p> <h1 id="Data_Protection_Rights"> What are your data protection rights? </h1> <p> Monkeytype would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following: </p> <ul> <li> The right to access – You have the right to request Monkeytype for copies of your personal data. We may limit the number of times this request can be made to depending on the size of the request. </li> <li> The right to rectification – You have the right to request that Monkeytype correct any information you believe is inaccurate. You also have the right to request Monkeytype to complete the information you believe is incomplete. </li> <li> The right to erasure – You have the right to request that Monkeytype erase your personal data, under certain conditions. </li> <li> The right to restrict processing – You have the right to request that Monkeytype restrict the processing of your personal data, under certain conditions. </li> <li> The right to object to processing – You have the right to object to Monkeytype processing of your personal data, under certain conditions. </li> <li> The right to data portability – You have the right to request that Monkeytype transfer the data that we have collected to another organization, or directly to you, under certain conditions. </li> </ul> <!-- <p>If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us at our email: jack@monkeytype.com</p> --> <h1 id="Log_Data">What log data do we collect?</h1> <p> 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. <b> THIS DATA DOES NOT CONTAIN ANY PERSONALLY IDENTIFIABLE INFORMATION. </b> We use this information for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information. </p> <p>In our case, this service is provided by Google Analytics.</p> <h1 id="Cookies">What are cookies?</h1> <p> 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 </p> <p> For further information, visit <a href="https://en.wikipedia.org/wiki/HTTP_cookie" target="_blank"> www.wikipedia.org/wiki/HTTP_cookie </a>. </p> <h1 id="Usage_of_Cookies">How do we use cookies?</h1> <p> Monkeytype uses cookies in a range of ways to improve your experience on our website, including: </p> <ul> <li>Keeping you signed in</li> <li>Remembering your active settings</li> <li>Remembering your active tags</li> </ul> <h1 id="Types_of_Cookies_Used">What types of cookies do we use?</h1> <p> 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. </p> <h1 id="Managing_Cookies">How to manage your cookies</h1> <p> 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. </p> <h1 id="External_Websites">Privacy policies of other websites</h1> <p> Monkeytype contains links to other external websites. <b> <u> Our privacy policy only applies to our website, so if you click on a link to another website, you should read their privacy policy. </u> </b> </p> <h1 id="Privacy_Policy_Modifications">Changes to our privacy policy</h1> <p> 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. </p> <!-- TODO: add way to view when file was last committed to using the GitHub api--> <h1 id="Contact_Info">How to contact us</h1> <p> 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. </p> <p> Email: <a href="mailto: jack@monkeytype.com" target="_blank" rel="noopener noreferrer" > jack@monkeytype.com </a> </p> Discord: <span aria-label="Click To Copy" data-balloon-pos="up" onclick="copyUserName()" > Miodec#1512 </span> </div> </div> <!-- TODO: Add image to go back to top of page --> </body> <script defer> // TODO: Add notification that appears when username copy is successful from notifications module function copyUserName() { if (true) { navigator.clipboard.writeText("Miodec#1512"); alert("Copied To Clipboard!"); } else { alert("Unable to copy username"); } } document.querySelector("#top").addEventListener("click", () => { window.location = "/"; }); </script> </html> ==> ./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 <== <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Email Handler | Monkeytype</title> <!-- <link rel="stylesheet" href="css/fa.css" /> --> <link rel="stylesheet" href="css/balloon.css" /> <link rel="stylesheet" href="css/style.css" /> <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" /> <link rel="stylesheet" href="" id="funBoxTheme" /> <link id="favicon" rel="shortcut icon" href="images/fav.png" /> <link rel="shortcut icon" href="images/fav.png" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <meta name="name" content="Monkeytype" /> <meta name="image" content="https://monkeytype.com/mtsocial.png" /> <meta name="description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta name="keywords" content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test" /> <meta name="author" content="Miodec" /> <meta property="og:title" content="Monkeytype" /> <meta property="og:url" content="https://monkeytype.com/" /> <meta property="og:type" content="website" /> <meta property="og:description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta property="og:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="theme-color" content="#e2b714" id="metaThemeColor" /> <meta name="twitter:title" content="Monkeytype" /> <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="twitter:card" content="summary_large_image" /> <style> #top { font-size: 2.5rem; } #middle { color: var(--text-color); display: grid; justify-content: center; } h1 { font-weight: unset; color: var(--main-color); font-size: 2rem; margin-top: 3rem; } body { justify-content: center; display: flex; } .preloader { text-align: center; display: grid; width: 300px; /* gap: 1rem; */ } .preloader .icon { font-size: 2rem; color: var(--main-color); margin-bottom: 1rem; } .preloader .subText { font-size: 1rem; color: var(--sub-color); font-style: italic; } .resetPassword { display: grid; width: 300px; gap: 1rem; } .hidden { display: none; } </style> </head> <body> <div id="centerContent"> <div id="top"> <div class="logo"> <div class="icon"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation: isolate" viewBox="-680 -1030 300 180" > <g> <path d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z" /> <path d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z " /> <path d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z" /> <path d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z " /> <path d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z " /> <path d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z" /> <path d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z " /> <path d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z " /> <path d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z " /> </g> </svg> </div> <div class="text"> <div class="top">monkey see</div> monkeytype <span style="color: var(--main-color)">Email Handler</span> </div> </div> </div> <div id="middle"> <div class="preloader"> <div class="icon"> <i class="fas fa-fw fa-spin fa-circle-notch"></i> </div> <div class="text"></div> <div class="subText"></div> </div> <div class="resetPassword hidden"> <input type="password" placeholder="New password" /> <div class="button">Change</div> </div> </div> </div> <!-- TODO: Add image to go back to top of page --> </body> <script src="js/jquery-3.5.1.min.js"></script> <script src="/__/firebase/8.4.2/firebase-app.js"></script> <!-- TODO: Add SDKs for Firebase products that you want to use https://firebase.google.com/docs/web/setup#available-libraries --> <script src="/__/firebase/8.4.2/firebase-analytics.js"></script> <script src="/__/firebase/8.4.2/firebase-auth.js"></script> <script src="/__/firebase/8.4.2/firebase-firestore.js"></script> <script src="/__/firebase/8.4.2/firebase-functions.js"></script> <!-- Initialize Firebase --> <script src="/__/firebase/init.js?useEmulator=true'"></script> <script defer> function handleVerifyEmail(actionCode, continueUrl) { firebase .auth() .applyActionCode(actionCode) .then((resp) => { // Email address has been verified. $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-check"></i>` ); $("#middle .preloader .text").text( `Your email address has been verified.` ); $("#middle .preloader .subText").text(`You can now close this tab.`); // TODO: Display a confirmation message to the user. // You could also provide the user with a link back to the app. // TODO: If a continue URL is available, display a button which on // click redirects the user back to the app via continueUrl with // additional state determined from that URL's parameters. }) .catch((error) => { $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-times"></i>` ); $("#middle .preloader .text").text(error.message); // Code is invalid or expired. Ask the user to verify their email address // again. }); } function showResetPassword() { $("#middle .preloader").addClass("hidden"); $("#middle .resetPassword").removeClass("hidden"); $("#middle .resetPassword input").focus(); } function hideResetPassword() { $("#middle .preloader").removeClass("hidden"); $("#middle .resetPassword").addClass("hidden"); } function handleResetPassword(actionCode, continueUrl) { // Verify the password reset code is valid. hideResetPassword(); firebase .auth() .verifyPasswordResetCode(actionCode) .then((email) => { var accountEmail = email; // TODO: Show the reset screen with the user's email and ask the user for // the new password. var newPassword = $("#middle .resetPassword input").val(); // Save the new password. firebase .auth() .confirmPasswordReset(actionCode, newPassword) .then((resp) => { // Password reset has been confirmed and new password updated. $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-check"></i>` ); $("#middle .preloader .text").text( `Your password has been changed.` ); $("#middle .preloader .subText").text( `You can now close this tab.` ); // TODO: Display a link back to the app, or sign-in the user directly // if the page belongs to the same domain as the app: firebase .auth() .signInWithEmailAndPassword(accountEmail, newPassword); // TODO: If a continue URL is available, display a button which on // click redirects the user back to the app via continueUrl with // additional state determined from that URL's parameters. }) .catch((error) => { $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-times"></i>` ); $("#middle .preloader .text").text(error.message); // Error occurred during confirmation. The code might have expired or the // password is too weak. }); }) .catch((error) => { $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-times"></i>` ); $("#middle .preloader .text").text(error.message); // $("#middle .preloader .subText").text(error); // Invalid or expired action code. Ask user to try to reset the password // again. }); } function getParameterByName(name, url = window.location.href) { name = name.replace(/[\[\]]/g, "\\$&"); var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url); if (!results) return null; if (!results[2]) return ""; return decodeURIComponent(results[2].replace(/\+/g, " ")); } document.addEventListener( "DOMContentLoaded", () => { try { // Get the action to complete. var mode = getParameterByName("mode"); // Get the one-time code from the query parameter. var actionCode = getParameterByName("oobCode"); // (Optional) Get the continue URL from the query parameter if available. var continueUrl = getParameterByName("continueUrl"); // (Optional) Get the language code if available. var lang = getParameterByName("lang") || "en"; // Configure the Firebase SDK. // // This is the minimum configuration required for the API to be used. // var config = { // 'apiKey': "YOU_API_KEY" // Copy this key from the web initialization // // snippet found in the Firebase console. // }; // var app = firebase.initializeApp(config); // var auth = firebase.auth(); if (!mode) { $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-times"></i>` ); $("#middle .preloader .text").text(`Mode parameter not found`); return; } if (!actionCode) { $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-times"></i>` ); $("#middle .preloader .text").text( `Action code parameter not found` ); return; } // Handle the user management action. switch (mode) { case "resetPassword": // Display reset password handler and UI. $(".logo .text span").text("Reset Password"); document.title = "Reset Password | Monkeytype"; showResetPassword(); break; case "recoverEmail": // Display email recovery handler and UI. handleRecoverEmail(actionCode); break; case "verifyEmail": $(".logo .text span").text("Verify Email"); document.title = "Verify Email | Monkeytype"; // Display email verification handler and UI. handleVerifyEmail(actionCode, continueUrl); break; default: $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-times"></i>` ); $("#middle .preloader .text").text(`Invalid mode`); console.error("no mode found"); // Error: invalid mode. } $("#middle .resetPassword .button").click(() => { handleResetPassword(actionCode, continueUrl); }); $("#middle .resetPassword input").keypress((e) => { if (e.key == "Enter") handleResetPassword(actionCode, continueUrl); }); } catch (e) { $("#middle .preloader .icon").html( `<i class="fas fa-fw fa-times"></i>` ); $("#middle .preloader .text").text( `Fatal error: ${e.message}. If this issue persists, please report it.` ); } }, false ); document.querySelector("#top").addEventListener("click", () => { window.location = "/"; }); </script> </html> ==> ./monkeytype/static/terms-of-service.html <== <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Terms of Service | Monkeytype</title> <!-- <link rel="stylesheet" href="css/fa.css" /> --> <link rel="stylesheet" href="css/balloon.css" /> <link rel="stylesheet" href="css/style.css" /> <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" /> <link rel="stylesheet" href="" id="funBoxTheme" /> <link id="favicon" rel="shortcut icon" href="images/fav.png" /> <link rel="shortcut icon" href="images/fav.png" /> <meta name="name" content="Monkeytype" /> <meta name="image" content="https://monkeytype.com/mtsocial.png" /> <meta name="description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta name="keywords" content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test" /> <meta name="author" content="Miodec" /> <meta property="og:title" content="Monkeytype" /> <meta property="og:url" content="https://monkeytype.com/" /> <meta property="og:type" content="website" /> <meta property="og:description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta property="og:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="theme-color" content="#e2b714" id="metaThemeColor" /> <meta name="twitter:title" content="Monkeytype" /> <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="twitter:card" content="summary_large_image" /> <style> #top { font-size: 2.5rem; } #middle { color: var(--text-color); } h1 { font-weight: unset; color: var(--main-color); font-size: 2rem; margin-top: 3rem; } body { justify-content: center; display: flex; } </style> </head> <body> <div id="centerContent"> <div id="top"> <div class="logo"> <div class="icon"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation: isolate" viewBox="-680 -1030 300 180" > <g> <path d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z" /> <path d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z " /> <path d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z" /> <path d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z " /> <path d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z " /> <path d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z" /> <path d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z " /> <path d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z " /> <path d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z " /> </g> </svg> </div> <div class="text"> <div class="top">monkey see</div> monkeytype <span style="color: var(--main-color)">Terms of Service</span> </div> </div> </div> <div id="middle"> <!-- make sure to update this date every time the tos is changed --> <p>These terms of service were last updated on September 11, 2021.</p> <h1>Agreement</h1> <p> 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. <strong> IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, YOU ARE NOT PERMITTED TO ACCESS OR USE OUR SERVICES. </strong> </p> <h1>Limitations</h1> <p> 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. </p> <p> 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. </p> <p> You agree that you will not upload, post, host, or transmit any content that: </p> <ol> <li>is unlawful or promotes unlawful activities;</li> <li>is or contains sexually obscene content;</li> <li>is libelous, defamatory, or fraudulent;</li> <li>is discriminatory or abusive toward any individual or group;</li> <li> 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; </li> <li> 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 </li> <li> 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. </li> </ol> <p>While using the Services, you agree that you will not:</p> <ol> <li> harass, abuse, threaten, or incite violence towards any individual or group, including other Users and Monkeytype contributors; </li> <li> 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; </li> <li> 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; </li> <li>access the Services in ways that exceed your authorization;</li> <li> 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; </li> <li> violate the privacy of any third party, such as by posting another person's personal information without their consent; </li> <li> 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; </li> <li> facilitate or encourage any violations of this Agreement or interfere with the operation, appearance, security, or functionality of the Services; or </li> <li>use the Services in any manner that is harmful to minors.</li> </ol> <p> 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. </p> <h1>Privacy Policy</h1> If you use our Services, you must abide by our Privacy Policy. You acknowledge that you have read our <a href="https://monkeytype.com/privacy-policy" target="_blank"> Privacy Policy </a> 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. <h1>Limitations on Automated Use</h1> You shouldn't use bots or access our Services in malicious or un-permitted ways. While accessing or using the Services, you may not: <ol> <li>use bots, hacks, or cheats while using our site;</li> <li>create manual requests to Monkeytype servers;</li> <li> tamper with or use non-public areas of the Services, or the computer or delivery systems of Monkeytype and/or its service providers; </li> <li> 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; </li> <li> 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; </li> <li> 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; </li> <li> 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 </li> <li> 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. </li> </ol> <h1>Links</h1> Monkeytype is not responsible for the contents of any linked sites. The use of any linked website is at the user's own risk. <h1>Changes</h1> 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. <h1>Disclaimer</h1> <p> 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. </p> <h1>Contact</h1> <p> 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. </p> <p> Email: <a href="mailto:jack@monkeytype.com" target="_blank" rel="noopener noreferrer" > jack@monkeytype.com </a> </p> Discord: <span aria-label="Click To Copy" data-balloon-pos="up" onclick="copyUserName()" > Miodec#1512 </span> </div> <p> Terms based on <a href="https://glitch.com/legal" target="_blank">Glitch terms</a> </p> </div> <!-- TODO: Add button to go back to top of page --> </body> <script defer> // TODO: Add notification that appears when username copy is successful from notifications module function copyUserName() { if (true) { navigator.clipboard.writeText("Miodec#1512"); alert("Copied To Clipboard!"); } else { alert("Unable to copy username"); } } document.querySelector("#top").addEventListener("click", () => { window.location = "/"; }); </script> </html> ==> ./monkeytype/static/robots.txt <== User-agent: * Disallow: ==> ./monkeytype/static/security-policy.html <== <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Security Policy | Monkeytype</title> <!-- <link rel="stylesheet" href="css/fa.css" /> --> <link rel="stylesheet" href="css/balloon.css" /> <link rel="stylesheet" href="css/style.css" /> <link rel="stylesheet" href="themes/serika_dark.css" id="currentTheme" /> <link rel="stylesheet" href="" id="funBoxTheme" /> <link id="favicon" rel="shortcut icon" href="images/fav.png" /> <link rel="shortcut icon" href="images/fav.png" /> <meta name="name" content="Monkeytype" /> <meta name="image" content="https://monkeytype.com/mtsocial.png" /> <meta name="description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta name="keywords" content="typing, test, typing-test, typing test, monkey-type, monkeytype, monkey type, monkey-types, monkeytypes, monkey types, types, monkey, type, miodec, wpm, words per minute, typing website, minimalistic, custom typing test, customizable, customisable, themes, random words, smooth caret, smooth, new, new typing site, new typing website, minimalist typing website, minimalistic typing website, minimalist typing test" /> <meta name="author" content="Miodec" /> <meta property="og:title" content="Monkeytype" /> <meta property="og:url" content="https://monkeytype.com/" /> <meta property="og:type" content="website" /> <meta property="og:description" content="A minimalistic, customisable typing website. Test yourself in various modes, track your progress and improve your typing speed." /> <meta property="og:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="theme-color" content="#e2b714" id="metaThemeColor" /> <meta name="twitter:title" content="Monkeytype" /> <meta name="twitter:image" content="https://monkeytype.com/mtsocial.png" /> <meta name="twitter:card" content="summary_large_image" /> <style> #top { font-size: 2.5rem; } #middle { color: var(--text-color); } #centerContent { align-items: flex-start; } h1 { font-weight: unset; color: var(--main-color); font-size: 2rem; margin-top: 3rem; } body { justify-content: center; display: flex; } </style> </head> <body> <div id="centerContent"> <div id="top"> <div class="logo"> <div class="icon"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation: isolate" viewBox="-680 -1030 300 180" > <g> <path d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z" /> <path d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z " /> <path d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z" /> <path d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z " /> <path d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z " /> <path d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z" /> <path d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z " /> <path d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z " /> <path d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z " /> </g> </svg> </div> <div class="text"> <div class="top">monkey see</div> monkeytype <span style="color: var(--main-color)">Security Policy</span> </div> </div> </div> <div id="middle"> <p> We take the security and integrity of Monkeytype very seriously. If you have found a vulnerability, please report it <abbr title="As Soon As Possible">ASAP</abbr> so we can quickly remediate the issue. </p> <p>Table of Contents</p> <!-- The last three internal links are redunant but give more context to the user when viewing the table of contents --> <ul> <li> <a href="#Vulnerability_Disclosure"> How to Disclose a Vulnerability </a> </li> <li><a href="#Submission_Guidelines">Submission Guidelines</a></li> </ul> <h1 id="Vulnerability_Disclosure">How to Disclose a Vulnerability</h1> <p> For vulnerabilities that impact the confidentiality, integrity, and availability of Monkeytype services, please send your disclosure via (1) <a href="mailto:jack@monkeytype.com">email</a> , or (2) ping <span aria-label="Click To Copy" data-balloon-pos="up" onclick="copyUserName()" > Miodec#1512 </span> on the <a href="https://www.discord.gg/monkeytype"> Monkeytype Discord server in the #development channel </a> and he can discuss the situation with you further in private. For non-security related platform bugs, follow the bug submission <a href="https://github.com/Miodec/monkeytype#bug-report-or-feature-request" > guidelines </a> . Include as much detail as possible to ensure reproducibility. At a minimum, vulnerability disclosures should include: </p> <ul> <li>Vulnerability Description</li> <li>Proof of Concept</li> <li>Impact</li> <li>Screenshots or Proof</li> </ul> <h1 id="Submission_Guidelines">Submission Guidelines</h1> <p> 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. </p> </div> </div> <!-- TODO: Add image to go back to top of page --> </body> <script defer> // TODO: Add notification that appears when username copy is successful from notifications module function copyUserName() { if (true) { navigator.clipboard.writeText("Miodec#1512"); alert("Copied To Clipboard!"); } else { alert("Unable to copy username"); } } document.querySelector("#top").addEventListener("click", () => { window.location = "/"; }); </script> </html> ==> ./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(` <div class="title"></div> <div class="inputs"></div> <div class="text"></div> <div class="button"></div>`); } 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(` <input type="number" min="1" val="${input.initVal}" placeholder="${ input.placeholder }" class="${input.hidden ? "hidden" : ""}" ${ input.hidden ? "" : "required" } autocomplete="off"> `); }); } else if (this.type === "text") { this.inputs.forEach((input) => { if (input.type) { el.find(".inputs").append(` <input type="${input.type}" val="${input.initVal}" placeholder="${ input.placeholder }" class="${input.hidden ? "hidden" : ""}" ${ input.hidden ? "" : "required" } autocomplete="off"> `); } else { el.find(".inputs").append(` <input type="text" val="${input.initVal}" placeholder="${ input.placeholder }" class="${input.hidden ? "hidden" : ""}" ${ input.hidden ? "" : "required" } autocomplete="off"> `); } }); } 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( `<div class="language button" language='${language}'>${language.replace( /_/g, " " )}</div>` ); }); 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( `<div class="theme button ${activeTheme}" theme='${ theme.name }' style="color:${theme.mainColor};background:${theme.bgColor}"> <div class="activeIndicator"><i class="fas fa-circle"></i></div> <div class="text">${theme.name.replace(/_/g, " ")}</div> <div class="favButton active"><i class="fas fa-star"></i></div></div>` ); } }); } 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( `<div class="theme button ${activeTheme}" theme='${ theme.name }' style="color:${theme.mainColor};background:${theme.bgColor}"> <div class="activeIndicator"><i class="fas fa-circle"></i></div> <div class="text">${theme.name.replace(/_/g, " ")}</div> <div class="favButton"><i class="far fa-star"></i></div></div>` ); } }); 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 ( <key>tab</key> )`); $("#bottom .keyTips").html(` <key>esc</key> - restart test<br> <key>tab</key> - command line`); } else { $(".pageSettings .tip").html(` tip: You can also change all these settings quickly using the command line ( <key>esc</key> )`); $("#bottom .keyTips").html(` <key>tab</key> - restart test<br> <key>esc</key> or <key>ctrl/cmd</key>+<key>shift</key>+<key>p</key> - 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( `<div class="devIndicator tr">DEV</div><div class="devIndicator bl">DEV</div>` ); } */ 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( `<div class="devIndicator tl">local</div><div class="devIndicator br">local</div>` ); $(".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 = `<div class="textIcon">${icon}</div>`; } else { icon = `<i class="fas fa-fw ${icon}"></i>`; } if (list.configKey) { if ( (obj.configValueMode && obj.configValueMode === "include" && Config[list.configKey].includes(obj.configValue)) || Config[list.configKey] === obj.configValue ) { icon = `<i class="fas fa-fw fa-check"></i>`; } else { icon = `<i class="fas fa-fw"></i>`; } } let iconHTML = `<div class="icon">${icon}</div>`; if (obj.noIcon && !isSingleListCommandLineActive()) { iconHTML = ""; } commandsHTML += `<div class="entry" command="${obj.id}">${iconHTML}<div>${obj.display}</div></div>`; } }); $("#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("</i>")[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 = `<i class="fas fa-fw"></i>`; if ( commandItem.configValue !== undefined && Config[parentCommand.configKey] === commandItem.configValue ) { icon = `<i class="fas fa-fw fa-check"></i>`; } 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('<div class="word active"></div>'); // $("#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 = `<div class='word'>`; for (let c = 0; c < word.length; c++) { if (Config.funbox === "arrows") { if (word.charAt(c) === "↑") { retval += `<letter><i class="fas fa-arrow-up"></i></letter>`; } if (word.charAt(c) === "↓") { retval += `<letter><i class="fas fa-arrow-down"></i></letter>`; } if (word.charAt(c) === "â†") { retval += `<letter><i class="fas fa-arrow-left"></i></letter>`; } if (word.charAt(c) === "→") { retval += `<letter><i class="fas fa-arrow-right"></i></letter>`; } } else if (word.charAt(c) === "t") { retval += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`; } else if (word.charAt(c) === "\n") { newlineafter = true; retval += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`; } else { retval += "<letter>" + word.charAt(c) + "</letter>"; } } retval += "</div>"; if (newlineafter) retval += "<div class='newline'></div>"; 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 = '<div class="word">word height</div><div class="word active"></div>'; } $("#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 += `<letter class='tabChar correct' style="opacity: 0"><i class="fas fa-long-arrow-alt-right"></i></letter>`; } else if (TestLogic.input.current[i] === "\n") { newlineafter = true; ret += `<letter class='nlChar correct' style="opacity: 0"><i class="fas fa-angle-down"></i></letter>`; } else { ret += `<letter class="correct">${TestLogic.input.current[i]}</letter>`; } } } 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 = `<i class="fas fa-arrow-up"></i>`; } if (currentLetter === "↓") { currentLetter = `<i class="fas fa-arrow-down"></i>`; } if (currentLetter === "â†") { currentLetter = `<i class="fas fa-arrow-left"></i>`; } if (currentLetter === "→") { currentLetter = `<i class="fas fa-arrow-right"></i>`; } } else if (currentLetter === "t") { tabChar = "tabChar"; currentLetter = `<i class="fas fa-long-arrow-alt-right"></i>`; } else if (currentLetter === "\n") { nlChar = "nlChar"; currentLetter = `<i class="fas fa-angle-down"></i>`; } if ( Misc.trailingComposeChars.test(input) && i > input.search(Misc.trailingComposeChars) ) continue; if (charCorrect) { ret += `<letter class="${ Config.highlightMode == "word" ? wordHighlightClassString : correctClass } ${tabChar}${nlChar}">${currentLetter}</letter>`; } else if ( currentLetter !== undefined && Misc.trailingComposeChars.test(input) && i === input.search(Misc.trailingComposeChars) ) { ret += `<letter class="${ Config.highlightMode == "word" ? wordHighlightClassString : "" } dead">${currentLetter}</letter>`; } else if (!showError) { if (currentLetter !== undefined) { ret += `<letter class="${ Config.highlightMode == "word" ? wordHighlightClassString : correctClass } ${tabChar}${nlChar}">${currentLetter}</letter>`; } } else if (currentLetter === undefined) { if (!Config.hideExtraLetters) { let letter = input[i]; if (letter == " " || letter == "t" || letter == "\n") { letter = "_"; } ret += `<letter class="${ Config.highlightMode == "word" ? wordHighlightClassString : "incorrect" } extra ${tabChar}${nlChar}">${letter}</letter>`; } } else { ret += `<letter class="${ Config.highlightMode == "word" ? wordHighlightClassString : "incorrect" } ${tabChar}${nlChar}">` + currentLetter + (Config.indicateTypos ? `<hint>${input[i]}</hint>` : "") + "</letter>"; } } 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 += `<letter><i class="fas fa-arrow-up"></i></letter>`; } if (currentWord[i] === "↓") { ret += `<letter><i class="fas fa-arrow-down"></i></letter>`; } if (currentWord[i] === "â†") { ret += `<letter><i class="fas fa-arrow-left"></i></letter>`; } if (currentWord[i] === "→") { ret += `<letter><i class="fas fa-arrow-right"></i></letter>`; } } else if (currentWord[i] === "t") { ret += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`; } else if (currentWord[i] === "\n") { ret += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`; } else { ret += `<letter class="${ Config.highlightMode == "word" ? wordHighlightClassString : "" }">` + currentWord[i] + "</letter>"; } } } 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("<div class='newline'></div>"); } 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( `<div class="smoothScroller" style="position: fixed;height:${wordHeight}px;width:100%"></div>` ); $("#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( `<div class="text-button restart" style="color:var(--error-color);"><i class="fas fa-sync-alt"></i>repeated</div>` ); } if (TestLogic.hasTab) { $(".pageTest #testModesNotice").append( `<div class="text-button"><i class="fas fa-long-arrow-alt-right"></i>shift + tab to restart</div>` ); } if (ChallengeController.active) { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsChallenges"><i class="fas fa-award"></i>${ChallengeController.active.display}</div>` ); } if (Config.mode === "zen") { $(".pageTest #testModesNotice").append( `<div class="text-button"><i class="fas fa-poll"></i>shift + enter to finish zen </div>` ); } // /^[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( `<div class="text-button" commands="commandsLanguages"><i class="fas fa-globe-americas"></i>${Config.language.replace( /_/g, " " )}</div>` ); } if (Config.difficulty === "expert") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsDifficulty"><i class="fas fa-star-half-alt"></i>expert</div>` ); } else if (Config.difficulty === "master") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsDifficulty"><i class="fas fa-star"></i>master</div>` ); } if (Config.blindMode) { $(".pageTest #testModesNotice").append( `<div class="text-button blind"><i class="fas fa-eye-slash"></i>blind</div>` ); } if (Config.lazyMode) { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsLazyMode"><i class="fas fa-couch"></i>lazy</div>` ); } if ( Config.paceCaret !== "off" || (Config.repeatedPace && TestLogic.isPaceRepeat) ) { let speed = ""; try { speed = ` (${Math.round(PaceCaret.settings.wpm)} wpm)`; } catch {} $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsPaceCaret"><i class="fas fa-tachometer-alt"></i>${ Config.paceCaret === "average" ? "average" : Config.paceCaret === "pb" ? "pb" : "custom" } pace${speed}</div>` ); } if (Config.minWpm !== "off") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsMinWpm"><i class="fas fa-bomb"></i>min ${Config.minWpmCustomSpeed} wpm</div>` ); } if (Config.minAcc !== "off") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsMinAcc"><i class="fas fa-bomb"></i>min ${Config.minAccCustom}% acc</div>` ); } if (Config.minBurst !== "off") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsMinBurst"><i class="fas fa-bomb"></i>min ${ Config.minBurstCustomSpeed } burst ${Config.minBurst === "flex" ? "(flex)" : ""}</div>` ); } if (Config.funbox !== "none") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsFunbox"><i class="fas fa-gamepad"></i>${Config.funbox.replace( /_/g, " " )}</div>` ); } if (Config.confidenceMode === "on") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsConfidenceMode"><i class="fas fa-backspace"></i>confidence</div>` ); } if (Config.confidenceMode === "max") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsConfidenceMode"><i class="fas fa-backspace"></i>max confidence</div>` ); } if (Config.stopOnError != "off") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsStopOnError"><i class="fas fa-hand-paper"></i>stop on ${Config.stopOnError}</div>` ); } if (Config.layout !== "default") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsLayouts"><i class="fas fa-keyboard"></i>emulating ${Config.layout.replace( /_/g, " " )}</div>` ); } if (Config.oppositeShiftMode !== "off") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsOppositeShiftMode"><i class="fas fa-exchange-alt"></i>opposite shift${ Config.oppositeShiftMode === "keymap" ? " (keymap)" : "" }</div>` ); } let tagsString = ""; try { DB.getSnapshot().tags.forEach((tag) => { if (tag.active === true) { tagsString += tag.name + ", "; } }); if (tagsString !== "") { $(".pageTest #testModesNotice").append( `<div class="text-button" commands="commandsTags"><i class="fas fa-tag"></i>${tagsString.substring( 0, tagsString.length - 2 )}</div>` ); } } 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 = `<div class='word' burst="${ TestStats.burstHistory[i] }" input="${TestLogic.corrected .getHistory(i) .replace(/"/g, """) .replace(/ /g, "_")}">`; } else { wordEl = `<div class='word' burst="${ TestStats.burstHistory[i] }" input="${input.replace(/"/g, """).replace(/ /g, "_")}">`; } 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 = `<div class='word error' burst="${ TestStats.burstHistory[i] }" input="${input.replace(/"/g, """).replace(/ /g, "_")}">`; } } } else { if (Config.mode != "zen" && input !== word) { wordEl = `<div class='word error' burst="${ TestStats.burstHistory[i] }" input="${input.replace(/"/g, """).replace(/ /g, "_")}">`; } } 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 += `<letter class="correct ${extraCorrected}">${input[c]}</letter>`; } else { wordEl += `<letter class="corrected ${extraCorrected}">` + input[c] + "</letter>"; } } else { if (input[c] === TestLogic.input.current) { wordEl += `<letter class='correct ${extraCorrected}'>` + word[c] + "</letter>"; } else if (input[c] === undefined) { wordEl += "<letter>" + word[c] + "</letter>"; } else { wordEl += `<letter class="incorrect ${extraCorrected}">` + word[c] + "</letter>"; } } } else { wordEl += '<letter class="incorrect extra">' + input[c] + "</letter>"; } } wordEl += "</div>"; } catch (e) { try { wordEl = "<div class='word'>"; for (let c = 0; c < word.length; c++) { wordEl += "<letter>" + word[c] + "</letter>"; } wordEl += "</div>"; } 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( `<div class="preloader"><i class="fas fa-fw fa-spin fa-circle-notch"></i></div>` ); 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( `<div class="wordInputAfter"> <div class="text"> ${input .replace(/t/g, "_") .replace(/\n/g, "_") .replace(/</g, "<") .replace(/>/g, ">")} </div> <div class="speed"> ${Math.round(Config.alwaysShowCPM ? burst * 5 : burst)}${ Config.alwaysShowCPM ? "cpm" : "wpm" } </div> </div>` ); } }); $(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><p>+/g, " "); sectionText = $("<div/>").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 = "<div>" + displayTime + "</div>"; } 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 = "<div>" + `${TestLogic.input.history.length}` + "</div>"; } else { timerNumberElement.innerHTML = "<div>" + `${TestLogic.input.history.length}/${outof}` + "</div>"; } } 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 = "<div>" + `${TestLogic.input.history.length}` + "</div>"; } 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(` <div tagid="${tag._id}" aria-label="PB: ${tpb}" data-balloon-pos="up">${tag.name}<i class="fas fa-crown hidden"></i></div> `); 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 += "<br>" + result.language.replace(/_/g, " "); } if (Config.punctuation) { testType += "<br>punctuation"; } if (Config.numbers) { testType += "<br>numbers"; } if (Config.blindMode) { testType += "<br>blind"; } if (Config.lazyMode) { testType += "<br>lazy"; } if (Config.funbox !== "none") { testType += "<br>" + Config.funbox.replace(/_/g, " "); } if (Config.difficulty == "expert") { testType += "<br>expert"; } else if (Config.difficulty == "master") { testType += "<br>master"; } $("#result .stats .testType .bottom").html(testType); } function updateOther( difficultyFailed, failReason, afkDetected, isRepeated, tooShort ) { let otherText = ""; if (difficultyFailed) { otherText += `<br>failed (${failReason})`; } if (afkDetected) { otherText += "<br>afk detected"; } if (TestStats.invalid) { otherText += "<br>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 += "<br>repeated"; } if (result.bailedOut) { otherText += "<br>bailed out"; } if (tooShort) { otherText += "<br>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(` <div class='noStressMessage' style=" text-align: center; grid-column: 1/3; font-size: 2rem; padding-bottom: 2rem; "> <i class="fas fa-check"></i> </div> `); $("#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, }; }