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

- -

- -

Effective date: September 8, 2021

-

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

-

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

-

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

-

Table of Contents

- - - -

What data do we collect?

-

Monkeytype collects the following data:

- - -

How do we collect your data?

- -

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

- - -

How will we use your data?

-

Monkeytype collects your data so that we can:

- - - -

How do we store your data?

-

Monkeytype securely stores your data using Firebase Firestore.

- -

- What are your data protection rights? -

-

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

- - - -

What log data do we collect?

-

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

-

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

- -

What are cookies?

-

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

-

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

- -

How do we use cookies?

-

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

- -

What types of cookies do we use?

-

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

-

How to manage your cookies

-

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

-

Privacy policies of other websites

-

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

- -

Changes to our privacy policy

-

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

- - -

How to contact us

-

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

-

- Email: - - jack@monkeytype.com - -

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

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

-

Agreement

-

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

- -

Limitations

-

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

-

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

-

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

-
    -
  1. is unlawful or promotes unlawful activities;
  2. -
  3. is or contains sexually obscene content;
  4. -
  5. is libelous, defamatory, or fraudulent;
  6. -
  7. is discriminatory or abusive toward any individual or group;
  8. -
  9. - 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; -
  10. -
  11. - 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 -
  12. -
  13. - 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. -
  14. -
- -

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

-
    -
  1. - harass, abuse, threaten, or incite violence towards any individual - or group, including other Users and Monkeytype contributors; -
  2. -
  3. - 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; -
  4. -
  5. - 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; -
  6. -
  7. access the Services in ways that exceed your authorization;
  8. -
  9. - 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; -
  10. -
  11. - violate the privacy of any third party, such as by posting another - person's personal information without their consent; -
  12. -
  13. - 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; -
  14. -
  15. - facilitate or encourage any violations of this Agreement or - interfere with the operation, appearance, security, or functionality - of the Services; or -
  16. -
  17. use the Services in any manner that is harmful to minors.
  18. -
-

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

-

Privacy Policy

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

Limitations on Automated Use

- You shouldn't use bots or access our Services in malicious or - un-permitted ways. While accessing or using the Services, you may not: -
    -
  1. use bots, hacks, or cheats while using our site;
  2. -
  3. create manual requests to Monkeytype servers;
  4. -
  5. - tamper with or use non-public areas of the Services, or the computer - or delivery systems of Monkeytype and/or its service providers; -
  6. -
  7. - 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; -
  8. -
  9. - 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; -
  10. -
  11. - 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; -
  12. -
  13. - 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 -
  14. -
  15. - 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. -
  16. -
-

Links

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

Changes

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

Disclaimer

-

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

-

Contact

-

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

-

- Email: - - jack@monkeytype.com - -

- Discord: - - Miodec#1512 - -
-

- Terms based on - Glitch terms -

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

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

-

Table of Contents

- - - -

How to Disclose a Vulnerability

-

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

- - -

Submission Guidelines

-

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

-
-
- - - - - -==> ./monkeytype/static/themes/terminal.css <== -:root { - --bg-color: #191a1b; - --caret-color: #79a617; - --main-color: #79a617; - --sub-color: #48494b; - --text-color: #e7eae0; - --error-color: #a61717; - --error-extra-color: #731010; - --colorful-error-color: #a61717; - --colorful-error-extra-color: #731010; -} - -==> ./monkeytype/static/themes/ms_cupcakes.css <== -:root { - --bg-color: #ffffff; - --main-color: #5ed5f3; - --caret-color: #303030; - --sub-color: #d64090; - --text-color: #0a282f; - --error-color: #000000; - --error-extra-color: #c9c9c9; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/fledgling.css <== -:root { - --bg-color: #3b363f; - --main-color: #fc6e83; - --caret-color: #474747; - --sub-color: #ead8d6; - --text-color: #fc6e83; - --error-color: #f52443; - --error-extra-color: #bd001c; - --colorful-error-color: #ff0a2f; - --colorful-error-extra-color: #000000; -} - -==> ./monkeytype/static/themes/horizon.css <== -:root { - --bg-color: #1C1E26; - --main-color:#c4a88a; - --caret-color: #BBBBBB; - --sub-color: #db886f; - --text-color: #bbbbbb; - --error-color: #D55170; - --error-extra-color: #ff3d3d; - --colorful-error-color: #D55170; - --colorful-error-extra-color:#D55170; -} - -#menu .icon-button:nth-child(1) { - color: #D55170; -} - -#menu .icon-button:nth-child(2) { - color: #E4A88A; -} - -#menu .icon-button:nth-child(3) { - color: #DB886F; -} - -#menu .icon-button:nth-child(4) { - color: #DB887A; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #FFC819; -} - -==> ./monkeytype/static/themes/vaporwave.css <== -:root { - --bg-color: #a4a7ea; - --main-color: #e368da; - --caret-color: #28cafe; - --sub-color: #7c7faf; - --text-color: #f1ebf1; - --error-color: #573ca9; - --error-extra-color: #3d2b77; - --colorful-error-color: #28cafe; - --colorful-error-extra-color: #25a9ce; -} - -==> ./monkeytype/static/themes/witch_girl.css <== -:root { - --bg-color: #f3dbda; - --main-color: #56786a; - --caret-color: #afc5bd; - --sub-color: #ddb4a7; - --text-color: #56786a; - --error-color: #b29a91; - --error-extra-color: #b29a91; - --colorful-error-color: #b29a91; - --colorful-error-extra-color: #b29a91; -} - -==> ./monkeytype/static/themes/gruvbox_light.css <== -:root { - --bg-color: #fbf1c7; - --main-color: #689d6a; - --caret-color: #689d6a; - --sub-color: #a89984; - --text-color: #3c3836; - --error-color: #cc241d; - --error-extra-color: #9d0006; - --colorful-error-color: #cc241d; - --colorful-error-extra-color: #9d0006; -} - -==> ./monkeytype/static/themes/soaring_skies.css <== -:root { - --bg-color: #fff9f2; - --main-color: #55c6f0; - --caret-color: #1e107a; - --sub-color: #1e107a; - --text-color: #1d1e1e; - --error-color: #fb5745; - --error-extra-color: #b03c30; - --colorful-error-color: #fb5745; - --colorful-error-extra-color: #b03c30; -} - -==> ./monkeytype/static/themes/terra.css <== -:root { - --bg-color: #0c100e; - --main-color: #89c559; - --caret-color: #89c559; - --sub-color: #436029; - --text-color: #f0edd1; - --error-color: #d3ca78; - --error-extra-color: #89844d; - --colorful-error-color: #d3ca78; - --colorful-error-extra-color: #89844d; -} - -==> ./monkeytype/static/themes/camping.css <== -:root { - --bg-color: #faf1e4; - --main-color: #618c56; - --caret-color: #618c56; - --sub-color: #c2b8aa; - --text-color: #3c403b; - --error-color: #ad4f4e; - --error-extra-color: #7e3a39; - --colorful-error-color: #ad4f4e; - --colorful-error-extra-color: #7e3a39; -} - -#top .logo .bottom { - color: #ad4f4e; -} - -==> ./monkeytype/static/themes/lil_dragon.css <== -:root { - --bg-color: #ebe1ef; - --main-color: #8a5bd6; - --caret-color: #212b43; - --sub-color: #ac76e5; - --text-color: #212b43; - --error-color: #f794ca; - --error-extra-color: #f279c2; - --colorful-error-color: #f794ca; - --colorful-error-extra-color: #f279c2; -} - -#menu .icon-button { - color: #ba96db; -} - -#menu .icon-button:hover { - color: #212b43; -} - -==> ./monkeytype/static/themes/laser.css <== -:root { - --bg-color: #221b44; - --main-color: #009eaf; - --caret-color: #009eaf; - --sub-color: #b82356; - --text-color: #dbe7e8; - --error-color: #a8d400; - --error-extra-color: #668000; - --colorful-error-color: #a8d400; - --colorful-error-extra-color: #668000; -} - -==> ./monkeytype/static/themes/desert_oasis.css <== -:root { - --bg-color: #fff2d5; /*Background*/ - --main-color: #d19d01; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ - --caret-color: #3a87fe; /*Cursor Color*/ - --sub-color: #0061fe; /*WPM text color of scrollbar and general color, before typed color*/ - --text-color: #332800; /*Color of text after hovering over it*/ - --error-color: #76bb40; - --error-extra-color: #4e7a27; - --colorful-error-color: #76bb40; - --colorful-error-extra-color: #4e7a27; -} - -#menu .icon-button:nth-child(1) { - color: #76bb40; -} - -#menu .icon-button:nth-child(2) { - color: #76bb40; -} - -#menu .icon-button:nth-child(3) { - color: #76bb40; -} - -#menu .icon-button:nth-child(4) { - color: #76bb40; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #76bb40; -} - -==> ./monkeytype/static/themes/honey.css <== -:root { - --bg-color: #f2aa00; - --main-color: #fff546; - --caret-color: #795200; - --sub-color: #a66b00; - --text-color: #f3eecb; - --error-color: #df3333; - --error-extra-color: #6d1f1f; - --colorful-error-color: #df3333; - --colorful-error-extra-color: #6d1f1f; -} - -==> ./monkeytype/static/themes/9009.css <== -:root { - --bg-color: #eeebe2; - --main-color: #080909; - --caret-color: #7fa480; - --sub-color: #99947f; - --text-color: #080909; - --error-color: #c87e74; - --colorful-error-color: #a56961; - --colorful-error-color: #c87e74; - --colorful-error-extra-color: #a56961; -} - -.word letter.incorrect { - color: var(--error-color); -} - -.word letter.incorrect.extra { - color: var(--colorful-error-color); -} - -.word.error { - border-bottom: solid 2px var(--error-color); -} - -key { - color: var(--sub-color); - background-color: var(--main-color); -} - -#menu .icon-button { - color: var(--main-color); -} - -#menu .icon-button:nth-child(1) { - color: var(--error-color); -} - -#menu .icon-button:nth-child(4) { - color: var(--caret-color); -} - -==> ./monkeytype/static/themes/sweden.css <== -:root { - --bg-color: #0058a3; - --main-color: #ffcc02; - --caret-color: #b5b5b5; - --sub-color: #57abdb; - --text-color: #ffffff; - --error-color: #e74040; - --error-extra-color: #a22f2f; - --colorful-error-color: #f56674; - --colorful-error-extra-color: #e33546; -} - -==> ./monkeytype/static/themes/solarized_dark.css <== -:root { - --bg-color: #002b36; - --main-color: #859900; - --caret-color: #dc322f; - --sub-color: #2aa198; - --text-color: #268bd2; - --error-color: #d33682; - --error-extra-color: #9b225c; - --colorful-error-color: #d33682; - --colorful-error-extra-color: #9b225c; -} - -==> ./monkeytype/static/themes/mizu.css <== -:root { - --bg-color: #afcbdd; - --main-color: #fcfbf6; - --caret-color: #fcfbf6; - --sub-color: #85a5bb; - --text-color: #1a2633; - --error-color: #bf616a; - --error-extra-color: #793e44; - --colorful-error-color: #bf616a; - --colorful-error-extra-color: #793e44; -} - -==> ./monkeytype/static/themes/terror_below.css <== -:root { - --bg-color: #0b1e1a; - --caret-color: #66ac92; - --main-color: #66ac92; - --sub-color: #015c53; - --text-color: #dceae5; - --error-color: #bf616a; - --error-extra-color: #793e44; - --colorful-error-color: #bf616a; - --colorful-error-extra-color: #793e44; -} - -==> ./monkeytype/static/themes/bingsu.css <== -:root { - /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */ - --bg-color: #b8a7aa; - --main-color: #83616e; - --caret-color: #ebe6ea; - --sub-color: #48373d; - --text-color: #ebe6ea; - --error-color: #921341; - --error-extra-color: #640b2c; - --colorful-error-color: #921341; - --colorful-error-extra-color: #640b2c; -} - -/* .word.error{ - border-bottom: double 4px var(--error-color); -} */ - -#menu .icon-button:nth-child(1) { - color: var(--caret-color); -} - -==> ./monkeytype/static/themes/botanical.css <== -:root { - --bg-color: #7b9c98; - --main-color: #eaf1f3; - --caret-color: #abc6c4; - --sub-color: #495755; - --text-color: #eaf1f3; - --error-color: #f6c9b4; - --error-extra-color: #f59a71; - --colorful-error-color: #f6c9b4; - --colorful-error-extra-color: #f59a71; -} - -==> ./monkeytype/static/themes/iceberg_dark.css <== -:root { - --bg-color: #161821; - --caret-color: #d2d4de; - --main-color: #84a0c6; - --sub-color: #595e76; - --text-color: #c6c8d1; - --error-color: #e27878; - --error-extra-color: #e2a478; - --colorful-error-color: #e27878; - --colorful-error-extra-color: #e2a478; -} - -==> ./monkeytype/static/themes/aether.css <== -:root { - --bg-color: #101820; - --main-color: #eedaea; - --caret-color: #eedaea; - --sub-color: #cf6bdd; - --text-color: #eedaea; - --error-color: #ff5253; - --error-extra-color: #e3002b; - --colorful-error-color: #ff5253; - --colorful-error-extra-color: #e3002b; -} - -#menu .icon-button:nth-child(1) { - color: #e4002b; -} - -#menu .icon-button:nth-child(2) { - color: #c53562; -} - -#menu .icon-button:nth-child(3) { - color: #95549e; -} - -#menu .icon-button:nth-child(4) { - color: #6744a1; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #393c73; -} - -==> ./monkeytype/static/themes/magic_girl.css <== -:root { - --bg-color: #ffffff; - --main-color: #f5b1cc; - --caret-color: #e45c96; - --sub-color: #93e8d3; - --text-color: #00ac8c; - --error-color: #ffe495; - --error-extra-color: #e45c96; - --colorful-error-color: #ffe485; - --colorful-error-extra-color: #e45c96; -} - -==> ./monkeytype/static/themes/mashu.css <== -:root { - --bg-color: #2b2b2c; - --main-color: #76689a; - --caret-color: #76689a; - --sub-color: #d8a0a6; - --text-color: #f1e2e4; - --error-color: #d44729; - --error-extra-color: #8f2f19; - --colorful-error-color: #d44729; - --colorful-error-extra-color: #8f2f19; -} - -==> ./monkeytype/static/themes/dark_magic_girl.css <== -:root { - --bg-color: #091f2c; - --main-color: #f5b1cc; - --caret-color: #93e8d3; - --sub-color: #93e8d3; - --text-color: #a288d9; - --error-color: #e45c96; - --error-extra-color: #e45c96; - --colorful-error-color: #00b398; - --colorful-error-extra-color: #e45c96; -} - -==> ./monkeytype/static/themes/mint.css <== -:root { - --bg-color: #05385b; - --main-color: #5cdb95; - --caret-color: #5cdb95; - --sub-color: #20688a; - --text-color: #edf5e1; - --error-color: #f35588; - --error-extra-color: #a3385a; - --colorful-error-color: #f35588; - --colorful-error-extra-color: #a3385a; -} - -==> ./monkeytype/static/themes/rudy.css <== -:root { - --bg-color: #1a2b3e; - --caret-color: #af8f5c; - --main-color: #af8f5c; - --sub-color: #3a506c; - --text-color: #c9c8bf; - --error-color: #bf616a; - --error-extra-color: #793e44; - --colorful-error-color: #bf616a; - --colorful-error-extra-color: #793e44; -} - -==> ./monkeytype/static/themes/dev.css <== -/*this theme is based on "Dev theme by KDr3w" color pallet: https://www.deviantart.com/kdr3w/art/Dev-825722799 */ -:root { - --bg-color: #1b2028; - --main-color: #23a9d5; - --caret-color: #4b5975; - --sub-color: #4b5975; - --text-color: #ccccb5; - --error-color: #b81b2c; - --error-extra-color: #84131f; - --colorful-error-color: #b81b2c; - --colorful-error-extra-color: #84131f; -} - -==> ./monkeytype/static/themes/dollar.css <== -:root { - --bg-color: #e4e4d4; - --main-color: #6b886b; - --caret-color: #424643; - --sub-color: #8a9b69; - --text-color: #555a56; - --error-color: #d60000; - --error-extra-color: #f68484; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/onedark.css <== -:root { - --bg-color: #2f343f; - --caret-color: #61afef; - --main-color: #61afef; - --sub-color: #eceff4; - --text-color: #98c379; - --error-color: #e06c75; - --error-extra-color: #d62436; - --colorful-error-color: #d62436; - --colorful-error-extra-color: #ff0019; -} - -==> ./monkeytype/static/themes/red_dragon.css <== -:root { - --bg-color: #1a0b0c; - --main-color: #ff3a32; - --caret-color: #ff3a32; - --sub-color: #e2a528; - --text-color: #4a4d4e; - --error-color: #771b1f; - --error-extra-color: #591317; - --colorful-error-color: #771b1f; - --colorful-error-extra-color: #591317; -} - -==> ./monkeytype/static/themes/tiramisu.css <== -:root { - --bg-color: #cfc6b9; - --main-color: #c0976f; - --caret-color: #7d5448; - --sub-color: #c0976f; - --text-color: #7d5448; - --error-color: #e9632d; - --error-extra-color: #e9632d; - --colorful-error-color: #e9632d; - --colorful-error-extra-color: #e9632d; -} - -==> ./monkeytype/static/themes/midnight.css <== -:root { - --bg-color: #0b0e13; - --main-color: #60759f; - --caret-color: #60759f; - --sub-color: #394760; - --text-color: #9fadc6; - --error-color: #c27070; - --error-extra-color: #c28b70; - --colorful-error-color: #c27070; - --colorful-error-extra-color: #c28b70; -} - -==> ./monkeytype/static/themes/dots.css <== -:root { - --bg-color: #121520; - --caret-color: #fff; - --main-color: #fff; - --sub-color: #676e8a; - --text-color: #fff; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #da3333; - --colorful-error-extra-color: #791717; -} - -#menu { - gap: 0.5rem; -} - -#top.focus #menu .icon-button, -#top.focus #menu:before, -#top.focus #menu:after { - background: var(--sub-color); -} - -#menu .icon-button { - border-radius: 10rem !important; - color: #121520; -} - -/* #menu:before{ - content: ""; - background: #f94348; - width: 1.25rem; - height: 1.25rem; - padding: .5rem; - border-radius: 10rem; -} */ - -#menu .icon-button:nth-child(1) { - background: #f94348; -} - -#menu .icon-button:nth-child(2) { - background: #9261ff; -} - -#menu .icon-button:nth-child(3) { - background: #3cc5f8; -} - -#menu .icon-button:nth-child(4) { - background: #4acb8a; -} - -#menu .icon-button:nth-child(5) { - background: #ffd543; -} - -#menu .icon-button:nth-child(6), -#menu .icon-button:nth-child(7) { - background: #ff9349; -} - -/* #menu:after{ - content: ""; - background: #ff9349; - width: 1.25rem; - height: 1.25rem; - padding: .5rem; - border-radius: 10rem; -} */ - -#top.focus #menu .icon-button.discord::after { - border-color: transparent; -} - -==> ./monkeytype/static/themes/ez_mode.css <== -:root { - --bg-color: #0068c6; - --main-color: #fa62d5; - --caret-color: #4ddb47; - --sub-color: #f5f5f5; - --text-color: #fa62d5; - --error-color: #4ddb47; - --error-extra-color: #42ba3b; - --colorful-error-color: #4ddb47; - --colorful-error-extra-color: #42ba3b; -} - -.pageSettings .section h1 { - color: var(--text-color); -} - -.pageSettings .section > .text { - color: var(--sub-color); -} - -.pageAbout .section .title { - color: var(--text-color); -} - -.pageAbout .section p { - color: var(--sub-color); -} - -#leaderboardsWrapper #leaderboards .title { - color: var(--sub-color); -} - -#leaderboardsWrapper #leaderboards .tables table thead { - color: var(--sub-color); -} - -#leaderboardsWrapper #leaderboards .tables table tbody { - color: var(--sub-color); -} - -==> ./monkeytype/static/themes/matrix.css <== -:root { - --bg-color: #000000; - --main-color: #15ff00; - --caret-color: #15ff00; - --sub-color: #003B00; - --text-color: #adffa7; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #da3333; - --colorful-error-extra-color: #791717; -} - -#liveWpm, -#timerNumber { - color: white; -} - -==> ./monkeytype/static/themes/aurora.css <== -:root { - --bg-color: #011926; - --main-color: #00e980; - --caret-color: #00e980; - --sub-color: #245c69; - --text-color: #fff; - --error-color: #b94da1; - --error-extra-color: #9b3a76; - --colorful-error-color: #b94da1; - --colorful-error-extra-color: #9b3a76; -} - -@keyframes rgb { - 0% { - color: #009fb4; - } - 25% { - color: #00e975; - } - 50% { - color: #00ffea; - } - 75% { - color: #00e975; - } - 100% { - color: #009fb4; - } -} - -@keyframes rgb-bg { - 0% { - background: #009fb4; - } - 25% { - background: #00e975; - } - 50% { - background: #00ffea; - } - 75% { - background: #00e975; - } - 100% { - background: #009fb4; - } -} - -.button.discord::after, -#caret, -.pageSettings .section .buttons .button.active, -.pageSettings .section.languages .buttons .language.active, -.pageAccount .group.filterButtons .buttons .button.active { - animation: rgb-bg 5s linear infinite; -} - -#top.focus .button.discord::after, -#top .button.discord.dotHidden::after { - animation-name: none !important; -} - -.logo .bottom, -#top .config .group .buttons .text-button.active, -#result .stats .group .bottom, -#menu .icon-button:hover, -#top .config .group .buttons .text-button:hover, -a:hover, -#words.flipped .word { - animation: rgb 5s linear infinite; -} - -#words.flipped .word letter.correct { - color: var(--sub-color); -} - -#words:not(.flipped) .word letter.correct { - animation: rgb 5s linear infinite; -} - -==> ./monkeytype/static/themes/night_runner.css <== -:root { - --bg-color: #212121; - --main-color: #feff04; - --caret-color: #feff04; - --sub-color: #5c4a9c; - --text-color: #e8e8e8; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #da3333; - --colorful-error-extra-color: #791717; -} - -==> ./monkeytype/static/themes/taro.css <== -:root { - /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */ - --bg-color: #b3baff; - --main-color: #130f1a; - --caret-color: #00e9e5; - --sub-color: #6f6c91; - --text-color: #130f1a; - --error-color: #ffe23e; - --error-extra-color: #fff1c3; - --colorful-error-color: #ffe23e; - --colorful-error-extra-color: #fff1c3; -} - -.word.error { - border-bottom: dotted 2px var(--text-color); -} - -#menu .icon-button:nth-child(1) { - background: var(--caret-color); - border-radius: 50%; -} - -#menu .icon-button:nth-child(2) { - background: var(--error-color); - border-radius: 50%; -} - -==> ./monkeytype/static/themes/vscode.css <== -:root { - --bg-color: #1e1e1e; - --main-color: #007acc; - --caret-color: #569cd6; - --sub-color: #4d4d4d; - --text-color: #d4d4d4; - --error-color: #f44747; - --error-extra-color: #f44747; - --colorful-error-color: #f44747; - --colorful-error-extra-color: #f44747; -} - -==> ./monkeytype/static/themes/nord.css <== -:root { - --bg-color: #242933; - --caret-color: #d8dee9; - --main-color: #d8dee9; - --sub-color: #617b94; - --text-color: #d8dee9; - --error-color: #bf616a; - --error-extra-color: #793e44; - --colorful-error-color: #bf616a; - --colorful-error-extra-color: #793e44; -} - -==> ./monkeytype/static/themes/iceberg_light.css <== -:root { - --bg-color: #e8e9ec; - --caret-color: #262a3f; - --main-color: #2d539e; - --sub-color: #adb1c4; - --text-color: #33374c; - --error-color: #cc517a; - --error-extra-color: #cc3768; - --colorful-error-color: #cc517a; - --colorful-error-extra-color: #cc3768; -} - -==> ./monkeytype/static/themes/wavez.css <== -:root { - --bg-color: #1c292f; - --main-color: #6bde3b; - --caret-color: #6bde3b; - --sub-color: #1a454e; - --text-color: #e9efe6; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/pink_lemonade.css <== -:root { - --bg-color: #f6d992; - --main-color: #f6a192; - --caret-color: #fcfcf8; - --sub-color: #f6b092; - --text-color: #fcfcf8; - --error-color: #ff6f69; - --error-extra-color: #ff6f69; - --colorful-error-color: #ff6f69; - --colorful-error-extra-color: #ff6f69; -} - -==> ./monkeytype/static/themes/dualshot.css <== -:root { - --bg-color: #737373; - --main-color: #212222; - --caret-color: #212222; - --sub-color: #aaaaaa; - --text-color: #212222; - --error-color: #c82931; - --error-extra-color: #ac1823; - --colorful-error-color: #c82931; - --colorful-error-extra-color: #ac1823; -} - -#menu .icon-button:nth-child(1) { - color: #2884bb; -} - -#menu .icon-button:nth-child(2) { - color: #25a5a9; -} - -#menu .icon-button:nth-child(3) { - color: #de9c24; -} - -#menu .icon-button:nth-child(4) { - color: #d82231; -} - -#menu .icon-button:nth-child(5) { - color: #212222; -} - -#menu .icon-button:nth-child(6) { - color: #212222; -} - -==> ./monkeytype/static/themes/material.css <== -:root { - --bg-color: #263238; - --main-color: #80cbc4; - --caret-color: #80cbc4; - --sub-color: #4c6772; - --text-color: #e6edf3; - --error-color: #fb4934; - --error-extra-color: #cc241d; - --colorful-error-color: #fb4934; - --colorful-error-extra-color: #cc241d; -} - -==> ./monkeytype/static/themes/paper.css <== -:root { - --bg-color: #eeeeee; - --main-color: #444444; - --caret-color: #444444; - --sub-color: #b2b2b2; - --text-color: #444444; - --error-color: #d70000; - --error-extra-color: #d70000; - --colorful-error-color: #d70000; - --colorful-error-extra-color: #d70000; -} - -==> ./monkeytype/static/themes/metropolis.css <== -:root { - --bg-color: #0f1f2c; - --main-color: #56c3b7; - --caret-color: #56c3b7; - --sub-color: #326984; - --text-color: #e4edf1; - --error-color: #d44729; - --error-extra-color: #8f2f19; - --colorful-error-color: #d44729; - --colorful-error-extra-color: #8f2f19; -} - -#top .logo .bottom { - color: #f4bc46; -} - -#menu .icon-button:nth-child(1) { - color: #d44729; -} - -#menu .icon-button:nth-child(2) { - color: #d44729; -} - -#menu .icon-button:nth-child(3) { - color: #d44729; -} - -#menu .icon-button:nth-child(4) { - color: #d44729; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6), -#menu .icon-button:nth-child(7) { - color: #d44729; -} - -==> ./monkeytype/static/themes/nebula.css <== -:root { - --bg-color: #212135; - --main-color: #be3c88; - --caret-color: #78c729; - --sub-color: #19b3b8; - --text-color: #838686; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/alpine.css <== -:root { - --bg-color: #6c687f; /*Background*/ - --main-color: #ffffff; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ - --caret-color: #585568; /*Cursor Color*/ - --sub-color: #9994b8; /*WPM text color of scrollbar and general color, before typed color*/ - --text-color: #ffffff; /*Color of text after hovering over it*/ - --error-color: #e32b2b; - --error-extra-color: #a62626; - --colorful-error-color: #e32b2b; - --colorful-error-extra-color: #a62626; -} - -==> ./monkeytype/static/themes/rgb.css <== -:root { - --bg-color: #111; - --main-color: #eee; - --caret-color: #eee; - --sub-color: #444; - --text-color: #eee; - --error-color: #eee; - --error-extra-color: #b3b3b3; - --colorful-error-color: #eee; - --colorful-error-extra-color: #b3b3b3; -} - -@keyframes rgb { - 0% { - color: #f44336; - } - 25% { - color: #ffc107; - } - 50% { - color: #4caf50; - } - 75% { - color: #3f51b5; - } - 100% { - color: #f44336; - } -} - -@keyframes rgb-bg { - 0% { - background: #f44336; - } - 25% { - background: #ffc107; - } - 50% { - background: #4caf50; - } - 75% { - background: #3f51b5; - } - 100% { - background: #f44336; - } -} - -.button.discord::after, -#caret, -.pageSettings .section .buttons .button.active, -.pageSettings .section.languages .buttons .language.active, -.pageAccount .group.filterButtons .buttons .button.active { - animation: rgb-bg 5s linear infinite; -} - -#top.focus .button.discord::after, -#top .button.discord.dotHidden::after { - animation-name: none !important; -} - -.logo .bottom, -#top .config .group .buttons .text-button.active, -#result .stats .group .bottom, -#menu .icon-button:hover, -#top .config .group .buttons .text-button:hover, -a:hover, -#words.flipped .word { - animation: rgb 5s linear infinite; -} - -/* .word letter.correct{ - animation: rgb 5s linear infinite; - -} */ - -#words.flipped .word letter.correct { - color: var(--sub-color); -} - -#words:not(.flipped) .word letter.correct { - animation: rgb 5s linear infinite; -} - -==> ./monkeytype/static/themes/hanok.css <== -:root { - --bg-color: #d8d2c3; - --main-color: #513a2a; - --caret-color: #513a2a; - --sub-color: #513a2a; - --text-color: #393b3b; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/serika_dark.css <== -:root { - --bg-color: #323437; - --main-color: #e2b714; - --caret-color: #e2b714; - --sub-color: #646669; - --text-color: #d1d0c5; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/modern_ink.css <== -:root { - --bg-color: #ffffff; - --main-color: #ff360d; - --caret-color: #ff0000; - --sub-color: #b7b7b7; - --text-color: #000000; - --error-color: #d70000; - --error-extra-color: #b00000; - --colorful-error-color: #ff1c1c; - --colorful-error-extra-color: #b00000; -} - -#menu .icon-button:nth-child(1) { - color: #ff0000; -} - -#menu .icon-button:nth-child(5) { - color: #ff0000; -} - -/* kinda confusing to type with this */ -/* .word letter.incorrect { - -webkit-transform: scale(0.5) translate(-100%, -100%); -} - -.word letter.incorrect.extra { - -webkit-transform: scale(0.5); -} */ - -.word.error { - border-bottom: solid 2px #ff0000; -} - -==> ./monkeytype/static/themes/muted.css <== -:root { - --bg-color: #525252; - --main-color: #C5B4E3; - --caret-color: #B1E4E3; - --sub-color: #939eae; - --text-color: #B1E4E3; - --error-color: #EDC1CD; - } - -==> ./monkeytype/static/themes/nautilus.css <== -:root { - --bg-color: #132237; - --main-color: #ebb723; - --caret-color: #ebb723; - --sub-color: #0b4c6c; - --text-color: #1cbaac; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #da3333; - --colorful-error-extra-color: #791717; -} - -==> ./monkeytype/static/themes/miami_nights.css <== -:root { - --bg-color: #18181a; - --main-color: #e4609b; - --caret-color: #e4609b; - --sub-color: #47bac0; - --text-color: #fff; - --error-color: #fff591; - --error-extra-color: #b6af68; - --colorful-error-color: #fff591; - --colorful-error-extra-color: #b6af68; -} - -==> ./monkeytype/static/themes/moonlight.css <== -/*inspired by GMK MOONLIGHT*/ -:root { - --bg-color: #1f2730; - --main-color: #c69f68; - --caret-color: #8f744b; - --sub-color: #4b5975; - --text-color: #ccccb5; - --error-color: #b81b2c; - --error-extra-color: #84131f; - --colorful-error-color: #b81b2c; - --colorful-error-extra-color: #84131f; -} -#menu { - gap: 0.5rem; -} -#top.focus #menu .icon-button, -#top.focus #menu:before, -#top.focus #menu:after { - background: var(--bg-color); -} -#menu .icon-button { - border-radius: rem !important; - color: #1f2730 !important; -} -#menu .icon-button :hover { - border-radius: rem !important; - color: #4b5975 !important; - transition: 0.25s; -} -#menu .icon-button:nth-child(1) { - background: #c69f68; -} -#menu .icon-button:nth-child(2) { - background: #c69f68; -} -#menu .icon-button:nth-child(3) { - background: #c69f68; -} -#menu .icon-button:nth-child(4) { - background: #c69f68; -} -#menu .icon-button:nth-child(5) { - background: #c69f68; -} -#menu .icon-button:nth-child(6), -#menu .icon-button:nth-child(7) { - background: #c69f68; -} -#top.focus #menu .icon-button.discord::after { - border-color: transparent; -} - -==> ./monkeytype/static/themes/darling.css <== -:root { - --bg-color: #fec8cd; - --main-color: #ffffff; - --caret-color: #ffffff; - --sub-color: #a30000; - --text-color: #ffffff; - --error-color: #2e7dde; - --error-extra-color: #2e7dde; - --colorful-error-color: #2e7dde; - --colorful-error-extra-color: #2e7dde; - --font: Roboto Mono; -} - -==> ./monkeytype/static/themes/pastel.css <== -:root { - --bg-color: #e0b2bd; - --main-color: #fbf4b6; - --caret-color: #fbf4b6; - --sub-color: #b4e9ff; - --text-color: #6d5c6f; - --error-color: #ff6961; - --error-extra-color: #c23b22; - --colorful-error-color: #ff6961; - --colorful-error-extra-color: #c23b22; -} - -==> ./monkeytype/static/themes/dark.css <== -:root { - --bg-color: #111; - --main-color: #eee; - --caret-color: #eee; - --sub-color: #444; - --text-color: #eee; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #da3333; - --colorful-error-extra-color: #791717; -} - -==> ./monkeytype/static/themes/ishtar.css <== -:root { - --bg-color: #202020; - --main-color: #91170c; - --caret-color: #c58940; - --sub-color: #847869; - --text-color: #fae1c3; - --error-color: #bb1e10; - --error-extra-color: #791717; - --colorful-error-color: #c5da33; - --colorful-error-extra-color: #849224; -} - -#top .logo .bottom { - color: #fae1c3; -} - -==> ./monkeytype/static/themes/retro.css <== -:root { - --bg-color: #dad3c1; - --main-color: #1d1b17; - --caret-color: #1d1b17; - --sub-color: #918b7d; - --text-color: #1d1b17; - --error-color: #bf616a; - --error-extra-color: #793e44; - --colorful-error-color: #bf616a; - --colorful-error-extra-color: #793e44; -} - -==> ./monkeytype/static/themes/stealth.css <== -:root { - --bg-color: #010203; - --main-color: #383e42; - --caret-color: #e25303; - --sub-color: #5e676e; - --text-color: #383e42; - --error-color: #e25303; - --error-extra-color: #73280c; - --colorful-error-color: #e25303; - --colorful-error-extra-color: #73280c; -} -#menu .icon-button:nth-child(4) { - color: #e25303; -} -#timerNumber { - color: #5e676e; -} - -==> ./monkeytype/static/themes/repose_dark.css <== -:root { - --bg-color: #2F3338; - --main-color: #D6D2BC; - --caret-color: #D6D2BC; - --sub-color: #8F8E84; - --text-color: #D6D2BC; - --error-color: #FF4A59; - --error-extra-color: #C43C53; - --colorful-error-color: #FF4A59; - --colorful-error-extra-color: #C43C53; -} - -==> ./monkeytype/static/themes/lime.css <== -:root { - --bg-color: #7c878e; - --main-color: #93c247; - --caret-color: #93c247; - --sub-color: #4b5257; - --text-color: #bfcfdc; - --error-color: #ea4221; - --error-extra-color: #7e2a33; - --colorful-error-color: #ea4221; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/blueberry_light.css <== -:root { - --bg-color: #dae0f5; - --main-color: #506477; - --caret-color: #df4576; - --sub-color: #92a4be; - --text-color: #678198; - --error-color: #df4576; - --error-extra-color: #d996ac; - --colorful-error-color: #df4576; - --colorful-error-extra-color: #d996ac; -} - -#top .logo .bottom { - color: #df4576; -} - -==> ./monkeytype/static/themes/fruit_chew.css <== -:root { - --bg-color: #d6d3d6; - --main-color: #5c1e5f; - --caret-color: #b92221; - --sub-color: #b49cb5; - --text-color: #282528; - --error-color: #bd2621; - --error-extra-color: #a62626; - --colorful-error-color: #bd2621; - --colorful-error-extra-color: #a62626; -} - -#menu .icon-button:nth-child(1) { - color: #a6bf50; -} - -#menu .icon-button:nth-child(2) { - color: #c3921a; -} - -#menu .icon-button:nth-child(3) { - color: #b92221; -} - -#menu .icon-button:nth-child(4) { - color: #88b6ce; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #661968; -} - -==> ./monkeytype/static/themes/olivia.css <== -:root { - --bg-color: #1c1b1d; - --main-color: #deaf9d; - --caret-color: #deaf9d; - --sub-color: #4e3e3e; - --text-color: #f2efed; - --error-color: #bf616a; - --error-extra-color: #793e44; - --colorful-error-color: #e03d4e; - --colorful-error-extra-color: #aa2f3b; -} - -==> ./monkeytype/static/themes/diner.css <== -:root { - --bg-color: #537997; - --main-color: #c3af5b; - --caret-color: #ad5145; - --sub-color: #445c7f; - --text-color: #dfdbc8; - --error-color: #ad5145; - --error-extra-color: #7e2a33; - --colorful-error-color: #ad5145; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/sewing_tin_light.css <== -:root { - --bg-color: #ffffff; - --main-color: #2d2076; - --caret-color: #fbdb8c; - --sub-color: #385eca; - --text-color: #2d2076; - --error-color: #f2ce83; - --error-extra-color: #f2ce83; - --colorful-error-color: #f2ce83; - --colorful-error-extra-color: #f2ce83; -} - -#menu .icon-button { - color: #f2ce83; -} - -#menu .icon-button:hover { - color: #c6915e; -} - -#top .logo .text { - background-color: #ffffff; /* fallback */ - background: -webkit-linear-gradient( - #2d2076, - #2d2076 25%, - #2e3395 25%, - #2e3395 50%, - #3049ba 50%, - #3049ba 75%, - #385eca 75%, - #385eca - ); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -#top .logo .text .top { - /* prevent it from being transparent */ - -webkit-text-fill-color: #385eca; -} - -==> ./monkeytype/static/themes/carbon.css <== -:root { - --bg-color: #313131; - --main-color: #f66e0d; - --caret-color: #f66e0d; - --sub-color: #616161; - --text-color: #f5e6c8; - --error-color: #e72d2d; - --error-extra-color: #7e2a33; - --colorful-error-color: #e72d2d; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/cafe.css <== -:root { - --bg-color: #ceb18d; - --main-color: #14120f; - --caret-color: #14120f; - --sub-color: #d4d2d1; - --text-color: #14120f; - --error-color: #c82931; - --error-extra-color: #ac1823; - --colorful-error-color: #c82931; - --colorful-error-extra-color: #ac1823; -} - -==> ./monkeytype/static/themes/future_funk.css <== -:root { - --bg-color: #2e1a47; - --main-color: #f7f2ea; - --caret-color: #f7f2ea; - --sub-color: #c18fff; - --text-color: #f7f2ea; - --error-color: #f04e98; - --error-extra-color: #bd1c66; - --colorful-error-color: #f04e98; - --colorful-error-extra-color: #bd1c66; -} - -#menu .icon-button:nth-child(1) { - color: #f04e98; -} - -#menu .icon-button:nth-child(2) { - color: #f8bed6; -} - -#menu .icon-button:nth-child(3) { - color: #f6eb61; -} - -#menu .icon-button:nth-child(4) { - color: #a4dbe8; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #a266ed; -} - -==> ./monkeytype/static/themes/fire.css <== -:root { - --bg-color: #0f0000; - --main-color: #b31313; - --caret-color: #b31313; - --sub-color: #683434; - --text-color: #ffffff; - --error-color: #2f3cb6; - --error-extra-color: #434a8f; - --colorful-error-color: #2f3cb6; - --colorful-error-extra-color: #434a8f; -} - -@keyframes rgb { - 0% { - color: #b31313; - } - 25% { - color: #ff9000; - } - 50% { - color: #fdda16; - } - 75% { - color: #ff9000; - } - 100% { - color: #b31313; - } -} - -@keyframes rgb-bg { - 0% { - background: #b31313; - } - 25% { - background: #ff9000; - } - 50% { - background: #fdda16; - } - 75% { - background: #ff9000; - } - 100% { - background: #b31313; - } -} - -.button.discord::after, -#caret, -.pageSettings .section .buttons .button.active, -.pageSettings .section.languages .buttons .language.active, -.pageAccount .group.filterButtons .buttons .button.active { - animation: rgb-bg 5s linear infinite; -} - -#top.focus .button.discord::after, -#top .button.discord.dotHidden::after { - animation-name: none !important; -} - -.logo .bottom, -#top .config .group .buttons .text-button.active, -#result .stats .group .bottom, -#menu .icon-button:hover, -#top .config .group .buttons .text-button:hover, -a:hover, -#words.flipped .word { - animation: rgb 5s linear infinite; -} - -#words.flipped .word letter.correct { - color: var(--sub-color); -} - -#words:not(.flipped) .word letter.correct { - animation: rgb 5s linear infinite; -} - -==> ./monkeytype/static/themes/striker.css <== -:root { - --bg-color: #124883; - --main-color: #d7dcda; - --caret-color: #d7dcda; - --sub-color: #0f2d4e; - --text-color: #d6dbd9; - --error-color: #fb4934; - --error-extra-color: #cc241d; - --colorful-error-color: #fb4934; - --colorful-error-extra-color: #cc241d; -} - -==> ./monkeytype/static/themes/monokai.css <== -:root { - --bg-color: #272822; - --main-color: #a6e22e; - --caret-color: #66d9ef; - --sub-color: #e6db74; - --text-color: #e2e2dc; - --error-color: #f92672; - --error-extra-color: #fd971f; - --colorful-error-color: #f92672; - --colorful-error-extra-color: #fd971f; -} - -==> ./monkeytype/static/themes/8008.css <== -:root { - --bg-color: #333a45; - --main-color: #f44c7f; - --caret-color: #f44c7f; - --sub-color: #939eae; - --text-color: #e9ecf0; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #c5da33; - --colorful-error-extra-color: #849224; -} - -==> ./monkeytype/static/themes/froyo.css <== -:root { - --bg-color: #e1dacb; - --main-color: #7b7d7d; - --caret-color: #7b7d7d; - --sub-color: #b29c5e; - --text-color: #7b7d7d; - --error-color: #f28578; - --error-extra-color: #d56558; - --colorful-error-color: #f28578; - --colorful-error-extra-color: #d56558; -} - -#menu .icon-button:nth-child(1) { - color: #ff7e73; -} - -#menu .icon-button:nth-child(2) { - color: #f5c370; -} - -#menu .icon-button:nth-child(3) { - color: #08d9a3; -} - -#menu .icon-button:nth-child(4) { - color: #0ca5e2; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #875ac6; -} - -==> ./monkeytype/static/themes/evil_eye.css <== -:root { - --bg-color: #0084c2; - --main-color: #f7f2ea; - --caret-color: #f7f2ea; - --sub-color: #01589f; - --text-color: #171718; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/deku.css <== -:root { - --bg-color: #058b8c; - --main-color: #b63530; - --caret-color: #b63530; - --sub-color: #255458; - --text-color: #f7f2ea; - --error-color: #b63530; - --error-extra-color: #530e0e; - --colorful-error-color: #ddca1f; - --colorful-error-extra-color: #8f8610; -} - -==> ./monkeytype/static/themes/alduin.css <== -:root { - --bg-color: #1c1c1c; - --main-color: #dfd7af; - --caret-color: #e3e3e3; - --sub-color: #444444; - --text-color: #f5f3ed; - --error-color: #af5f5f; - --error-extra-color: #4d2113; - --colorful-error-color: #af5f5f; - --colorful-error-extra-color: #4d2113; -} - -==> ./monkeytype/static/themes/dracula.css <== -:root { - --bg-color: #282a36; - --main-color: #f2f2f2; - --caret-color: #f2f2f2; - --sub-color: #bd93f9; - --text-color: #f2f2f2; - --error-color: #f758a0; - --error-extra-color: #732e51; - --colorful-error-color: #f758a0; - --colorful-error-extra-color: #732e51; -} - -#menu .icon-button:nth-child(1) { - color: #ec75c4; -} - -#menu .icon-button:nth-child(2) { - color: #8be9fd; -} - -#menu .icon-button:nth-child(3) { - color: #50fa7b; -} - -#menu .icon-button:nth-child(4) { - color: #f1fa8c; -} - -#menu .icon-button:nth-child(5) { - color: #ffb86c; -} - -#menu .icon-button:nth-child(6) { - color: #ffb86c; -} - -==> ./monkeytype/static/themes/metaverse.css <== -:root { - --bg-color: #232323; - --main-color: #d82934; - --caret-color: #d82934; - --sub-color: #5e5e5e; - --text-color: #e8e8e8; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #d7da33; - --colorful-error-extra-color: #737917; -} - -==> ./monkeytype/static/themes/hammerhead.css <== -:root { - --bg-color: #030613; - --main-color: #4fcdb9; - --caret-color: #4fcdb9; - --sub-color: #1e283a; - --text-color: #e2f1f5; - --error-color: #e32b2b; - --error-extra-color: #a62626; - --colorful-error-color: #e32b2b; - --colorful-error-extra-color: #a62626; -} - -==> ./monkeytype/static/themes/bushido.css <== -:root { - --bg-color: #242933; - --main-color: #ec4c56; - --caret-color: #ec4c56; - --sub-color: #596172; - --text-color: #f6f0e9; - --error-color: #ec4c56; - --error-extra-color: #9b333a; - --colorful-error-color: #ecdc4c; - --colorful-error-extra-color: #bdb03d; -} - -==> ./monkeytype/static/themes/matcha_moccha.css <== -:root { - --bg-color: #523525; - --main-color: #7ec160; - --caret-color: #7ec160; - --sub-color: #9e6749; - --text-color: #ecddcc; - --error-color: #fb4934; - --error-extra-color: #cc241d; - --colorful-error-color: #fb4934; - --colorful-error-extra-color: #cc241d; -} - -==> ./monkeytype/static/themes/modern_dolch.css <== -:root { - --bg-color: #2d2e30; - --main-color: #7eddd3; - --caret-color: #7eddd3; - --sub-color: #54585c; - --text-color: #e3e6eb; - --error-color: #d36a7b; - --error-extra-color: #994154; - --colorful-error-color: #d36a7b; - --colorful-error-extra-color: #994154; -} - -==> ./monkeytype/static/themes/creamsicle.css <== -:root { - --bg-color: #ff9869; - --main-color: #fcfcf8; - --caret-color: #fcfcf8; - --sub-color: #ff661f; - --text-color: #fcfcf8; - --error-color: #6a0dad; - --error-extra-color: #6a0dad; - --colorful-error-color: #6a0dad; - --colorful-error-extra-color: #6a0dad; -} - -==> ./monkeytype/static/themes/strawberry.css <== -:root { - --bg-color: #f37f83; - --main-color: #fcfcf8; - --caret-color: #fcfcf8; - --sub-color: #e53c58; - --text-color: #fcfcf8; - --error-color: #fcd23f; - --error-extra-color: #d7ae1e; - --colorful-error-color: #fcd23f; - --colorful-error-extra-color: #d7ae1e; -} - -==> ./monkeytype/static/themes/shadow.css <== -:root { - --bg-color: #000; - --main-color: #eee; - --caret-color: #eee; - --sub-color: #444; - --text-color: #eee; - --error-color: #fff; - --error-extra-color: #d8d8d8; - --colorful-error-color: #fff; - --colorful-error-extra-color: #d8d8d8; -} - -#top .logo .icon{ - color: #8C3230; -} - -#top .logo .text{ - color: #557D8D; -} - -@keyframes shadow { - to { - color: #000; - } -} - -@keyframes shadow-repeat { - 50% { - color: #000; - } - 100% { - color: #eee; - } -} - -#liveWpm, -#timerNumber { - color: white; -} - -#top .config .group .buttons .text-button.active, -#result .stats .group, -#menu .icon-button:hover, -#top .config .group .buttons .text-button:hover, -a:hover { - animation: shadow-repeat 3s linear infinite forwards; -} - -#logo, -#typingTest .word letter.correct { - animation: shadow 5s linear 1 forwards; -} - -==> ./monkeytype/static/themes/bento.css <== -:root { - --bg-color: #2d394d; - --main-color: #ff7a90; - --caret-color: #ff7a90; - --sub-color: #4a768d; - --text-color: #fffaf8; - --error-color: #ee2a3a; - --error-extra-color: #f04040; - --colorful-error-color: #fc2032; - --colorful-error-extra-color: #f04040; -} - -==> ./monkeytype/static/themes/menthol.css <== -:root { - --bg-color: #00c18c; - --main-color: #ffffff; - --caret-color: #99fdd8; - --sub-color: #186544; - --text-color: #ffffff; - --error-color: #e03c3c; - --error-extra-color: #b12525; - --colorful-error-color: #e03c3c; - --colorful-error-extra-color: #b12525; -} - -==> ./monkeytype/static/themes/our_theme.css <== -:root { - --bg-color: #ce1226; - --main-color: #fcd116; - --caret-color: #fcd116; - --sub-color: #6d0f19; - --text-color: #ffffff; - --error-color: #fcd116; - --error-extra-color: #fcd116; - --colorful-error-color: #1672fc; - --colorful-error-extra-color: #1672fc; -} - -==> ./monkeytype/static/themes/mr_sleeves.css <== -:root { - --bg-color: #d1d7da; - --main-color: #daa99b; - --caret-color: #8fadc9; - --sub-color: #9a9fa1; - --text-color: #1d1d1d; - --error-color: #bf6464; - --error-extra-color: #793e44; - --colorful-error-color: #8fadc9; - --colorful-error-extra-color: #667c91; -} - -#top .logo .bottom { - color: #8fadc9; -} - -#top .config .group .buttons .text-button.active { - color: #daa99b; -} - -/* #menu .icon-button:nth-child(1){ - color: #daa99b; -} - -#menu .icon-button:nth-child(2){ - color: #daa99b; -} - -#menu .icon-button:nth-child(3){ - color: #8fadc9; -} - -#menu .icon-button:nth-child(4), -#menu .icon-button:nth-child(5){ - color: #8fadc9; -} */ - -==> ./monkeytype/static/themes/retrocast.css <== -:root { - --bg-color: #07737a; - --main-color: #88dbdf; - --caret-color: #88dbdf; - --sub-color: #f3e03b; - --text-color: #ffffff; - --error-color: #ff585d; - --error-extra-color: #c04455; - --colorful-error-color: #ff585d; - --colorful-error-extra-color: #c04455; -} - -#menu .icon-button:nth-child(1) { - color: #88dbdf; -} - -#menu .icon-button:nth-child(2) { - color: #88dbdf; -} - -#menu .icon-button:nth-child(3) { - color: #88dbdf; -} - -#menu .icon-button:nth-child(4) { - color: #ff585d; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #f3e03b; -} - -==> ./monkeytype/static/themes/frozen_llama.css <== -:root { - --bg-color: #9bf2ea; - --main-color: #6d44a6; - --caret-color: #ffffff; - --sub-color: #b690fd; - --text-color: #ffffff; - --error-color: #e42629; - --error-extra-color: #e42629; - --colorful-error-color: #e42629; - --colorful-error-extra-color: #e42629; -} - -==> ./monkeytype/static/themes/solarized_light.css <== -:root { - --bg-color: #fdf6e3; - --main-color: #859900; - --caret-color: #dc322f; - --sub-color: #2aa198; - --text-color: #181819; - --error-color: #d33682; - --error-extra-color: #9b225c; - --colorful-error-color: #d33682; - --colorful-error-extra-color: #9b225c; -} - -==> ./monkeytype/static/themes/bliss.css <== -:root { - --bg-color: #262727; - --main-color: #f0d3c9; - --caret-color: #f0d3c9; - --sub-color: #665957; - --text-color: #fff; - --error-color: #bd4141; - --error-extra-color: #883434; - --colorful-error-color: #bd4141; - --colorful-error-extra-color: #883434; -} - -==> ./monkeytype/static/themes/repose_light.css <== -:root { - --bg-color: #EFEAD0; - --main-color: #5F605E; - --caret-color: #5F605E; - --sub-color: #8F8E84; - --text-color: #333538; - --error-color: #C43C53; - --error-extra-color: #A52632; - --colorful-error-color: #C43C53; - --colorful-error-extra-color: #A52632; -} - -==> ./monkeytype/static/themes/fundamentals.css <== -:root { - --bg-color: #727474; - --main-color: #7fa482; - --caret-color: #196378; - --sub-color: #cac4be; - --text-color: #131313; - --error-color: #5e477c; - --error-extra-color: #413157; - --colorful-error-color: #5e477c; - --colorful-error-extra-color: #413157; -} - -#top .logo .bottom { - color: #196378; -} - -==> ./monkeytype/static/themes/miami.css <== -:root { - --bg-color: #f35588; - --main-color: #05dfd7; - --caret-color: #a3f7bf; - --text-color: #f0e9ec; - --sub-color: #94294c; - --error-color: #fff591; - --error-extra-color: #b9b269; - --colorful-error-color: #fff591; - --colorful-error-extra-color: #b9b269; -} - -==> ./monkeytype/static/themes/sewing_tin.css <== -:root { - --bg-color: #241963; - --main-color: #f2ce83; - --caret-color: #fbdb8c; - --sub-color: #446ad5; - --text-color: #ffffff; - --error-color: #c6915e; - --error-extra-color: #c6915e; - --colorful-error-color: #c6915e; - --colorful-error-extra-color: #c6915e; -} - -#menu .icon-button { - color: #f2ce83; -} - -#menu .icon-button:hover { - color: #c6915e; -} - -==> ./monkeytype/static/themes/fleuriste.css <== -:root { - --bg-color: #c6b294; - --main-color: #405a52; - --caret-color: #8a785b; - --sub-color: #64374d; - --text-color: #091914; - --error-color: #990000; - --error-extra-color: #8a1414; - --colorful-error-color: #a63a3a; - --colorful-error-extra-color: #bd4c4c; -} - -#menu .icon-button:nth-child(1, 3, 5) { - background: #405a52; -} -#menu .icon-button:nth-child(2, 4) { - background: #64374d; -} - - - -==> ./monkeytype/static/themes/rose_pine.css <== -:root { - --bg-color: #1f1d27; /*Background*/ - --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ - --caret-color: #f6c177; /*Cursor Color*/ - --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ - --text-color: #e0def4; /*Color of text after hovering over it*/ - --error-color: #eb6f92; - --error-extra-color: #ebbcba; - --colorful-error-color: #eb6f92; - --colorful-error-extra-color: #ebbcba; -} - -==> ./monkeytype/static/themes/sonokai.css <== -:root { - --bg-color: #2c2e34; - --main-color: #9ed072; - --caret-color: #f38c71; - --sub-color: #e7c664; - --text-color: #e2e2e3; - --error-color: #fc5d7c; - --error-extra-color: #ecac6a; - --colorful-error-color: #fc5d7c; - --colorful-error-extra-color: #ecac6a; -} - -==> ./monkeytype/static/themes/trackday.css <== -:root { - --bg-color: #464d66; - --main-color: #e0513e; - --caret-color: #475782; - --sub-color: #5c7eb9; - --text-color: #cfcfcf; - --error-color: #e44e4e; - --error-extra-color: #fd3f3f; - --colorful-error-color: #ff2e2e; - --colorful-error-extra-color: #bb2525; -} - -#menu .icon-button:nth-child(1) { - color: #e0513e; -} - -#menu .icon-button:nth-child(3) { - color: #cfcfcf; -} - -#menu .icon-button:nth-child(2) { - color: #ccc500; -} - -==> ./monkeytype/static/themes/milkshake.css <== -:root { - --bg-color: #ffffff; - --main-color: #212b43; - --caret-color: #212b43; - --sub-color: #62cfe6; - --text-color: #212b43; - --error-color: #f19dac; - --error-extra-color: #e58c9d; - --colorful-error-color: #f19dac; - --colorful-error-extra-color: #e58c9d; -} - -#menu .icon-button:nth-child(1) { - color: #f19dac; -} - -#menu .icon-button:nth-child(2) { - color: #f6f4a0; -} - -#menu .icon-button:nth-child(3) { - color: #73e4d0; -} - -#menu .icon-button:nth-child(4) { - color: #61cfe6; -} - -#menu .icon-button:nth-child(5) { - color: #ba96db; -} - -#menu .icon-button:nth-child(6) { - color: #ba96db; -} - -==> ./monkeytype/static/themes/trance.css <== -:root { - --bg-color: #00021b; - --main-color: #e51376; - --caret-color: #e51376; - --sub-color: #3c4c79; - --text-color: #fff; - --error-color: #02d3b0; - --error-extra-color: #3f887c; - --colorful-error-color: #02d3b0; - --colorful-error-extra-color: #3f887c; -} - -@keyframes rgb { - 0% { - color: #e51376; - } - 50% { - color: #0e77ee; - } - 100% { - color: #e51376; - } -} - -@keyframes rgb-bg { - 0% { - background: #e51376; - } - 50% { - background: #0e77ee; - } - 100% { - background: #e51376; - } -} - -.button.discord::after, -#caret, -.pageSettings .section .buttons .button.active, -.pageSettings .section.languages .buttons .language.active, -.pageAccount .group.filterButtons .buttons .button.active { - animation: rgb-bg 5s linear infinite; -} - -#top.focus .button.discord::after, -#top .button.discord.dotHidden::after { - animation-name: none !important; -} - -.logo .bottom, -#top .config .group .buttons .text-button.active, -#result .stats .group .bottom, -#menu .icon-button:hover, -#top .config .group .buttons .text-button:hover, -a:hover, -#words.flipped .word { - animation: rgb 5s linear infinite; -} - -#words.flipped .word letter.correct { - color: var(--sub-color); -} - -#words:not(.flipped) .word letter.correct { - animation: rgb 5s linear infinite; -} - -==> ./monkeytype/static/themes/nausea.css <== -:root { - --bg-color: #323437; - --main-color: #e2b714; - --caret-color: #e2b714; - --sub-color: #646669; - --text-color: #d1d0c5; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -@keyframes woah { - 0% { - transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) - scaleY(0.9); - } - - 25% { - transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1) scaleY(0.8); - } - - 50% { - transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(0.9) - scaleY(0.9); - } - - 75% { - transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1.5) - scaleY(1.1); - } - - 100% { - transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) - scaleY(0.9); - } -} - -@keyframes plsstop { - 0% { - background: #323437; - } - - 50% { - background: #3e4146; - } - - 100% { - background: #323437; - } -} - -#middle { - animation: woah 7s infinite cubic-bezier(0.5, 0, 0.5, 1); -} - -#centerContent { - transform: rotate(5deg); - perspective: 500px; -} - -body { - animation: plsstop 10s infinite cubic-bezier(0.5, 0, 0.5, 1); - overflow: hidden; -} - -==> ./monkeytype/static/themes/superuser.css <== -:root { - --bg-color: #262a33; - --main-color: #43ffaf; - --caret-color: #43ffaf; - --sub-color: #526777; - --text-color: #e5f7ef; - --error-color: #ff5f5f; - --error-extra-color: #d22a2a; - --colorful-error-color: #ff5f5f; - --colorful-error-extra-color: #d22a2a; -} - -==> ./monkeytype/static/themes/serika.css <== -:root { - --main-color: #e2b714; - --caret-color: #e2b714; - --sub-color: #aaaeb3; - --bg-color: #e1e1e3; - --text-color: #323437; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #da3333; - --colorful-error-extra-color: #791717; -} - -==> ./monkeytype/static/themes/gruvbox_dark.css <== -:root { - --bg-color: #282828; - --main-color: #d79921; - --caret-color: #fabd2f; - --sub-color: #665c54; - --text-color: #ebdbb2; - --error-color: #fb4934; - --error-extra-color: #cc241d; - --colorful-error-color: #cc241d; - --colorful-error-extra-color: #9d0006; -} - -==> ./monkeytype/static/themes/godspeed.css <== -:root { - --bg-color: #eae4cf; - --main-color: #9abbcd; - --caret-color: #f4d476; - --sub-color: #c0bcab; - --text-color: #646669; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/joker.css <== -:root { - --bg-color: #1a0e25; - --main-color: #99de1e; - --caret-color: #99de1e; - --sub-color: #7554a3; - --text-color: #e9e2f5; - --error-color: #e32b2b; - --error-extra-color: #a62626; - --colorful-error-color: #e32b2b; - --colorful-error-extra-color: #a62626; -} - -==> ./monkeytype/static/themes/rose_pine_dawn.css <== -:root { - --bg-color: #fffaf3; /*Background*/ - --main-color: #56949f; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ - --caret-color: #ea9d34; /*Cursor Color*/ - --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ - --text-color: #286983; /*Color of text after hovering over it*/ - --error-color: #b4637a; - --error-extra-color: #d7827e; - --colorful-error-color: #b4637a; - --colorful-error-extra-color: #d7827e; -} - -==> ./monkeytype/static/themes/grand_prix.css <== -:root { - --bg-color: #36475c; - --main-color: #c0d036; - --caret-color: #c0d036; - --sub-color: #5c6c80; - --text-color: #c1c7d7; - --error-color: #fc5727; - --error-extra-color: #fc5727; - --colorful-error-color: #fc5727; - --colorful-error-extra-color: #fc5727; -} - -==> ./monkeytype/static/themes/lavender.css <== -:root { - --bg-color: #ada6c2; - --main-color: #e4e3e9; - --caret-color: #e4e3e9; - --sub-color: #e4e3e9; - --text-color: #2f2a41; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; - } - - #menu .icon-button { - border-radius: 10rem !important; - background: #2f2a41; - color: #e4e3e9; - - } - - #menu .icon-button:hover { - color: #ada6c2; - } - -==> ./monkeytype/static/themes/watermelon.css <== -:root { - --bg-color: #1f4437; - --main-color: #d6686f; - --caret-color: #d6686f; - --sub-color: #3e7a65; - --text-color: #cdc6bc; - --error-color: #c82931; - --error-extra-color: #ac1823; - --colorful-error-color: #c82931; - --colorful-error-extra-color: #ac1823; -} - -==> ./monkeytype/static/themes/copper.css <== -:root { - --bg-color: #442f29; - --main-color: #b46a55; - --caret-color: #c25c42; - --sub-color: #7ebab5; - --text-color: #e7e0de; - --error-color: #a32424; - --error-extra-color: #ec0909; - --colorful-error-color: #a32424; - --colorful-error-extra-color: #ec0909; -} - -==> ./monkeytype/static/themes/beach.css <== -:root { - --bg-color: #ffeead; - --main-color: #96ceb4; - --caret-color: #ffcc5c; - --sub-color: #ffcc5c; - --text-color: #5b7869; - --error-color: #ff6f69; - --error-extra-color: #ff6f69; - --colorful-error-color: #ff6f69; - --colorful-error-extra-color: #ff6f69; - } - - #menu .icon-button:nth-child(1), - #menu .icon-button:nth-child(2), - #menu .icon-button:nth-child(3), - #menu .icon-button:nth-child(4), - #menu .icon-button:nth-child(5), - #menu .icon-button:nth-child(6) { - color: #ff6f69; - } - -==> ./monkeytype/static/themes/pulse.css <== -:root { - --bg-color: #181818; - --main-color: #17b8bd; - --caret-color: #17b8bd; - --sub-color: #53565a; - --text-color: #e5f4f4; - --error-color: #da3333; - --error-extra-color: #791717; - --colorful-error-color: #da3333; - --colorful-error-extra-color: #791717; -} - -==> ./monkeytype/static/themes/drowning.css <== -:root { - --bg-color: #191826; - --main-color: #4a6fb5; - --caret-color: #4f85e8; - --sub-color: #50688c; - --text-color: #9393a7; - --error-color: #be555f; - --error-extra-color: #7e2a33; - --colorful-error-color: #be555f; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/blueberry_dark.css <== -:root { - --bg-color: #212b42; - --main-color: #add7ff; - --caret-color: #962f7e; - --sub-color: #5c7da5; - --text-color: #91b4d5; - --error-color: #df4576; - --error-extra-color: #d996ac; - --colorful-error-color: #df4576; - --colorful-error-extra-color: #d996ac; -} - -#top .logo .bottom { - color: #962f7e; -} - -==> ./monkeytype/static/themes/arch.css <== -:root { - --bg-color: #0c0d11; - --main-color: #7ebab5; - --caret-color: #7ebab5; - --sub-color: #454864; - --text-color: #f6f5f5; - --error-color: #ff4754; - --error-extra-color: #b02a33; - --colorful-error-color: #ff4754; - --colorful-error-extra-color: #b02a33; -} - -==> ./monkeytype/static/themes/olive.css <== -:root { - --bg-color: #e9e5cc; - --caret-color: #92946f; - --main-color: #92946f; - --sub-color: #b7b39e; - --text-color: #373731; - --error-color: #cf2f2f; - --error-extra-color: #a22929; - --colorful-error-color: #cf2f2f; - --colorful-error-extra-color: #a22929; -} - -==> ./monkeytype/static/themes/luna.css <== -:root { - --bg-color: #221c35; - --main-color: #f67599; - --caret-color: #f67599; - --sub-color: #5a3a7e; - --text-color: #ffe3eb; - --error-color: #efc050; - --error-extra-color: #c5972c; - --colorful-error-color: #efc050; - --colorful-error-extra-color: #c5972c; -} - -==> ./monkeytype/static/themes/red_samurai.css <== -:root { - --bg-color: #84202c; - --main-color: #c79e6e; - --caret-color: #c79e6e; - --sub-color: #55131b; - --text-color: #e2dad0; - --error-color: #33bbda; - --error-extra-color: #176b79; - --colorful-error-color: #33bbda; - --colorful-error-extra-color: #176779; -} - -==> ./monkeytype/static/themes/cyberspace.css <== -:root { - --bg-color: #181c18; - --main-color: #00ce7c; - --caret-color: #00ce7c; - --sub-color: #9578d3; - --text-color: #c2fbe1; - --error-color: #ff5f5f; - --error-extra-color: #d22a2a; - --colorful-error-color: #ff5f5f; - --colorful-error-extra-color: #d22a2a; -} - -==> ./monkeytype/static/themes/shoko.css <== -:root { - --bg-color: #ced7e0; - --main-color: #81c4dd; - --caret-color: #81c4dd; - --sub-color: #7599b1; - --text-color: #3b4c58; - --error-color: #bf616a; - --error-extra-color: #793e44; - --colorful-error-color: #bf616a; - --colorful-error-extra-color: #793e44; -} - -==> ./monkeytype/static/themes/oblivion.css <== -:root { - --bg-color: #313231; - --main-color: #a5a096; - --caret-color: #a5a096; - --sub-color: #5d6263; - --text-color: #f7f5f1; - --error-color: #dd452e; - --error-extra-color: #9e3423; - --colorful-error-color: #dd452e; - --colorful-error-extra-color: #9e3423; -} - -#menu .icon-button:nth-child(1) { - color: #9a90b4; -} - -#menu .icon-button:nth-child(2) { - color: #8db14b; -} - -#menu .icon-button:nth-child(3) { - color: #fca321; -} - -#menu .icon-button:nth-child(4) { - color: #2984a5; -} - -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #dd452e; -} - -==> ./monkeytype/static/themes/comfy.css <== -:root { - --bg-color: #4a5b6e; - --main-color: #f8cdc6; - --caret-color: #9ec1cc; - --sub-color: #9ec1cc; - --text-color: #f5efee; - --error-color: #c9465e; - --error-extra-color: #c9465e; - --colorful-error-color: #c9465e; - --colorful-error-extra-color: #c9465e; -} - -==> ./monkeytype/static/themes/chaos_theory.css <== -:root { - --bg-color: #141221; - --main-color: #fd77d7; - --caret-color: #dde5ed; - --text-color: #dde5ed; - --error-color: #fd77d7; - --sub-color: #676e8a; - --error-color: #FF5869; - --error-extra-color: #b03c47; - --colorful-error-color: #FF5869; - --colorful-error-extra-color: #b03c47; -} - -#top .logo .text { - -webkit-transform: rotateY(180deg); - unicode-bidi: bidi-override; - transition: 0.5s; -} - -#top .logo .top { - font-family: "Comic Sans MS", "Comic Sans", cursive; -} - -#top .logo .icon { - -webkit-transform: rotateX(180deg); - transition: 0.5s; -} - -#words .incorrect.extra { - -webkit-transform: rotateY(180deg); - unicode-bidi: bidi-override; - direction: rtl; -} - -#bottom .leftright .right .current-theme .text { - /* font-family: "Comic Sans MS", "Comic Sans", cursive; */ -} - -#caret { - background-image: url(https://i.imgur.com/yN31JmJ.png); - background-color: transparent; - background-size: 1rem; - background-position: center; - background-repeat: no-repeat; -} - -#caret.default { - width: 4px; -} - -.config .toggleButton { - -webkit-transform: rotateY(180deg); - unicode-bidi: bidi-override; - direction: rtl; - align-content: right; -} - -.config .mode .text-button { - -webkit-transform: rotateY(180deg); - unicode-bidi: bidi-override; - direction: rtl; - align-content: right; -} - -.config .wordCount .text-button, -.config .time .text-button, -.config .quoteLength .text-button, -.config .customText .text-button { - -webkit-transform: rotateY(180deg); - unicode-bidi: bidi-override; - direction: rtl; - align-content: right; -} - -#top.focus #menu .icon-button, -#top.focus #menu:before, -#top.focus #menu:after { - background: var(--sub-color); - -webkit-transform: rotateY(180deg) !important; -} - -#top.focus .logo .text, -#top.focus .logo:before, -#top.focus .logo:after { - -webkit-transform: rotateY(0deg); - direction: ltr; -} - -#top.focus .logo .icon, -#top.focus .logo:before, -#top.focus .logo:after { - -webkit-transform: rotateX(0deg); - direction: ltr; -} - -#bottom .leftright .right .current-theme:hover .fas.fa-fw.fa-palette { - -webkit-transform: rotateY(180deg); - transition: 0.5s; -} -#menu { - gap: 0.5rem; -} - -#menu .icon-button { - border-radius: 10rem i !important; - color: var(--bg-color); - transition: 0.5s; -} - -#menu .icon-button:nth-child(1) { - background: #ab92e1; -} - -#menu .icon-button:nth-child(2) { - background: #f3ea5d; -} - -#menu .icon-button:nth-child(3) { - background: #7ae1bf; -} - -#menu .icon-button:nth-child(4) { - background: #ff5869; -} - -#menu .icon-button:nth-child(5) { - background: #fc76d9; -} - -#menu .icon-button:nth-child(6) { - background: #fc76d9; -} - -==> ./monkeytype/static/themes/bouquet.css <== -:root { - --bg-color: #173f35; - --main-color: #eaa09c; - --caret-color: #eaa09c; - --sub-color: #408e7b; - --text-color: #e9e0d2; - --error-color: #d44729; - --error-extra-color: #8f2f19; - --colorful-error-color: #d44729; - --colorful-error-extra-color: #8f2f19; -} - -==> ./monkeytype/static/themes/ryujinscales.css <== -:root { - --bg-color: #081426; - --main-color: #f17754; - --caret-color: #ef6d49; - --sub-color: #ffbc90; - --text-color: #ffe4bc; - --error-color: #ca4754; - --error-extra-color: #7e2a33; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; - } - -/* your theme has been added to the _list file and the textColor property is the theme's main color */ -==> ./monkeytype/static/themes/graen.css <== -:root { - --bg-color: #303c36; - --main-color: #a59682; - --caret-color: #601420; - --sub-color: #181d1a; - --text-color: #a59682; - --error-color: #601420; - --error-extra-color: #5f0715; - --colorful-error-color: #601420; - --colorful-error-extra-color: #5f0715; -} - -#menu .icon-button:nth-child(1), -#menu .icon-button:nth-child(2), -#menu .icon-button:nth-child(3), -#menu .icon-button:nth-child(4), -#menu .icon-button:nth-child(5), -#menu .icon-button:nth-child(6) { - color: #601420; -} - -==> ./monkeytype/static/themes/mountain.css <== -:root { - --bg-color: #0f0f0f; - --main-color: #e7e7e7; - --caret-color: #f5f5f5; - --sub-color: #4c4c4c; - --text-color: #e7e7e7; - --error-color: #ac8c8c; - --error-extra-color: #c49ea0; - --colorful-error-color: #aca98a; - --colorful-error-extra-color: #c4c19e; -} - -==> ./monkeytype/static/themes/voc.css <== -:root { - --bg-color: #190618; - --main-color: #e0caac; - --caret-color: #e0caac; - --sub-color: #4c1e48; - --text-color: #eeeae4; - --error-color: #af3735; - --error-extra-color: #7e2a29; - --colorful-error-color: #af3735; - --colorful-error-extra-color: #7e2a29; -} - -==> ./monkeytype/static/themes/norse.css <== -:root { - --bg-color: #242425; - --main-color: #2b5f6d; - --caret-color: #2b5f6d; - --sub-color: #505b5e; - --text-color: #ccc2b1; - --error-color: #7e2a2a; - --error-extra-color: #771d1d; - --colorful-error-color: #ca4754; - --colorful-error-extra-color: #7e2a33; -} - -==> ./monkeytype/static/themes/rose_pine_moon.css <== -:root { - --bg-color: #2a273f; /*Background*/ - --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ - --caret-color: #f6c177; /*Cursor Color*/ - --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ - --text-color: #e0def4; /*Color of text after hovering over it*/ - --error-color: #eb6f92; - --error-extra-color: #ebbcba; - --colorful-error-color: #eb6f92; - --colorful-error-extra-color: #ebbcba; -} - -==> ./monkeytype/static/themes/80s_after_dark.css <== -:root { - --bg-color: #1b1d36; - --main-color: #fca6d1; - --caret-color: #99d6ea; - --sub-color: #99d6ea; - --text-color: #e1e7ec; - --error-color: #fffb85; - --error-extra-color: #fffb85; - --colorful-error-color: #fffb85; - --colorful-error-extra-color: #fffb85; -} - -==> ./monkeytype/static/themes/peaches.css <== -:root { - --bg-color: #e0d7c1; - --main-color: #dd7a5f; - --caret-color: #dd7a5f; - --sub-color: #e7b28e; - --text-color: #5f4c41; - --error-color: #ff6961; - --error-extra-color: #c23b22; - --colorful-error-color: #ff6961; - --colorful-error-extra-color: #c23b22; -} - -==> ./monkeytype/static/sw.js <== -const staticCacheName = "sw-cache"; // this is given a unique name on build - -self.addEventListener("activate", (event) => { - caches.keys().then((names) => { - for (let name of names) { - if (name !== staticCacheName) event.waitUntil(caches.delete(name)); - } - }); - event.waitUntil(self.clients.claim()); -}); - -self.addEventListener("install", (event) => { - event.waitUntil(self.skipWaiting()); - event.waitUntil( - caches.open(staticCacheName).then((cache) => { - // Cache the base file(s) - return cache.add("/"); - }) - ); -}); - -self.addEventListener("fetch", async (event) => { - const host = new URL(event.request.url).host; - if ( - [ - "localhost:5005", - "api.monkeytype.com", - "api.github.com", - "www.google-analytics.com", - ].includes(host) || - host.endsWith("wikipedia.org") - ) { - // if hostname is a non-static api, fetch request - event.respondWith(fetch(event.request)); - } else { - // Otherwise, assume host is serving a static file, check cache and add response to cache if not found - event.respondWith( - caches.open(staticCacheName).then((cache) => { - return cache.match(event.request).then(async (response) => { - // Check if request in cache - if (response) { - // if response was found in the cache, send from cache - return response; - } else { - // if response was not found in cache fetch from server, cache it and send it - response = await fetch(event.request); - cache.put(event.request.url, response.clone()); - return response; - } - }); - }) - ); - } -}); - -==> ./monkeytype/.nvmrc <== -14.18.1 -==> ./monkeytype/.prettierrc <== -{ - "tabWidth": 2, - "useTabs": false, - "htmlWhitespaceSensitivity": "ignore" -} - -==> ./monkeytype/gulpfile.js <== -const { task, src, dest, series, watch } = require("gulp"); -const axios = require("axios"); -const browserify = require("browserify"); -const babelify = require("babelify"); -const concat = require("gulp-concat"); -const del = require("del"); -const source = require("vinyl-source-stream"); -const buffer = require("vinyl-buffer"); -const vinylPaths = require("vinyl-paths"); -const eslint = require("gulp-eslint"); -var sass = require("gulp-sass")(require("dart-sass")); -const replace = require("gulp-replace"); -const uglify = require("gulp-uglify"); -// sass.compiler = require("dart-sass"); - -let eslintConfig = { - parser: "babel-eslint", - globals: [ - "jQuery", - "$", - "firebase", - "moment", - "html2canvas", - "ClipboardItem", - "grecaptcha", - ], - envs: ["es6", "browser", "node"], - plugins: ["json"], - extends: ["plugin:json/recommended"], - rules: { - "json/*": ["error"], - "constructor-super": "error", - "for-direction": "error", - "getter-return": "error", - "no-async-promise-executor": "error", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-args": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "warn", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": ["warn", { allowEmptyCatch: true }], - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", - "no-extra-semi": "error", - "no-fallthrough": "error", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-inner-declarations": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "warn", - "no-misleading-character-class": "error", - "no-mixed-spaces-and-tabs": "error", - "no-new-symbol": "error", - "no-obj-calls": "error", - "no-octal": "error", - "no-prototype-builtins": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", - "no-undef": "error", - "no-unexpected-multiline": "warn", - "no-unreachable": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unused-labels": "error", - "no-unused-vars": ["warn", { argsIgnorePattern: "e|event" }], - "no-use-before-define": "warn", - "no-useless-catch": "error", - "no-useless-escape": "error", - "no-with": "error", - "require-yield": "error", - "use-isnan": "error", - "valid-typeof": "error", - }, -}; - -//refactored files, which should be es6 modules -//once all files are moved here, then can we use a bundler to its full potential -const refactoredSrc = [ - "./src/js/axios-instance.js", - "./src/js/db.js", - "./src/js/misc.js", - "./src/js/layouts.js", - "./src/js/sound.js", - "./src/js/theme-colors.js", - "./src/js/chart-controller.js", - "./src/js/theme-controller.js", - "./src/js/config.js", - "./src/js/tag-controller.js", - "./src/js/preset-controller.js", - "./src/js/ui.js", - "./src/js/commandline.js", - "./src/js/commandline-lists.js", - "./src/js/commandline.js", - "./src/js/challenge-controller.js", - "./src/js/mini-result-chart.js", - "./src/js/account-controller.js", - "./src/js/simple-popups.js", - "./src/js/settings.js", - "./src/js/input-controller.js", - "./src/js/route-controller.js", - "./src/js/ready.js", - "./src/js/monkey-power.js", - - "./src/js/account/all-time-stats.js", - "./src/js/account/pb-tables.js", - "./src/js/account/result-filters.js", - "./src/js/account/verification-controller.js", - "./src/js/account.js", - - "./src/js/elements/monkey.js", - "./src/js/elements/notifications.js", - "./src/js/elements/leaderboards.js", - "./src/js/elements/account-button.js", - "./src/js/elements/loader.js", - "./src/js/elements/sign-out-button.js", - "./src/js/elements/about-page.js", - "./src/js/elements/psa.js", - "./src/js/elements/new-version-notification.js", - "./src/js/elements/mobile-test-config.js", - "./src/js/elements/loading-page.js", - "./src/js/elements/scroll-to-top.js", - - "./src/js/popups/custom-text-popup.js", - "./src/js/popups/pb-tables-popup.js", - "./src/js/popups/quote-search-popup.js", - "./src/js/popups/quote-submit-popup.js", - "./src/js/popups/quote-approve-popup.js", - "./src/js/popups/rate-quote-popup.js", - "./src/js/popups/version-popup.js", - "./src/js/popups/support-popup.js", - "./src/js/popups/contact-popup.js", - "./src/js/popups/custom-word-amount-popup.js", - "./src/js/popups/custom-test-duration-popup.js", - "./src/js/popups/word-filter-popup.js", - "./src/js/popups/result-tags-popup.js", - "./src/js/popups/edit-tags-popup.js", - "./src/js/popups/edit-preset-popup.js", - "./src/js/popups/custom-theme-popup.js", - "./src/js/popups/import-export-settings-popup.js", - "./src/js/popups/custom-background-filter.js", - - "./src/js/settings/language-picker.js", - "./src/js/settings/theme-picker.js", - "./src/js/settings/settings-group.js", - - "./src/js/test/custom-text.js", - "./src/js/test/british-english.js", - "./src/js/test/lazy-mode.js", - "./src/js/test/shift-tracker.js", - "./src/js/test/out-of-focus.js", - "./src/js/test/caret.js", - "./src/js/test/manual-restart-tracker.js", - "./src/js/test/test-stats.js", - "./src/js/test/focus.js", - "./src/js/test/practise-words.js", - "./src/js/test/test-ui.js", - "./src/js/test/keymap.js", - "./src/js/test/result.js", - "./src/js/test/live-wpm.js", - "./src/js/test/caps-warning.js", - "./src/js/test/live-acc.js", - "./src/js/test/live-burst.js", - "./src/js/test/timer-progress.js", - "./src/js/test/test-logic.js", - "./src/js/test/funbox.js", - "./src/js/test/pace-caret.js", - "./src/js/test/pb-crown.js", - "./src/js/test/test-timer.js", - "./src/js/test/test-config.js", - "./src/js/test/layout-emulator.js", - "./src/js/test/poetry.js", - "./src/js/test/wikipedia.js", - "./src/js/test/today-tracker.js", - "./src/js/test/weak-spot.js", - "./src/js/test/wordset.js", - "./src/js/test/tts.js", - "./src/js/replay.js", -]; - -//legacy files -//the order of files is important -const globalSrc = ["./src/js/global-dependencies.js", "./src/js/exports.js"]; - -//concatenates and lints legacy js files and writes the output to dist/gen/index.js -task("cat", function () { - return src(globalSrc) - .pipe(concat("index.js")) - .pipe(eslint(eslintConfig)) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()) - .pipe(dest("./dist/gen")); -}); - -task("sass", function () { - return src("./src/sass/*.scss") - .pipe(concat("style.scss")) - .pipe(sass({ outputStyle: "compressed" }).on("error", sass.logError)) - .pipe(dest("dist/css")); -}); - -task("static", function () { - return src("./static/**/*", { dot: true }).pipe(dest("./dist/")); -}); - -//copies refactored js files to dist/gen so that they can be required by dist/gen/index.js -task("copy-modules", function () { - return src(refactoredSrc, { allowEmpty: true }).pipe(dest("./dist/gen")); -}); - -//bundles the refactored js files together with index.js (the concatenated legacy js files) -//it's odd that the entry point is generated, so we should seek a better way of doing this -task("browserify", function () { - const b = browserify({ - //index.js is generated by task "cat" - entries: "./dist/gen/index.js", - //a source map isn't very useful right now because - //the source files are concatenated together - debug: false, - }); - return b - .transform( - babelify.configure({ - presets: ["@babel/preset-env"], - plugins: ["@babel/transform-runtime"], - }) - ) - .bundle() - .pipe(source("monkeytype.js")) - .pipe(buffer()) - .pipe( - uglify({ - mangle: false, - }) - ) - .pipe(dest("./dist/js")); -}); - -//lints only the refactored files -task("lint", function () { - let filelist = refactoredSrc; - filelist.push("./static/**/*.json"); - return src(filelist) - .pipe(eslint(eslintConfig)) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()); -}); - -task("clean", function () { - return src("./dist/", { allowEmpty: true }).pipe(vinylPaths(del)); -}); - -task("updateSwCacheName", function () { - let date = new Date(); - let dateString = - date.getFullYear() + - "-" + - (date.getMonth() + 1) + - "-" + - date.getDate() + - "-" + - date.getHours() + - "-" + - date.getMinutes() + - "-" + - date.getSeconds(); - return src(["static/sw.js"]) - .pipe( - replace( - /const staticCacheName = .*;/g, - `const staticCacheName = "sw-cache-${dateString}";` - ) - ) - .pipe(dest("./dist/")); -}); - -task( - "compile", - series( - "lint", - "cat", - "copy-modules", - "browserify", - "static", - "sass", - "updateSwCacheName" - ) -); - -task("watch", function () { - watch(["./static/**/*", "./src/**/*"], series("compile")); -}); - -task("build", series("clean", "compile")); - - -==> monkeytype/src/sass/about.scss <== -.pageAbout { - display: grid; - gap: 2rem; - - .created { - text-align: center; - color: var(--sub-color); - a { - text-decoration: none; - } - } - - .section { - display: grid; - gap: 0.25rem; - - .title { - font-size: 2rem; - line-height: 2rem; - color: var(--sub-color); - margin: 1rem 0; - } - - .contactButtons, - .supportButtons { - margin-top: 1rem; - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 1rem; - .button { - text-decoration: none; - font-size: 1.5rem; - padding: 2rem 0; - .fas, - .fab { - margin-right: 1rem; - } - } - } - - .supporters, - .contributors { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 0.25rem; - color: var(--text-color); - } - - h1 { - font-size: 1rem; - line-height: 1rem; - color: var(--sub-color); - margin: 0; - font-weight: 300; - } - - p { - margin: 0; - padding: 0; - color: var(--text-color); - } - } -} - -==> monkeytype/src/sass/banners.scss <== -#bannerCenter { - position: fixed; - width: 100%; - z-index: 9999; - .banner { - background: var(--sub-color); - color: var(--bg-color); - display: flex; - justify-content: center; - .container { - max-width: 1000px; - display: grid; - grid-template-columns: auto 1fr auto; - gap: 1rem; - align-items: center; - width: 100%; - justify-items: center; - .image { - // background-image: url(images/merchdropwebsite2.png); - height: 2.3rem; - background-size: cover; - aspect-ratio: 6/1; - background-position: center; - background-repeat: no-repeat; - margin-left: 2rem; - } - .icon { - margin-left: 1rem; - margin-top: 0.5rem; - margin-bottom: 0.5rem; - } - .text { - margin-top: 0.5rem; - margin-bottom: 0.5rem; - } - .closeButton { - margin-right: 1rem; - margin-top: 0.5rem; - margin-bottom: 0.5rem; - transition: 0.125s; - &:hover { - cursor: pointer; - color: var(--text-color); - } - } - } - &.good { - background: var(--main-color); - } - &.bad { - background: var(--error-color); - } - } -} - -==> monkeytype/src/sass/popups.scss <== -.popupWrapper { - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.75); - position: fixed; - left: 0; - top: 0; - z-index: 1000; - display: grid; - justify-content: center; - align-items: center; - padding: 2rem 0; -} - -#customTextPopupWrapper { - #customTextPopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 60vw; - .wordfilter { - width: 33%; - justify-self: right; - } - - textarea { - background: rgba(0, 0, 0, 0.1); - padding: 1rem; - color: var(--main-color); - border: none; - outline: none; - font-size: 1rem; - font-family: var(--font); - width: 100%; - border-radius: var(--roundness); - resize: vertical; - height: 200px; - color: var(--text-color); - overflow-x: hidden; - overflow-y: scroll; - } - - .inputs { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - align-items: center; - justify-items: left; - } - - .randomInputFields { - display: grid; - grid-template-columns: 1fr auto 1fr; - text-align: center; - align-items: center; - width: 100%; - gap: 1rem; - } - } -} - -#wordFilterPopupWrapper { - #wordFilterPopup { - color: var(--sub-color); - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 400px; - - input { - width: 100%; - } - - .group { - display: grid; - gap: 0.5rem; - } - - .lengthgrid { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto 1fr; - column-gap: 1rem; - } - - .tip { - color: var(--sub-color); - font-size: 0.8rem; - } - - .loadingIndicator { - justify-self: center; - } - } -} - -#quoteRatePopupWrapper { - #quoteRatePopup { - color: var(--sub-color); - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 2rem; - width: 800px; - - display: grid; - grid-template-areas: "ratingStats ratingStats submitButton" "spacer spacer spacer" "quote quote quote"; - grid-template-columns: auto 1fr; - - color: var(--text-color); - - .spacer { - grid-area: spacer; - grid-column: 1/4; - width: 100%; - height: 0.1rem; - border-radius: var(--roundness); - background: var(--sub-color); - opacity: 0.25; - } - - .submitButton { - font-size: 2rem; - grid-area: submitButton; - color: var(--sub-color); - &:hover { - color: var(--text-color); - } - } - - .top { - color: var(--sub-color); - font-size: 0.8rem; - } - - .ratingStats { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 1rem; - grid-area: ratingStats; - .top { - font-size: 1rem; - } - .val { - font-size: 2.25rem; - } - } - - .quote { - display: grid; - grid-area: quote; - gap: 1rem; - grid-template-areas: - "text text text" - "id length source"; - grid-template-columns: 1fr 1fr 3fr; - .text { - grid-area: text; - } - .id { - grid-area: id; - } - .length { - grid-area: length; - } - .source { - grid-area: source; - } - } - - .stars { - display: grid; - color: var(--sub-color); - font-size: 2rem; - grid-template-columns: auto auto auto auto auto; - justify-content: flex-start; - align-items: center; - cursor: pointer; - } - .star { - transition: 0.125s; - } - i { - pointer-events: none; - } - .star.active { - color: var(--text-color); - } - } -} - -#simplePopupWrapper { - #simplePopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 400px; - - .title { - font-size: 1.5rem; - color: var(--sub-color); - } - - .inputs { - display: grid; - gap: 1rem; - } - - .text { - font-size: 1rem; - color: var(--text-color); - } - } -} - -#mobileTestConfigPopupWrapper { - #mobileTestConfigPopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 1rem; - display: grid; - gap: 1rem; - width: calc(100vw - 2rem); - // margin-left: 1rem; - max-width: 400px; - - .title { - font-size: 1.5rem; - color: var(--sub-color); - } - - .inputs { - display: grid; - gap: 1rem; - } - - .text { - font-size: 1rem; - color: var(--text-color); - } - - .group { - display: grid; - gap: 0.5rem; - } - } -} - -#customWordAmountPopupWrapper, -#customTestDurationPopupWrapper, -#practiseWordsPopupWrapper, -#pbTablesPopupWrapper { - #customWordAmountPopup, - #customTestDurationPopup, - #practiseWordsPopup, - #pbTablesPopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 400px; - - .title { - font-size: 1.5rem; - color: var(--sub-color); - } - - .tip { - font-size: 0.75rem; - color: var(--sub-color); - } - - .text { - font-size: 1rem; - color: var(--text-color); - } - } - - #customTestDurationPopup { - .preview { - font-size: 0.75rem; - color: var(--sub-color); - } - } -} - -#pbTablesPopupWrapper #pbTablesPopup { - .title { - color: var(--text-color); - } - min-width: 50rem; - max-height: calc(100vh - 10rem); - overflow-y: scroll; - table { - border-spacing: 0; - border-collapse: collapse; - color: var(--text-color); - - td { - padding: 0.5rem 0.5rem; - } - - thead { - color: var(--sub-color); - font-size: 0.75rem; - } - - tbody tr:nth-child(odd) td { - background: rgba(0, 0, 0, 0.1); - } - - td.infoIcons span { - margin: 0 0.1rem; - } - .miniResultChartButton { - opacity: 0.25; - transition: 0.25s; - cursor: pointer; - &:hover { - opacity: 1; - } - } - .sub { - opacity: 0.5; - } - td { - text-align: right; - } - td:nth-child(6), - td:nth-child(7) { - text-align: center; - } - tbody td:nth-child(1) { - font-size: 1.5rem; - } - } -} - -#customThemeShareWrapper { - #customThemeShare { - width: 50vw; - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - overflow-y: scroll; - } -} - -#quoteSearchPopupWrapper { - #quoteSearchPopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 80vw; - max-width: 1000px; - height: 80vh; - grid-template-rows: auto auto auto 1fr; - - #quoteSearchTop { - display: flex; - justify-content: space-between; - - .title { - font-size: 1.5rem; - color: var(--sub-color); - } - - .buttons { - width: 33%; - display: grid; - gap: 0.5rem; - .button { - width: 100%; - } - } - } - - #extraResults { - text-align: center; - color: var(--sub-color); - } - #quoteSearchResults { - display: grid; - gap: 0.5rem; - height: auto; - overflow-y: scroll; - - .searchResult { - display: grid; - grid-template-columns: 1fr 1fr 3fr 0fr; - grid-template-areas: - "text text text text" - "id len source report"; - grid-auto-rows: auto; - width: 100%; - gap: 0.5rem; - transition: 0.25s; - padding: 1rem; - box-sizing: border-box; - user-select: none; - cursor: pointer; - height: min-content; - - .text { - grid-area: text; - overflow: visible; - color: var(--text-color); - } - .id { - grid-area: id; - font-size: 0.8rem; - color: var(--sub-color); - } - .length { - grid-area: len; - font-size: 0.8rem; - color: var(--sub-color); - } - .source { - grid-area: source; - font-size: 0.8rem; - color: var(--sub-color); - } - .resultChevron { - grid-area: chevron; - display: flex; - align-items: center; - justify-items: center; - color: var(--sub-color); - font-size: 2rem; - } - .report { - grid-area: report; - color: var(--sub-color); - transition: 0.25s; - &:hover { - color: var(--text-color); - } - } - .sub { - opacity: 0.5; - } - } - .searchResult:hover { - background: rgba(0, 0, 0, 0.1); - border-radius: 5px; - } - } - } -} -#settingsImportWrapper { - #settingsImport { - width: 50vw; - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - overflow-y: scroll; - } -} - -#quoteSubmitPopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 1000px; - grid-template-rows: auto auto auto auto auto auto auto auto auto; - height: 100%; - max-height: 40rem; - overflow-y: scroll; - - label { - color: var(--sub-color); - margin-bottom: -1rem; - } - - .title { - font-size: 1.5rem; - color: var(--sub-color); - } - textarea { - resize: vertical; - width: 100%; - padding: 10px; - line-height: 1.2rem; - min-height: 5rem; - } - .characterCount { - position: absolute; - top: -1.25rem; - right: 0.25rem; - color: var(--sub-color); - user-select: none; - &.red { - color: var(--error-color); - } - } -} - -#quoteApprovePopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 1000px; - height: 80vh; - grid-template-rows: auto 1fr; - - .top { - display: flex; - justify-content: space-between; - .title { - font-size: 1.5rem; - color: var(--sub-color); - } - .button { - width: 33%; - } - } - - .quotes { - display: grid; - gap: 1rem; - height: auto; - overflow-y: scroll; - align-content: baseline; - - .quote { - display: grid; - grid-template-columns: 1fr auto; - grid-auto-rows: auto 2rem; - width: 100%; - gap: 1rem; - transition: 0.25s; - box-sizing: border-box; - user-select: none; - height: min-content; - margin-bottom: 1rem; - - .text { - grid-column: 1/2; - grid-row: 1/2; - overflow: visible; - color: var(--text-color); - resize: vertical; - min-height: 4rem; - } - .source { - grid-column: 1/2; - grid-row: 2/3; - color: var(--text-color); - } - .buttons { - display: flex; - flex-direction: column; - justify-content: center; - margin-right: 1rem; - grid-column: 2/3; - grid-row: 1/4; - color: var(--sub-color); - } - - .bottom { - display: flex; - justify-content: space-around; - color: var(--sub-color); - .length.red { - color: var(--error-color); - } - } - - .sub { - opacity: 0.5; - } - } - .searchResult:hover { - background: rgba(0, 0, 0, 0.1); - border-radius: 5px; - } - } -} - -#quoteReportPopupWrapper { - #quoteReportPopup { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - width: 1000px; - grid-template-rows: auto auto auto auto auto auto auto auto auto; - height: auto; - max-height: 40rem; - overflow-y: scroll; - - label { - color: var(--sub-color); - margin-bottom: -1rem; - } - - .text { - color: var(--sub-color); - } - - .quote { - font-size: 1.5rem; - } - - .title { - font-size: 1.5rem; - color: var(--sub-color); - } - - textarea { - resize: vertical; - width: 100%; - padding: 10px; - line-height: 1.2rem; - min-height: 5rem; - } - - .characterCount { - position: absolute; - top: -1.25rem; - right: 0.25rem; - color: var(--sub-color); - user-select: none; - &.red { - color: var(--error-color); - } - } - } -} - -#resultEditTagsPanelWrapper { - #resultEditTagsPanel { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - overflow-y: scroll; - width: 500px; - - .buttons { - display: grid; - gap: 0.1rem; - grid-template-columns: 1fr 1fr 1fr; - } - } -} - -#versionHistoryWrapper { - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.75); - position: fixed; - left: 0; - top: 0; - z-index: 1000; - display: grid; - justify-content: center; - align-items: start; - padding: 5rem 0; - - #versionHistory { - width: 75vw; - height: 100%; - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - @extend .ffscroll; - overflow-y: scroll; - - .tip { - text-align: center; - color: var(--sub-color); - } - - .releases { - display: grid; - gap: 4rem; - - .release { - display: grid; - grid-template-areas: - "title date" - "body body"; - - .title { - grid-area: title; - font-size: 2rem; - color: var(--sub-color); - } - - .date { - grid-area: date; - text-align: right; - color: var(--sub-color); - align-self: center; - } - - .body { - grid-area: body; - color: var(--text-color); - } - - &:last-child { - margin-bottom: 2rem; - } - } - } - } -} - -#supportMeWrapper { - #supportMe { - width: 900px; - // height: 400px; - overflow-y: scroll; - max-height: 100%; - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - grid-template-rows: auto auto auto; - gap: 2rem; - @extend .ffscroll; - - .title { - font-size: 2rem; - line-height: 2rem; - color: var(--main-color); - } - - .text { - color: var(--text-color); - } - - .subtext { - color: var(--sub-color); - font-size: 0.75rem; - } - - .buttons { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 1rem; - - .button { - display: block; - width: 100%; - height: 100%; - padding: 2rem 0; - display: grid; - gap: 1rem; - text-decoration: none; - .text { - transition: 0.25s; - } - &:hover .text { - color: var(--bg-color); - } - .icon { - font-size: 5rem; - line-height: 5rem; - } - } - } - } -} - -#contactPopupWrapper { - #contactPopup { - // height: 400px; - overflow-y: scroll; - max-height: 100%; - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - grid-template-rows: auto auto auto; - gap: 2rem; - @extend .ffscroll; - margin: 0 2rem; - max-width: 900px; - - .title { - font-size: 2rem; - line-height: 2rem; - color: var(--main-color); - } - - .text { - color: var(--text-color); - span { - color: var(--error-color); - } - } - - .subtext { - color: var(--sub-color); - font-size: 0.75rem; - grid-area: subtext; - } - - .buttons { - display: grid; - gap: 1rem; - grid-template-columns: 1fr 1fr; - - .button { - display: block; - width: 100%; - height: 100%; - padding: 1rem 0; - display: grid; - // gap: 0.5rem; - text-decoration: none; - grid-template-areas: "icon textgroup"; - grid-template-columns: auto 1fr; - text-align: left; - align-items: center; - .textGroup { - grid-area: textgroup; - } - .text { - font-size: 1.5rem; - line-height: 2rem; - transition: 0.25s; - } - &:hover .text { - color: var(--bg-color); - } - .icon { - grid-area: icon; - font-size: 2rem; - line-height: 2rem; - padding: 0 1rem; - } - } - } - } -} - -#presetWrapper { - #presetEdit { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - overflow-y: scroll; - } -} - -#tagsWrapper { - #tagsEdit { - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 1rem; - overflow-y: scroll; - } -} - -==> monkeytype/src/sass/account.scss <== -.signOut { - font-size: 1rem; - line-height: 1rem; - justify-self: end; - // background: var(--sub-color); - color: var(--sub-color); - width: fit-content; - width: -moz-fit-content; - padding: 0.5rem; - border-radius: var(--roundness); - cursor: pointer; - transition: 0.25s; - float: right; - - &:hover { - color: var(--text-color); - } - - .fas { - margin-right: 0.5rem; - } -} - -.pageAccount { - display: grid; - gap: 1rem; - - .content { - display: grid; - gap: 2rem; - } - - .sendVerificationEmail { - cursor: pointer; - } - - .timePbTable, - .wordsPbTable { - .sub { - opacity: 0.5; - } - td { - text-align: right; - } - tbody td:nth-child(1) { - font-size: 1.5rem; - } - } - - .showAllTimePbs, - .showAllWordsPbs { - margin-top: 1rem; - } - - .topFilters .buttons { - display: flex; - justify-content: space-evenly; - gap: 1rem; - .button { - width: 100%; - } - } - - .miniResultChartWrapper { - // pointer-events: none; - z-index: 999; - display: none; - height: 15rem; - background: var(--bg-color); - width: 45rem; - position: absolute; - border-radius: var(--roundness); - padding: 1rem; - // box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25); - } - - .miniResultChartBg { - display: none; - z-index: 998; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.25); - position: fixed; - left: 0; - top: 0; - } - - .doublegroup { - display: grid; - grid-auto-flow: column; - gap: 1rem; - .titleAndTable { - .title { - color: var(--sub-color); - } - } - } - - .triplegroup { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 1rem; - - .text { - align-self: center; - color: var(--sub-color); - } - } - - .group { - &.noDataError { - margin: 20rem 0; - // height: 30rem; - // line-height: 30rem; - text-align: center; - } - - &.createdDate { - text-align: center; - color: var(--sub-color); - } - - &.personalBestTables { - .tables { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - } - } - - &.history { - .active { - animation: flashHighlight 4s linear 0s 1; - } - - .loadMoreButton { - background: rgba(0, 0, 0, 0.1); - color: var(--text-color); - text-align: center; - padding: 0.5rem; - border-radius: var(--roundness); - cursor: pointer; - -webkit-transition: 0.25s; - transition: 0.25s; - -webkit-user-select: none; - display: -ms-grid; - display: grid; - -ms-flex-line-pack: center; - align-content: center; - margin-top: 1rem; - - &:hover, - &:focus { - color: var(--bg-color); - background: var(--text-color); - } - } - } - - .title { - color: var(--sub-color); - } - - .val { - font-size: 3rem; - line-height: 3.5rem; - } - - .chartjs-render-monitor { - width: 100% !important; - } - - &.chart { - position: relative; - - .above { - display: flex; - justify-content: center; - margin-bottom: 1rem; - color: var(--sub-color); - flex-wrap: wrap; - - .group { - display: flex; - align-items: center; - } - - .fas, - .punc { - margin-right: 0.25rem; - } - - .spacer { - width: 1rem; - } - } - - .below { - text-align: center; - color: var(--sub-color); - margin-top: 1rem; - display: grid; - grid-template-columns: auto 300px; - align-items: center; - .text { - height: min-content; - } - .buttons { - display: grid; - gap: 0.5rem; - } - } - .chart { - height: 400px; - } - .chartPreloader { - position: absolute; - width: 100%; - background: rgba(0, 0, 0, 0.5); - height: 100%; - display: grid; - align-items: center; - justify-content: center; - font-size: 5rem; - text-shadow: 0 0 3rem black; - } - } - } - - table { - border-spacing: 0; - border-collapse: collapse; - color: var(--text-color); - - td { - padding: 0.5rem 0.5rem; - } - - thead { - color: var(--sub-color); - font-size: 0.75rem; - } - - tbody tr:nth-child(odd) td { - background: rgba(0, 0, 0, 0.1); - } - - td.infoIcons span { - margin: 0 0.1rem; - } - .miniResultChartButton { - opacity: 0.25; - transition: 0.25s; - cursor: pointer; - &:hover { - opacity: 1; - } - } - } - - #resultEditTags { - transition: 0.25s; - &:hover { - cursor: pointer; - color: var(--text-color); - opacity: 1 !important; - } - } -} - -.pageAccount { - .group.filterButtons { - gap: 1rem; - display: grid; - grid-template-columns: 1fr 1fr; - - .buttonsAndTitle { - height: fit-content; - height: -moz-fit-content; - display: grid; - gap: 0.25rem; - color: var(--sub-color); - line-height: 1rem; - font-size: 1rem; - - &.testDate .buttons, - &.languages .buttons, - &.layouts .buttons, - &.funbox .buttons, - &.tags .buttons { - grid-template-columns: repeat(4, 1fr); - grid-auto-flow: unset; - } - } - - .buttons { - display: grid; - grid-auto-flow: column; - gap: 1rem; - - .button { - background: rgba(0, 0, 0, 0.1); - color: var(--text-color); - text-align: center; - padding: 0.5rem; - border-radius: var(--roundness); - cursor: pointer; - transition: 0.25s; - -webkit-user-select: none; - display: grid; - align-content: center; - - &.active { - background: var(--main-color); - color: var(--bg-color); - } - - &:hover { - color: var(--bg-color); - background: var(--main-color); - } - } - } - } -} - -.header-sorted { - font-weight: bold; -} - -.sortable:hover { - cursor: pointer; - user-select: none; - background-color: rgba(0, 0, 0, 0.1); -} - -==> monkeytype/src/sass/monkey.scss <== -#monkey { - width: 308px; - height: 0; - margin: 0 auto; - animation: shake 0s infinite; - div { - height: 200px; - width: 308px; - position: fixed; - } - .up { - background-image: url("../images/monkey/m3.png"); - } - .left { - background-image: url("../images/monkey/m1.png"); - } - .right { - background-image: url("../images/monkey/m2.png"); - } - .both { - background-image: url("../images/monkey/m4.png"); - } - .fast { - .up { - background-image: url("../images/monkey/m3_fast.png"); - } - .left { - background-image: url("../images/monkey/m1_fast.png"); - } - .right { - background-image: url("../images/monkey/m2_fast.png"); - } - .both { - background-image: url("../images/monkey/m4_fast.png"); - } - } -} - -==> monkeytype/src/sass/core.scss <== -@import url("https://fonts.googleapis.com/css2?family=Fira+Code&family=IBM+Plex+Sans:wght@600&family=Inconsolata&family=Roboto+Mono&family=Source+Code+Pro&family=JetBrains+Mono&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Montserrat&family=Roboto&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Lexend+Deca&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Oxygen&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Nunito&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Itim&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Comfortaa&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Coming+Soon&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Lato&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Lalezar&display=swap"); -@import url("https://fonts.googleapis.com/css?family=Noto+Naskh+Arabic&display=swap"); - -:root { - --roundness: 0.5rem; - --font: "Roboto Mono"; - // scroll-behavior: smooth; - scroll-padding-top: 2rem; -} - -::placeholder { - color: var(--sub-color); - opacity: 1; - /* Firefox */ -} - -#nocss { - display: none !important; - pointer-events: none; -} - -.ffscroll { - scrollbar-width: thin; - scrollbar-color: var(--sub-color) transparent; -} - -html { - @extend .ffscroll; - overflow-y: scroll; -} - -a { - display: inline-block; - color: var(--sub-color); - transition: 0.25s; - &:hover { - color: var(--text-color); - } -} - -body { - margin: 0; - padding: 0; - min-height: 100vh; - font-family: var(--font); - color: var(--text-color); - overflow-x: hidden; - background: var(--bg-color); -} - -.customBackground { - content: ""; - width: 100vw; - height: 100vh; - position: fixed; - left: 0; - top: 0; - background-position: center center; - background-repeat: no-repeat; - z-index: -999; - justify-content: center; - align-items: center; - display: flex; -} - -#backgroundLoader { - height: 3px; - position: fixed; - width: 100%; - background: var(--main-color); - animation: loader 2s cubic-bezier(0.38, 0.16, 0.57, 0.82) infinite; - z-index: 9999; -} - -label.checkbox { - span { - display: block; - font-size: 0.76rem; - color: var(--sub-color); - margin-left: 1.5rem; - } - - input { - margin: 0 !important; - cursor: pointer; - width: 0; - height: 0; - display: none; - - & ~ .customTextCheckbox { - width: 12px; - height: 12px; - background: rgba(0, 0, 0, 0.1); - border-radius: 2px; - box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.1); - display: inline-block; - margin: 0 0.5rem 0 0.25rem; - transition: 0.25s; - } - - &:checked ~ .customTextCheckbox { - background: var(--main-color); - } - } -} - -#centerContent { - max-width: 1000px; - // min-width: 500px; - // margin: 0 auto; - display: grid; - grid-auto-flow: row; - min-height: 100vh; - padding-left: 2rem; - padding-right: 2rem; - padding-top: 2rem; - padding-bottom: 2rem; - gap: 2rem; - align-items: center; - z-index: 999; - grid-template-rows: auto 1fr auto; - width: 100%; - &.wide125 { - max-width: 1250px; - } - &.wide150 { - max-width: 1500px; - } - &.wide200 { - max-width: 2000px; - } - &.widemax { - max-width: unset; - } -} - -key { - color: var(--bg-color); - background-color: var(--sub-color); - /* font-weight: bold; */ - padding: 0.1rem 0.3rem; - margin: 3px 0; - border-radius: 0.1rem; - display: inline-block; - font-size: 0.7rem; - line-height: 0.7rem; -} - -.pageLoading { - display: grid; - justify-content: center; -} - -.pageLoading, -.pageAccount { - .preloader { - text-align: center; - justify-self: center; - display: grid; - .barWrapper { - display: grid; - gap: 1rem; - grid-row: 1; - grid-column: 1; - .bar { - width: 20rem; - height: 0.5rem; - background: rgba(0, 0, 0, 0.1); - border-radius: var(--roundness); - .fill { - height: 100%; - width: 0%; - background: var(--main-color); - border-radius: var(--roundness); - // transition: 1s; - } - } - } - .icon { - grid-row: 1; - grid-column: 1; - font-size: 2rem; - color: var(--main-color); - margin-bottom: 1rem; - } - } -} - -.devIndicator { - position: fixed; - font-size: 3rem; - color: var(--sub-color); - opacity: 0.25; - z-index: -1; - - &.tl { - top: 2rem; - left: 2rem; - } - - &.tr { - top: 2rem; - right: 2rem; - } - - &.bl { - bottom: 2rem; - left: 2rem; - } - - &.br { - bottom: 2rem; - right: 2rem; - } -} - -* { - box-sizing: border-box; -} - -.hidden { - display: none !important; -} - -.invisible { - opacity: 0 !important; - pointer-events: none !important; -} - -.button { - color: var(--text-color); - cursor: pointer; - transition: 0.25s; - padding: 0.4rem; - border-radius: var(--roundness); - background: rgba(0, 0, 0, 0.1); - text-align: center; - -webkit-user-select: none; - // display: grid; - align-content: center; - height: min-content; - height: -moz-min-content; - line-height: 1rem; - - &:hover { - color: var(--bg-color); - background: var(--text-color); - outline: none; - } - &:focus { - color: var(--main-color); - background: var(--sub-color); - outline: none; - } - - &.active { - background: var(--main-color); - color: var(--bg-color); - &:hover { - // color: var(--text-color); - background: var(--text-color); - outline: none; - } - &:focus { - color: var(--bg-color); - background: var(--main-color); - outline: none; - } - } - - &.disabled { - opacity: 0.5; - cursor: default; - pointer-events: none; - &:hover { - color: var(--text-color); - background: rgba(0, 0, 0, 0.1); - outline: none; - } - } - - &.disabled.active { - opacity: 0.5; - cursor: default; - pointer-events: none; - &:hover { - color: var(--bg-color); - background: var(--main-color); - outline: none; - } - } -} - -.text-button { - transition: 0.25s; - color: var(--sub-color); - cursor: pointer; - margin-right: 0.25rem; - cursor: pointer; - outline: none; - - &.active { - color: var(--main-color); - } - - &:hover, - &:focus { - color: var(--text-color); - } -} - -.icon-button { - display: grid; - grid-auto-flow: column; - align-content: center; - transition: 0.25s; - padding: 0.5rem; - border-radius: var(--roundness); - cursor: pointer; - - &:hover { - color: var(--text-color); - } - &:focus { - // background: var(--sub-color); - color: var(--sub-color); - border: none; - outline: none; - } - &.disabled { - opacity: 0.5; - cursor: default; - pointer-events: none; - } -} - -.scrollToTopButton { - bottom: 2rem; - right: 2rem; - position: fixed; - font-size: 2rem; - width: 4rem; - height: 4rem; - text-align: center; - line-height: 4rem; - background: var(--bg-color); - border-radius: 99rem; - z-index: 99; - cursor: pointer; - color: var(--sub-color); - transition: 0.25s; - &:hover { - color: var(--text-color); - } -} - -==> monkeytype/src/sass/login.scss <== -.pageLogin { - display: flex; - grid-auto-flow: column; - gap: 1rem; - justify-content: space-around; - align-items: center; - - .side { - display: grid; - gap: 0.5rem; - justify-content: center; - grid-template-columns: 1fr; - - input { - width: 15rem; - } - - &.login { - grid-template-areas: - "title forgotButton" - "form form"; - - .title { - grid-area: title; - } - - #forgotPasswordButton { - grid-area: forgotButton; - font-size: 0.75rem; - line-height: 0.75rem; - height: fit-content; - height: -moz-fit-content; - align-self: center; - justify-self: right; - padding: 0.25rem 0; - color: var(--sub-color); - cursor: pointer; - transition: 0.25s; - - &:hover { - color: var(--text-color); - } - } - - form { - grid-area: form; - } - } - } - - form { - display: grid; - gap: 0.5rem; - width: 100%; - } - - .preloader { - position: fixed; - left: 50%; - top: 50%; - font-size: 2rem; - transform: translate(-50%, -50%); - color: var(--main-color); - transition: 0.25s; - } -} - -==> monkeytype/src/sass/z_media-queries.scss <== -@media only screen and (max-width: 1200px) { - #leaderboardsWrapper { - #leaderboards { - .tables { - grid-template-columns: unset; - } - .tables .rightTableWrapper, - .tables .leftTableWrapper { - height: calc(50vh - 12rem); - } - } - } -} - -@media only screen and (max-width: 1050px) { - .pageSettings .section.fullWidth .buttons { - grid-template-columns: 1fr 1fr 1fr; - } - - #result .morestats { - gap: 1rem; - grid-template-rows: 1fr 1fr; - } - #supportMe { - width: 90vw !important; - .buttons { - .button { - .icon { - font-size: 3rem !important; - line-height: 3rem !important; - } - } - } - } - #customTextPopup { - width: 80vw !important; - - .wordfilter.button { - width: 50% !important; - } - } -} - -@media only screen and (max-width: 1000px) { - #quoteRatePopup { - width: 90vw !important; - } - #bottom { - .leftright { - .left { - gap: 0.25rem 1rem; - display: grid; - grid-template-rows: 1fr 1fr; - grid-auto-flow: row; - grid-template-columns: auto auto auto auto; - } - .right { - display: grid; - grid-template-rows: 1fr 1fr; - gap: 0.25rem 1rem; - } - } - } -} - -@media only screen and (max-width: 900px) { - // #leaderboards { - // .mainTitle { - // font-size: 1.5rem !important; - // line-height: 1.5rem !important; - // } - // } - .merchBanner { - img { - display: none; - } - .text { - padding: 0.25rem 0; - } - } - .pageAccount { - .group.personalBestTables { - .tables { - grid-template-columns: 1fr; - } - } - .group.history { - table { - thead, - tbody { - td:nth-child(1), - td:nth-child(8), - td:nth-child(9) { - display: none; - } - } - } - } - } -} - -@media only screen and (max-width: 800px) { - .pageSettings .settingsGroup.quickNav .links { - grid-auto-flow: unset; - grid-template-columns: 1fr 1fr 1fr; - justify-items: center; - } - #bannerCenter .banner .container { - grid-template-columns: 1fr auto; - .image { - display: none; - } - .lefticon { - display: none; - } - .text { - margin-left: 2rem; - } - } - #centerContent { - #top { - grid-template-areas: - "logo config" - "menu config"; - grid-template-columns: auto auto; - .logo { - margin-bottom: 0; - } - } - - #menu { - gap: 0.5rem; - font-size: 0.8rem; - line-height: 0.8rem; - margin-top: -0.5rem; - - .icon-button { - padding: 0.25rem; - } - } - } - - #contactPopupWrapper #contactPopup .buttons { - grid-template-columns: 1fr; - } - - .pageAbout .section { - .contributors, - .supporters { - grid-template-columns: 1fr 1fr 1fr; - } - .contactButtons, - .supportButtons { - grid-template-columns: 1fr 1fr; - } - } - - .pageSettings .section.customBackgroundFilter { - .groups { - grid-template-columns: 1fr; - } - .saveContainer { - grid-column: -1/-2; - } - } - - .pageSettings { - .section.themes .tabContent.customTheme { - } - } - - #commandLine, - #commandLineInput { - width: 600px !important; - } -} - -@media only screen and (max-width: 700px) { - #leaderboardsWrapper { - #leaderboards { - .leaderboardsTop { - flex-direction: column; - align-items: baseline; - } - } - } - .pageAccount { - .triplegroup { - grid-template-columns: 1fr 1fr; - .emptygroup { - display: none; - } - } - .group.chart .below { - grid-template-columns: 1fr; - gap: 0.5rem; - } - .group.topFilters .buttonsAndTitle .buttons { - display: grid; - justify-content: unset; - } - .group.history { - table { - thead, - tbody { - td:nth-child(6) { - display: none; - } - } - } - } - } -} - -@media only screen and (max-width: 650px) { - #quoteRatePopup { - .ratingStats { - grid-template-columns: 1fr 1fr !important; - } - .quote { - grid-template-areas: - "text text text" - "source source source" - "id length length" !important; - } - } - .pageSettings .section { - grid-template-columns: 1fr; - grid-template-areas: - "title" - "text" - "buttons"; - - & > .text { - margin-bottom: 1rem; - } - } - - #result { - .buttons { - grid-template-rows: 1fr 1fr 1fr; - #nextTestButton { - grid-column: 1/5; - width: 100%; - text-align: center; - } - } - } - - #supportMe { - width: 80vw !important; - .buttons { - grid-template-columns: none !important; - .button { - grid-template-columns: auto auto; - align-items: center; - .icon { - font-size: 2rem !important; - line-height: 2rem !important; - } - } - } - } - - .pageSettings .section.fullWidth .buttons { - grid-template-columns: 1fr 1fr; - } -} - -@media only screen and (max-width: 600px) { - .pageAbout .section .supporters, - .pageAbout .section .contributors { - grid-template-columns: 1fr 1fr; - } - #top .logo .bottom { - margin-top: 0; - } - .pageLogin { - display: grid; - gap: 5rem; - grid-auto-flow: unset; - } - #middle { - #result { - grid-template-areas: - "stats stats" - "chart chart" - "morestats morestats"; - .stats { - grid-template-areas: "wpm acc"; - gap: 2rem; - } - .stats.morestats { - grid-template-rows: 1fr 1fr 1fr; - gap: 1rem; - } - } - } - #commandLine, - #commandLineInput { - width: 500px !important; - } - #customTextPopupWrapper { - #customTextPopup { - .wordfilter.button { - width: 100% !important; - justify-self: auto; - } - - .inputs { - display: flex !important; - flex-direction: column; - justify-content: flex-start; - } - } - } - #leaderboardsWrapper #leaderboards { - padding: 1rem; - gap: 1rem; - .mainTitle { - font-size: 2rem; - line-height: 2rem; - } - .title { - font-size: 1rem; - } - .leaderboardsTop { - .buttonGroup { - gap: 0.1rem !important; - - .button { - padding: 0.4rem !important; - font-size: 0.7rem !important; - } - } - } - } - .pageAccount { - .group.history { - table { - thead, - tbody { - td:nth-child(7), - td:nth-child(5) { - display: none; - } - } - } - } - } -} - -@media only screen and (max-width: 550px) { - .keymap { - .row { - height: 1.25rem; - } - .keymap-key { - width: 1.25rem; - height: 1.25rem; - border-radius: 0.3rem; - font-size: 0.6rem; - } - } - - #contactPopupWrapper #contactPopup .buttons .button .text { - font-size: 1rem; - } - #contactPopupWrapper #contactPopup .buttons .button .icon { - font-size: 1.5rem; - line-height: 1.5rem; - } - #contactPopupWrapper #contactPopup { - padding: 1rem; - } - .pageAbout .section .supporters, - .pageAbout .section .contributors { - grid-template-columns: 1fr; - } - - #simplePopupWrapper #simplePopup { - width: 90vw; - } - - .pageSettings { - .settingsGroup.quickNav { - display: none; - } - .section.fullWidth .buttons { - grid-template-columns: 1fr; - } - .section .buttons { - grid-auto-flow: row; - } - .section.customBackgroundFilter .groups .group { - grid-template-columns: auto 1fr; - .title { - grid-column: 1/3; - } - } - } - - .pageAbout .section { - .contactButtons, - .supportButtons { - grid-template-columns: 1fr; - } - } - - .pageAccount { - .triplegroup { - grid-template-columns: 1fr; - } - .group.history { - table { - thead, - tbody { - td:nth-child(3) { - display: none; - } - } - } - } - } - - #top { - align-items: self-end; - .logo { - .icon { - width: 1.5rem !important; - } - .text { - font-size: 1.5rem !important; - margin-bottom: 0.3rem !important; - } - .bottom { - font-size: 1.75rem; - line-height: 1.75rem; - margin-top: 0; - } - .top { - display: none; - } - } - #menu { - .icon-button { - padding: 0; - } - } - } - #bottom { - .leftright { - .left { - gap: 0.25rem 1rem; - display: grid; - grid-template-rows: 1fr 1fr 1fr; - grid-template-columns: auto auto auto; - grid-auto-flow: row; - } - .right { - display: grid; - grid-template-rows: 1fr 1fr 1fr; - gap: 0.25rem 1rem; - } - } - } - #centerContent { - #top { - grid-template-columns: 1fr auto; - .desktopConfig { - display: none; - } - .mobileConfig { - display: block; - } - } - padding: 1rem; - } - #middle { - #result { - .stats { - grid-template-areas: - "wpm" - "acc"; - gap: 1rem; - } - } - } - #result { - .buttons { - grid-template-rows: 1fr 1fr 1fr 1fr; - #nextTestButton { - grid-column: 1/3; - width: 100%; - text-align: center; - } - } - } - #commandLine, - #commandLineInput { - width: 400px !important; - } -} - -@media only screen and (max-width: 400px) { - #top .logo .bottom { - font-size: 1.5rem; - line-height: 1.5rem; - margin-top: 0; - } - - #top .config { - grid-gap: 0.25rem; - .group .buttons { - font-size: 0.65rem; - line-height: 0.65rem; - } - } - - #bottom { - font-size: 0.65rem; - .leftright { - grid-template-columns: 1fr 1fr; - .left { - grid-template-rows: 1fr 1fr 1fr 1fr; - grid-template-columns: 1fr 1fr; - grid-auto-flow: row; - } - .right { - // justify-self: left; - // grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr 1fr 1fr 1fr; - gap: 0.25rem 1rem; - } - } - } - - #commandLine, - #commandLineInput { - width: 300px !important; - } - - #leaderboardsWrapper #leaderboards .tables .titleAndTable .titleAndButtons { - grid-template-columns: unset; - } -} - -@media only screen and (max-width: 350px) { - .keymap { - display: none !important; - } - .pageLogin .side input { - width: 90vw; - } -} - -@media (hover: none) and (pointer: coarse) { - #commandLineMobileButton { - display: block !important; - } -} - -==> monkeytype/src/sass/inputs.scss <== -input, -textarea { - outline: none; - border: none; - border-radius: var(--roundness); - background: rgba(0, 0, 0, 0.1); - color: var(--text-color); - padding: 0.5rem; - font-size: 1rem; - font-family: var(--font); -} - -input[type="range"] { - -webkit-appearance: none; - padding: 0; - width: 100%; - height: 1rem; - border-radius: var(--roundness); - &::-webkit-slider-thumb { - -webkit-appearance: none; - padding: 0; - border: none; - width: 2rem; - height: 1rem; - border-radius: var(--roundness); - background-color: var(--main-color); - } - - &::-moz-range-thumb { - -webkit-appearance: none; - padding: 0; - border: none; - width: 2rem; - height: 1rem; - border-radius: var(--roundness); - background-color: var(--main-color); - } -} - -input[type="color"] { - height: 3px; //i dont know why its 3, but safari gods have spoken - 3 makes it work - opacity: 0; - padding: 0; - margin: 0; - position: absolute; - pointer-events: none; -} - -::-moz-color-swatch { - border: none; -} - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - margin: 0; -} - -input[type="number"] { - -moz-appearance: textfield; -} - -.select2-dropdown { - background-color: var(--bg-color); - color: var(--text-color); - outline: none; -} - -.select2-selection { - background: rgba(0, 0, 0, 0.1); - height: fit-content; - height: -moz-fit-content; - padding: 5px; - border-radius: var(--roundness); - color: var(--text-color); - font: var(--font); - border: none; - outline: none; -} - -.select2-container--default - .select2-selection--single - .select2-selection__rendered { - color: var(--text-color); - outline: none; -} - -.select2-container--default - .select2-results__option--highlighted.select2-results__option--selectable { - background-color: var(--text-color); - color: var(--bg-color); -} - -.select2-container--default .select2-results__option--selected { - background-color: var(--bg-color); - color: var(--sub-color); -} - -.select2-container--open .select2-dropdown--below { - border-color: rgba(0, 0, 0, 0.1); - background: var(--bg-color); - color: var(--sub-color); - border-radius: var(--roundness); -} - -.select2-container--default .select2-selection--single { - color: var(--text-color); - background: rgba(0, 0, 0, 0.1); - outline: none; - border: none; - height: auto; -} - -.select2-selection:focus { - height: fit-content; - height: -moz-fit-content; - padding: 5px; - border-radius: var(--roundness); - color: var(--text-color); - font: var(--font); - border: none; - outline: none; -} -.select2-selection:active { - height: fit-content; - height: -moz-fit-content; - padding: 5px; - border-radius: var(--roundness); - color: var(--text-color); - font: var(--font); - border: none; - outline: none; -} - -.select2-container--default - .select2-selection--single - .select2-selection__arrow { - height: 35px; -} - -.select2-container--default - .select2-selection--single - .select2-selection__arrow - b { - border-color: var(--sub-color) transparent transparent transparent; -} - -.select2-container--default.select2-container--open - .select2-selection--single - .select2-selection__arrow - b { - border-color: var(--sub-color) transparent; -} - -.select2-container--default .select2-search--dropdown .select2-search__field { - border-color: rgba(0, 0, 0, 0.1); - background: var(--bg-color); - color: var(--text-color); - border-radius: var(--roundness); -} - -==> monkeytype/src/sass/commandline.scss <== -#commandLineWrapper { - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.75); - position: fixed; - left: 0; - top: 0; - z-index: 1000; - display: grid; - justify-content: center; - align-items: start; - padding: 5rem 0; - - #commandInput { - width: 700px; - background: var(--bg-color); - border-radius: var(--roundness); - - input { - background: var(--bg-color); - padding: 1rem; - color: var(--main-color); - border: none; - outline: none; - font-size: 1rem; - width: 100%; - border-radius: var(--roundness); - } - - .shiftEnter { - padding: 0.5rem 1rem; - font-size: 0.75rem; - line-height: 0.75rem; - color: var(--sub-color); - text-align: center; - } - } - - #commandLine { - width: 700px; - background: var(--bg-color); - border-radius: var(--roundness); - - .searchicon { - color: var(--sub-color); - margin: 1px 1rem 0 1rem; - } - - input { - background: var(--bg-color); - padding: 1rem 1rem 1rem 0; - color: var(--text-color); - border: none; - outline: none; - font-size: 1rem; - width: 100%; - border-radius: var(--roundness); - } - - .separator { - background: black; - width: 100%; - height: 1px; - margin-bottom: 0.5rem; - } - - .listTitle { - color: var(--text-color); - padding: 0.5rem 1rem; - font-size: 0.75rem; - line-height: 0.75rem; - } - - .suggestions { - display: block; - @extend .ffscroll; - overflow-y: scroll; - max-height: calc(100vh - 10rem - 3rem); - display: grid; - cursor: pointer; - user-select: none; - - .entry { - padding: 0.5rem 1rem; - font-size: 0.75rem; - line-height: 0.75rem; - color: var(--sub-color); - display: grid; - grid-template-columns: auto 1fr; - - div { - pointer-events: none; - } - - .textIcon { - font-weight: 900; - /* width: 1.25rem; */ - display: inline-block; - letter-spacing: -0.1rem; - margin-right: 0.5rem; - text-align: center; - width: 1.25em; - } - - .fas { - margin-right: 0.5rem; - } - - &:last-child { - border-radius: 0 0 var(--roundness) var(--roundness); - } - - &.activeMouse { - color: var(--bg-color); - background: var(--text-color); - cursor: pointer; - } - - &.activeKeyboard { - color: var(--bg-color); - background: var(--text-color); - } - - // &:hover { - // color: var(--text-color); - // background: var(--sub-color); - // cursor: pointer; - // } - } - } - } -} - -==> monkeytype/src/sass/notifications.scss <== -#notificationCenter { - width: 350px; - z-index: 99999999; - display: grid; - gap: 1rem; - position: fixed; - right: 1rem; - top: 1rem; - .history { - display: grid; - gap: 1rem; - } - .notif { - user-select: none; - .icon { - color: var(--bg-color); - opacity: 0.5; - padding: 1rem 1rem; - align-items: center; - display: grid; - font-size: 1.25rem; - } - .message { - padding: 1rem 1rem 1rem 0; - .title { - color: var(--bg-color); - font-size: 0.75em; - opacity: 0.5; - line-height: 0.75rem; - } - } - - position: relative; - background: var(--sub-color); - color: var(--bg-color); - display: grid; - grid-template-columns: min-content auto min-content; - border-radius: var(--roundness); - border-width: 0.25rem; - - &.bad { - background-color: var(--error-color); - } - - &.good { - background-color: var(--main-color); - } - - &:hover { - // opacity: .5; - // box-shadow: 0 0 20px rgba(0,0,0,.25); - cursor: pointer; - &::after { - opacity: 1; - } - } - &::after { - transition: 0.125s; - font-family: "Font Awesome 5 Free"; - background: rgba(0, 0, 0, 0.5); - opacity: 0; - font-weight: 900; - content: "\f00d"; - position: absolute; - width: 100%; - height: 100%; - color: var(--bg-color); - font-size: 2.5rem; - display: grid; - /* align-self: center; */ - align-items: center; - text-align: center; - border-radius: var(--roundness); - } - } -} - -==> monkeytype/src/sass/caret.scss <== -#caret { - height: 1.5rem; - background: var(--caret-color); - animation: caretFlashSmooth 1s infinite; - position: absolute; - border-radius: var(--roundness); - // transition: 0.05s; - transform-origin: top left; -} - -#paceCaret { - height: 1.5rem; - // background: var(--sub-color); - background: var(--sub-color); - opacity: 0.5; - position: absolute; - border-radius: var(--roundness); - // transition: 0.25s; - transform-origin: top left; - width: 2px; -} - -#caret, -#paceCaret { - &.off { - width: 0; - } - - &.default { - width: 2px; - } - - &.carrot { - background-color: transparent; - background-image: url("../images/carrot.png"); - background-size: contain; - background-position: center; - background-repeat: no-repeat; - width: 0.25rem; - &.size2 { - margin-left: -0.1rem; - } - &.size3 { - margin-left: -0.2rem; - } - &.size4 { - margin-left: -0.3rem; - } - } - - &.banana { - background-color: transparent; - background-image: url("../images/banana.png"); - background-size: contain; - background-position: center; - background-repeat: no-repeat; - width: 1rem; - &.size2 { - margin-left: -0.1rem; - } - &.size3 { - margin-left: -0.5rem; - } - &.size4 { - margin-left: -0.3rem; - } - } - - &.block { - width: 0.7em; - margin-left: 0.25em; - border-radius: 0; - z-index: -1; - } - - &.outline { - @extend #caret, .block; - animation-name: none; - background: transparent; - border: 1px solid var(--caret-color); - } - - &.underline { - height: 2px; - width: 0.8em; - margin-top: 1.3em; - margin-left: 0.3em; - - &.size125 { - margin-top: 1.8em; - } - - &.size15 { - margin-top: 2.1em; - } - - &.size2 { - margin-top: 2.7em; - } - - &.size3 { - margin-top: 3.9em; - } - &.size4 { - margin-top: 4.7em; - } - } - - &.size125 { - transform: scale(1.25); - } - - &.size15 { - transform: scale(1.45); - } - - &.size2 { - transform: scale(1.9); - } - - &.size3 { - transform: scale(2.8); - } - - &.size4 { - transform: scale(3.7); - } -} - -==> monkeytype/src/sass/test.scss <== -#timerWrapper { - opacity: 0; - transition: 0.25s; - z-index: -1; - position: relative; - z-index: 99; - #timer { - position: fixed; - top: 0; - left: 0; - width: 100vw; - /* height: 0.5rem; */ - height: 0.5rem; - background: black; - /* background: #0f0f0f; */ - /* background: red; */ - // transition: 1s linear; - z-index: -1; - - &.timerMain { - background: var(--main-color); - } - - &.timerSub { - background: var(--sub-color); - } - - &.timerText { - background: var(--text-color); - } - } -} - -.pageTest { - position: relative; - - .ssWatermark { - font-size: 1.25rem; - color: var(--sub-color); - line-height: 1rem; - text-align: right; - } - - #timerNumber { - pointer-events: none; - transition: 0.25s; - height: 0; - color: black; - line-height: 0; - z-index: -1; - text-align: center; - left: 0; - width: 100%; - position: relative; - font-size: 10rem; - opacity: 0; - width: 0; - height: 0; - margin: 0 auto; - display: grid; - justify-content: center; - bottom: 6rem; - transition: none; - } - - #largeLiveWpmAndAcc { - font-size: 10rem; - color: black; - width: 100%; - left: 0; - text-align: center; - z-index: -1; - height: 0; - line-height: 0; - top: 5rem; - position: relative; - display: grid; - grid-auto-flow: column; - justify-content: center; - gap: 5rem; - - #liveWpm { - opacity: 0; - } - - #liveAcc { - opacity: 0; - } - - #liveBurst { - opacity: 0; - } - } - - #largeLiveWpmAndAcc.timerMain, - #timerNumber.timerMain { - color: var(--main-color); - } - - #timer.timerMain { - background: var(--main-color); - } - - #largeLiveWpmAndAcc.timerSub, - #timerNumber.timerSub { - color: var(--sub-color); - } - - #timer.timerSub { - background: var(--sub-color); - } - - #largeLiveWpmAndAcc.timerText, - #timerNumber.timerText { - color: var(--text-color); - } - - #timer.timerText { - background: var(--text-color); - } -} - -#words { - height: fit-content; - height: -moz-fit-content; - display: flex; - flex-wrap: wrap; - width: 100%; - align-content: flex-start; - user-select: none; - padding-bottom: 1em; - - .newline { - width: inherit; - } - - letter { - border-bottom-style: solid; - border-bottom-width: 0.05em; - border-bottom-color: transparent; - &.dead { - border-bottom-width: 0.05em; - border-bottom-color: var(--sub-color); - } - &.tabChar, - &.nlChar { - margin: 0 0.25rem; - opacity: 0.2; - } - } - - /* a little hack for right-to-left languages */ - &.rightToLeftTest { - //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages - direction: rtl; - .word { - //flex-direction: row-reverse; - direction: rtl; - } - } - &.withLigatures { - letter { - display: inline; - } - } - &.blurred { - opacity: 0.25; - filter: blur(4px); - -webkit-filter: blur(4px); - } - - &.flipped { - .word { - color: var(--text-color); - - & letter.dead { - border-bottom-color: var(--sub-color) !important; - } - - & letter.correct { - color: var(--sub-color); - } - - & letter.corrected { - color: var(--sub-color); - border-bottom: 2px dotted var(--main-color); - } - - & letter.extraCorrected { - border-right: 2px dotted var(--main-color); - } - } - } - - &.colorfulMode { - .word { - & letter.dead { - border-bottom-color: var(--main-color) !important; - } - - & letter.correct { - color: var(--main-color); - } - - & letter.corrected { - color: var(--main-color); - border-bottom: 2px dotted var(--text-color); - } - - & letter.extraCorrected { - border-right: 2px dotted var(--text-color); - } - - & letter.incorrect { - color: var(--colorful-error-color); - } - - & letter.incorrect.extra { - color: var(--colorful-error-extra-color); - } - } - } - - &.flipped.colorfulMode { - .word { - color: var(--main-color); - - & letter.dead { - border-bottom-color: var(--sub-color) !important; - } - - & letter.correct { - color: var(--sub-color); - } - - & letter.corrected { - color: var(--sub-color); - border-bottom: 2px dotted var(--main-color); - } - - & letter.extraCorrected { - border-right: 2px dotted var(--main-color); - } - - & letter.incorrect { - color: var(--colorful-error-color); - } - - & letter.incorrect.extra { - color: var(--colorful-error-extra-color); - } - } - } -} - -.word { - margin: 0.25rem; - color: var(--sub-color); - font-variant: no-common-ligatures; - // display: flex; - // transition: 0.25s - /* margin-bottom: 1px; */ - border-bottom: 2px solid transparent; - line-height: 1rem; - letter { - display: inline-block; - } - - &.lastbeforenewline::after { - font-family: "Font Awesome 5 Free"; - font-weight: 600; - content: "\f107"; - margin-left: 0.5rem; - opacity: 0.25; - } - - // transition: .25s; - .wordInputAfter { - opacity: 1; - position: absolute; - background: var(--sub-color); - color: var(--bg-color); - /* background: red; */ - padding: 0.5rem; - /* left: .5rem; */ - margin-left: -0.5rem; - // margin-top: -1.5rem; - border-radius: var(--roundness); - // box-shadow: 0 0 10px rgba(0,0,0,.25); - transition: 0.25s; - text-shadow: none; - top: -0.5rem; - z-index: 10; - cursor: text; - .speed { - font-size: 0.75rem; - } - } -} - -#words.size125 .word { - line-height: 1.25rem; - font-size: 1.25rem; - margin: 0.31rem; -} - -#words.size15 .word { - line-height: 1.5rem; - font-size: 1.5rem; - margin: 0.37rem; -} - -#words.size2 .word { - line-height: 2rem; - font-size: 2rem; - margin: 0.5rem; -} - -#words.size3 .word { - line-height: 3rem; - font-size: 3rem; - margin: 0.75rem; -} - -#words.size4 .word { - line-height: 4rem; - font-size: 4rem; - margin: 1rem; -} - -#words.nospace { - .word { - margin: 0.5rem 0; - } -} - -#words.arrows { - .word { - margin: 0.5rem 0; - letter { - margin: 0 0.25rem; - } - } -} - -.word.error { - /* margin-bottom: 1px; */ - border-bottom: 2px solid var(--error-color); - text-shadow: 1px 0px 0px var(--bg-color), - // 2px 0px 0px var(--bg-color), - -1px 0px 0px var(--bg-color), - // -2px 0px 0px var(--bg-color), - 0px 1px 0px var(--bg-color), - 1px 1px 0px var(--bg-color), -1px 1px 0px var(--bg-color); -} - -#words.noErrorBorder, -#resultWordsHistory.noErrorBorder { - .word.error { - text-shadow: none; - } -} -// .word letter { -// transition: .1s; -// height: 1rem; -// line-height: 1rem; -/* margin: 0 1px; */ -// } - -.word letter.correct { - color: var(--text-color); -} - -.word letter.corrected { - color: var(--text-color); - border-bottom: 2px dotted var(--main-color); -} - -.word letter.extraCorrected { - border-right: 2px dotted var(--main-color); -} - -.word letter.incorrect { - color: var(--error-color); - position: relative; -} - -.word letter.incorrect hint { - position: absolute; - bottom: -1em; - color: var(--text-color); - line-height: initial; - font-size: 0.75em; - text-shadow: none; - padding: 1px; - left: 0; - opacity: 0.5; - text-align: center; - width: 100%; -} - -.word letter.incorrect.extra { - color: var(--error-extra-color); -} - -.word letter.missing { - opacity: 0.5; -} - -#words.flipped.colorfulMode .word.error, -#words.colorfulMode .word.error { - border-bottom: 2px solid var(--colorful-error-color); -} - -#wordsInput { - opacity: 0; - padding: 0; - margin: 0; - border: none; - outline: none; - display: block; - resize: none; - position: fixed; - z-index: -1; - cursor: default; - pointer-events: none; -} - -#capsWarning { - background: var(--main-color); - color: var(--bg-color); - display: table; - position: absolute; - left: 50%; - // top: 66vh; - transform: translateX(-50%) translateY(-50%); - padding: 1rem; - border-radius: var(--roundness); - /* margin-top: 1rem; */ - transition: 0.25s; - z-index: 999; - pointer-events: none; - - i { - margin-right: 0.5rem; - } -} - -#result { - display: grid; - // height: 200px; - gap: 1rem; - // grid-template-columns: auto 1fr; - // justify-content: center; - align-items: center; - grid-template-columns: auto 1fr; - grid-template-areas: - "stats chart" - "morestats morestats"; - // "wordsHistory wordsHistory" - // "buttons buttons" - // "login login" - // "ssw ssw"; - - &:focus { - outline: none; - } - - .buttons { - display: grid; - grid-auto-flow: column; - gap: 1rem; - justify-content: center; - // grid-area: buttons; - grid-column: 1/3; - } - - .ssWatermark { - // grid-area: ssw; - grid-column: 1/3; - } - - #resultWordsHistory, - #resultReplay { - // grid-area: wordsHistory; - color: var(--sub-color); - // grid-column: 1/3; - margin-bottom: 1rem; - .icon-button { - padding: 0; - margin-left: 0.5rem; - } - .heatmapLegend { - display: inline-grid; - grid-template-columns: auto auto auto; - gap: 1rem; - font-size: 0.75rem; - color: var(--sub-color); - width: min-content; - .boxes { - display: flex; - .box { - width: 1rem; - height: 1rem; - } - .box:nth-child(1) { - background: var(--colorful-error-color); - border-radius: var(--roundness) 0 0 var(--roundness); - } - .box:nth-child(2) { - background: var(--colorful-error-color); - filter: opacity(0.6); - } - .box:nth-child(3) { - background: var(--sub-color); - } - .box:nth-child(4) { - background: var(--main-color); - filter: opacity(0.6); - } - .box:nth-child(5) { - background: var(--main-color); - border-radius: 0 var(--roundness) var(--roundness) 0; - } - } - } - .title { - user-select: none; - margin-bottom: 0.25rem; - } - .words { - display: flex; - flex-wrap: wrap; - width: 100%; - align-content: flex-start; - user-select: none; - .word { - position: relative; - margin: 0.18rem 0.6rem 0.15rem 0; - letter.correct { - color: var(--text-color); - } - letter.incorrect { - color: var(--error-color); - } - letter.incorrect.extra { - color: var(--error-extra-color); - } - &.heatmap-0 letter { - color: var(--colorful-error-color); - } - &.heatmap-1 letter { - color: var(--colorful-error-color); - filter: opacity(0.6); - } - &.heatmap-2 letter { - color: var(--sub-color); - } - &.heatmap-3 letter { - color: var(--main-color); - filter: opacity(0.6); - } - &.heatmap-4 letter { - color: var(--main-color); - } - } - &.rightToLeftTest { - //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages - direction: rtl; - .word { - //flex-direction: row-reverse; - direction: rtl; - } - } - &.withLigatures { - letter { - display: inline; - } - } - } - } - - .chart { - grid-area: chart; - width: 100%; - - canvas { - width: 100% !important; - height: 100%; - } - - max-height: 200px; - height: 200px; - - .title { - color: var(--sub-color); - margin-bottom: 1rem; - } - } - - .loginTip { - grid-column: 1/3; - text-align: center; - color: var(--sub-color); - // grid-area: login; - grid-column: 1/3; - .link { - text-decoration: underline; - display: inline-block; - cursor: pointer; - } - } - - .stats { - grid-area: stats; - display: grid; - // column-gap: 0.5rem; - gap: 0.5rem; - justify-content: center; - align-items: center; - // grid-template-areas: - // "wpm acc" - // "wpm key" - // "raw time" - // "consistency consistency" - // "source source" - // "leaderboards leaderboards" - // "testType infoAndTags"; - // grid-template-areas: - // "wpm acc key consistency testType leaderboards source" - // "wpm raw time nothing infoAndTags leaderboards source"; - grid-template-areas: - "wpm" - "acc"; - margin-bottom: 1rem; - - &.morestats { - display: grid; - grid-auto-flow: column; - grid-template-areas: none; - align-items: flex-start; - justify-content: space-between; - column-gap: 2rem; - grid-area: morestats; - - // grid-template-areas: "raw consistency testType infoAndTags leaderboards source" - // "key time testType infoAndTags leaderboards source"; - .subgroup { - display: grid; - gap: 0.5rem; - } - } - - .group { - // margin-bottom: 0.5rem; - - .top { - color: var(--sub-color); - font-size: 1rem; - line-height: 1rem; - margin-bottom: 0.25rem; - } - - .bottom { - color: var(--main-color); - font-size: 2rem; - line-height: 2rem; - } - - &.time { - .afk, - .timeToday { - color: var(--sub-color); - font-size: 0.75rem; - line-height: 0.75rem; - margin-left: 0.2rem; - } - } - - &.source { - #rateQuoteButton, - #reportQuoteButton { - padding: 0 0.25rem; - } - #rateQuoteButton { - display: inline-grid; - gap: 0.25rem; - } - } - } - - // .infoAndTags { - // display: grid; - // gap: 0.5rem; - // align-self: baseline; - // // grid-area: infoAndTags; - // color: var(--sub-color); - - // .top { - // font-size: 1rem; - // line-height: 1rem; - // } - - // .bottom { - // font-size: 1rem; - // line-height: 1rem; - // } - // } - - .info, - .tags, - .source { - .top { - font-size: 1rem; - line-height: 1rem; - } - - .bottom { - font-size: 1rem; - line-height: 1rem; - } - } - - .source { - max-width: 30rem; - } - - .tags .bottom .fas { - margin-left: 0.5rem; - } - - .wpm { - grid-area: wpm; - - .top { - font-size: 2rem; - line-height: 1.5rem; - display: flex; - // margin-top: -0.5rem; - - // .crownWrapper { - // width: 1.7rem; - // overflow: hidden; - // height: 1.7rem; - // margin-left: 0.5rem; - // // margin-top: 0.98rem; - // margin-top: -0.5rem; - - .crown { - height: 1.7rem; - width: 1.7rem; - margin-left: 0.5rem; - margin-top: -0.2rem; - font-size: 0.7rem; - line-height: 1.7rem; - background: var(--main-color); - color: var(--bg-color); - border-radius: 0.6rem; - text-align: center; - align-self: center; - width: 1.7rem; - height: 1.7rem; - } - // } - } - - .bottom { - font-size: 4rem; - line-height: 4rem; - } - } - - .testType, - .leaderboards { - .bottom { - font-size: 1rem; - line-height: 1rem; - .lbChange .fas { - margin-right: 0.15rem; - } - } - } - - .acc { - grid-area: acc; - - .top { - font-size: 2rem; - line-height: 1.5rem; - } - - .bottom { - font-size: 4rem; - line-height: 4rem; - } - } - - .burst { - grid-area: burst; - - .top { - font-size: 2rem; - line-height: 1.5rem; - } - - .bottom { - font-size: 4rem; - line-height: 4rem; - } - } - - // .key { - // grid-area: key; - // } - - // .time { - // grid-area: time; - // } - - // .raw { - // grid-area: raw; - // } - } -} - -#restartTestButton, -#showWordHistoryButton, -#saveScreenshotButton, -#restartTestButtonWithSameWordset, -#nextTestButton, -#practiseWordsButton, -#watchReplayButton { - position: relative; - border-radius: var(--roundness); - padding: 1rem 2rem; - width: min-content; - width: -moz-min-content; - color: var(--sub-color); - transition: 0.25s; - cursor: pointer; - - &:hover, - &:focus { - color: var(--main-color); - outline: none; - } - - &:focus { - background: var(--sub-color); - } -} - -#retrySavingResultButton { - position: relative; - border-radius: var(--roundness); - padding: 1rem 2rem; - color: var(--error-color); - transition: 0.25s; - cursor: pointer; - width: max-content; - width: -moz-max-content; - background: var(--colorful-error-color); - color: var(--bg-color); - justify-self: center; - justify-content: center; - margin: 0 auto 1rem auto; - user-select: none; - - &:hover, - &:focus { - background: var(--text-color); - outline: none; - } - - &:focus { - background: var(--text-color); - } -} - -#showWordHistoryButton { - opacity: 1; -} - -#replayWords { - cursor: pointer; -} - -#replayStopwatch { - color: var(--main-color); - display: inline-block; - margin: 0; -} - -#restartTestButton { - margin: 0 auto; - margin-top: 1rem; -} - -.pageTest { - #wordsWrapper { - position: relative; - } - #memoryTimer { - background: var(--main-color); - color: var(--bg-color); - padding: 1rem; - border-radius: var(--roundness); - /* width: min-content; */ - text-align: center; - width: max-content; - /* justify-self: center; */ - left: 50%; - position: absolute; - transform: translateX(-50%); - top: -6rem; - user-select: none; - pointer-events: none; - opacity: 0; - } - .outOfFocusWarning { - text-align: center; - height: 0; - line-height: 150px; - z-index: 999; - position: relative; - user-select: none; - pointer-events: none; - } - - #testModesNotice { - display: grid; - grid-auto-flow: column; - gap: 1rem; - color: var(--sub-color); - text-align: center; - margin-bottom: 1.25rem; - height: 1rem; - line-height: 1rem; - transition: 0.125s; - justify-content: center; - user-select: none; - - .fas { - margin-right: 0.5rem; - } - } - #miniTimerAndLiveWpm { - height: 0; - margin-left: 0.37rem; - display: flex; - font-size: 1rem; - line-height: 1rem; - margin-top: -1.5rem; - position: absolute; - color: black; - - .time { - margin-right: 2rem; - } - - .wpm, - .acc { - margin-right: 2rem; - } - - .time, - .wpm, - .acc, - .burst { - opacity: 0; - } - - &.timerMain { - color: var(--main-color); - } - - &.timerSub { - color: var(--sub-color); - } - - &.timerText { - color: var(--text-color); - } - - &.size125 { - margin-top: -1.75rem; - font-size: 1.25rem; - line-height: 1.25rem; - } - &.size15 { - margin-top: -2rem; - font-size: 1.5rem; - line-height: 1.5rem; - } - &.size2 { - margin-top: -2.5rem; - font-size: 2rem; - line-height: 2rem; - } - &.size3 { - margin-top: -3.5rem; - font-size: 3rem; - line-height: 3rem; - } - &.size4 { - margin-top: -4.5rem; - font-size: 4rem; - line-height: 4rem; - } - } -} - -#middle.focus .pageTest { - #testModesNotice { - opacity: 0 !important; - } -} - -==> monkeytype/src/sass/leaderboards.scss <== -#leaderboardsWrapper { - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.75); - position: fixed; - left: 0; - top: 0; - z-index: 1000; - display: grid; - justify-content: center; - align-items: center; - padding: 5rem 0; - - #leaderboards { - width: 85vw; - // height: calc(95vh - 5rem); - overflow-y: auto; - background: var(--bg-color); - border-radius: var(--roundness); - padding: 2rem; - display: grid; - gap: 2rem 0; - grid-template-rows: 3rem auto; - grid-template-areas: - "title buttons" - "tables tables"; - grid-template-columns: 1fr 1fr; - - .leaderboardsTop { - width: 200%; - min-width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - - .buttonGroup .button { - padding: 0.4rem 2.18rem; - } - } - - .mainTitle { - font-size: 3rem; - line-height: 3rem; - grid-area: title; - } - - .subTitle { - color: var(--sub-color); - } - - .title { - font-size: 2rem; - line-height: 2rem; - margin-bottom: 0.5rem; - } - - .tables { - grid-area: tables; - display: grid; - gap: 1rem; - grid-template-columns: 1fr 1fr; - font-size: 0.8rem; - width: 100%; - - .sub { - opacity: 0.5; - } - - .alignRight { - text-align: right; - } - - .titleAndTable { - display: grid; - width: 100%; - - .titleAndButtons { - display: grid; - grid-template-columns: 1fr auto; - .buttons { - display: grid; - grid-template-columns: auto 1fr 1fr; - align-items: center; - // margin-top: .1rem; - gap: 1rem; - color: var(--sub-color); - .button { - padding-left: 1rem; - padding-right: 1rem; - } - } - } - - .title { - grid-area: 1/1; - margin-bottom: 0; - line-height: 2.5rem; - } - - .subtitle { - grid-area: 1/1; - align-self: center; - justify-self: right; - color: var(--sub-color); - } - } - - .leftTableWrapper, - .rightTableWrapper { - height: calc(100vh - 22rem); - @extend .ffscroll; - overflow-y: scroll; - overflow-x: auto; - } - - .leftTableWrapper::-webkit-scrollbar, - .rightTableWrapper::-webkit-scrollbar { - height: 5px; - width: 5px; - } - - table { - width: 100%; - border-spacing: 0; - border-collapse: collapse; - - tr td:first-child { - text-align: center; - } - - tr.me { - td { - color: var(--main-color); - // font-weight: 900; - } - } - - td { - padding: 0.5rem 0.5rem; - } - - thead { - color: var(--sub-color); - font-size: 0.75rem; - - td { - padding: 0.5rem; - background: var(--bg-color); - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 99; - } - } - - tbody { - color: var(--text-color); - - tr:nth-child(odd) td { - background: rgba(0, 0, 0, 0.1); - } - } - - tfoot { - td { - padding: 1rem 0.5rem; - position: -webkit-sticky; - position: sticky; - bottom: -5px; - background: var(--bg-color); - color: var(--main-color); - z-index: 4; - } - } - - tr { - td:first-child { - padding-left: 1rem; - } - td:last-child { - padding-right: 1rem; - } - } - } - } - - .buttons { - .buttonGroup { - display: grid; - grid-auto-flow: column; - gap: 1rem; - grid-area: 1/2; - } - } - } -} - -==> monkeytype/src/sass/keymap.scss <== -.keymap { - display: grid; - grid-template-rows: 1fr 1fr 1fr; - justify-content: center; - white-space: nowrap; - // height: 140px; - gap: 0.25rem; - margin-top: 1rem; - user-select: none; - - .row { - height: 2rem; - gap: 0.25rem; - } - - .keymap-key { - display: flex; - background-color: transparent; - color: var(--sub-color); - border-radius: var(--roundness); - border: 0.05rem solid; - border-color: var(--sub-color); - text-align: center; - justify-content: center; - align-items: center; - width: 2rem; - height: 2rem; - position: relative; - - .bump { - width: 0.75rem; - height: 0.05rem; - background: var(--sub-color); - position: absolute; - border-radius: var(--roundness); - // margin-top: 1.5rem; - bottom: 0.15rem; - } - - &.active-key { - color: var(--bg-color); - background-color: var(--main-color); - border-color: var(--main-color); - - .bump { - background: var(--bg-color); - } - } - - &#KeySpace { - &:hover { - cursor: pointer; - color: var(--main-color); - } - } - - &#KeySpace, - &#KeySpace2 { - width: 100%; - } - - &#KeySpace2 { - opacity: 0; - } - - &.flash { - animation: flashKey 1s cubic-bezier(0.16, 1, 0.3, 1) forwards; - } - } - - .hidden-key, - .hide-key { - opacity: 0; - } - - .keymap-split-spacer, - .keymap-stagger-split-spacer, - .keymap-matrix-split-spacer { - display: none; - } - - .r1 { - display: grid; - grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - .r2 { - display: grid; - grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1rem; - } - - .r3 { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - .r4 { - display: grid; - grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2.75fr; - } - - .r5 { - display: grid; - grid-template-columns: 3.5fr 6fr 3.5fr; - font-size: 0.5rem; - // &.matrixSpace { - // // grid-template-columns: 6.75fr 1.9fr 6.75fr; - // grid-template-columns: 6.9fr 4.6fr 6.9fr; // wider spacebar - // } - // &.splitSpace { - // // grid-template-columns: 6.75fr 1.9fr 6.75fr; - // grid-template-columns: 4fr 7.5fr 4fr; - // } - } - &.matrix { - .r1, - .r2, - .r3 { - grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - .r4 { - grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - .r5 { - grid-template-columns: 3.25fr 5fr 2fr 1fr; - } - - .r1, - .r2, - .r3 { - :nth-child(13) { - opacity: 0; - } - - :nth-child(14) { - opacity: 0; - } - } - } - &.split { - .keymap-split-spacer { - display: block; - } - .keymap-stagger-split-spacer { - display: block; - } - - .r1 { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; - } - - .r2 { - display: grid; - grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - .r3 { - display: grid; - grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; - } - - .r4 { - display: grid; - grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr; - } - .r5 { - grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr; - } - #KeySpace2 { - opacity: 1; - } - } - &.split_matrix { - .keymap-split-spacer { - display: block; - width: 2rem; - height: 2rem; - } - .keymap-stagger-split-spacer { - display: none; - } - .keymap-matrix-split-spacer { - display: block; - width: 2rem; - height: 2rem; - } - .r1, - .r2, - .r3 { - grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - .r4 { - grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - - .r5 { - grid-template-columns: 3.225fr 3fr 1fr 3fr 2fr; - } - #KeySpace2 { - opacity: 1; - } - - .r1 { - :nth-child(12) { - opacity: 0; - } - } - - .r1, - .r2, - .r3 { - :nth-child(13) { - opacity: 0; - } - - :nth-child(14) { - opacity: 0; - } - } - } - &.alice { - .keymap-split-spacer { - display: block; - } - .r4 .keymap-split-spacer { - display: none; - } - .keymap-stagger-split-spacer { - display: block; - } - - .r1 { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; - .keymap-key:nth-child(2) { - //1 - margin-left: 45%; - } - .keymap-key:nth-child(3) { - //2 - margin-top: -2px; - margin-left: 45%; - } - .keymap-key:nth-child(4), - .keymap-key:nth-child(5), - .keymap-key:nth-child(6), - .keymap-key:nth-child(7) { - //3456 - transform: rotate(10deg); - margin-left: 45%; - } - .keymap-key:nth-child(4) { - //3 - margin-top: 3px; - } - .keymap-key:nth-child(5) { - //4 - margin-top: 10px; - } - .keymap-key:nth-child(6) { - //5 - margin-top: 17px; - } - .keymap-key:nth-child(7) { - //6 - margin-top: 24px; - } - .keymap-key:nth-child(9), - .keymap-key:nth-child(10), - .keymap-key:nth-child(11), - .keymap-key:nth-child(12) { - //7890 - transform: rotate(-10deg); - margin-left: -48%; - } - .keymap-key:nth-child(12) { - //7 - margin-top: -1px; - } - .keymap-key:nth-child(11) { - //8 - margin-top: 6px; - } - .keymap-key:nth-child(10) { - //9 - margin-top: 13px; - } - .keymap-key:nth-child(9) { - //10 - margin-top: 20px; - } - .keymap-key:nth-child(13), - .keymap-key:nth-child(14) { - //-= - margin-left: -40%; - } - } - - .r2 { - display: grid; - grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - .keymap-key:nth-child(2) { - //Q - margin-left: 20%; - } - .keymap-key:nth-child(3), - .keymap-key:nth-child(4), - .keymap-key:nth-child(5), - .keymap-key:nth-child(6) { - //WERT - transform: rotate(10deg); - margin-left: 45%; - } - .keymap-key:nth-child(4), - .keymap-key:nth-child(10) { - //EI - margin-top: 8px; - } - .keymap-key:nth-child(5), - .keymap-key:nth-child(9) { - //RU - margin-top: 15px; - } - .keymap-key:nth-child(6), - .keymap-key:nth-child(8) { - //TY - margin-top: 22px; - } - - .keymap-key:nth-child(8), - .keymap-key:nth-child(9), - .keymap-key:nth-child(10), - .keymap-key:nth-child(11) { - //YUIO - transform: rotate(-10deg); - margin-left: -12%; - } - } - - .r3 { - display: grid; - grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; - .keymap-key:nth-child(2) { - //A - margin-left: -5px; - } - .keymap-key:nth-child(3), - .keymap-key:nth-child(4), - .keymap-key:nth-child(5), - .keymap-key:nth-child(6) { - //SDFG - margin-left: -1px; - transform: rotate(10deg); - } - .keymap-key:nth-child(4), - .keymap-key:nth-child(10) { - //DK - margin-top: 8px; - } - .keymap-key:nth-child(5), - .keymap-key:nth-child(9) { - //FJ - margin-top: 15px; - } - .keymap-key:nth-child(6), - .keymap-key:nth-child(8) { - //GH - margin-top: 22px; - } - - .keymap-key:nth-child(8), - .keymap-key:nth-child(9), - .keymap-key:nth-child(10), - .keymap-key:nth-child(11) { - //HJKL - transform: rotate(-10deg); - margin-left: -25%; - } - } - - .r4 { - display: grid; - grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr; - .keymap-key:nth-child(2) { - margin-left: -18px; - } - .keymap-key:nth-child(3) { - //Z - margin-left: -15px; - } - .keymap-key:nth-child(4), - .keymap-key:nth-child(5), - .keymap-key:nth-child(6), - .keymap-key:nth-child(7) { - //XCVB - margin-left: -11px; - transform: rotate(10deg); - margin-top: 2px; - } - .keymap-key:nth-child(12) { - //, - margin-top: 4px; - margin-left: -5px; - } - .keymap-key:nth-child(5), - .keymap-key:nth-child(11) { - //CM - margin-top: 10px; - } - .keymap-key:nth-child(6), - .keymap-key:nth-child(10) { - //VN - margin-top: 18px; - } - .keymap-key:nth-child(7) { - //B - margin-top: 24px; - } - - .keymap-key:nth-child(10), - .keymap-key:nth-child(11), - .keymap-key:nth-child(12) { - //NM, - transform: rotate(-10deg); - margin-left: -25%; - } - } - .r5 { - grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr; - } - #KeySpace2 { - opacity: 1; - } - - // div#KeyE.keymap-key, - // div#KeyD.keymap-key { - // margin-top: 6px; - // } - // div#KeyC.keymap-key { - // margin-top: 8px; - // } - // div#KeyR.keymap-key, - // div#KeyF.keymap-key { - // margin-top: 12px; - // } - // div#KeyV.keymap-key { - // margin-top: 14px; - // } - // div#KeyT.keymap-key, - // div#KeyG.keymap-key { - // margin-top: 18px; - // } - // div#KeyB.keymap-key { - // margin-top: 20px; - // } - // div#KeyY.keymap-key, - // div#KeyU.keymap-key, - // div#KeyI.keymap-key, - // div#KeyO.keymap-key { - // transform: rotate(-10deg); - // margin-left: -25%; - // } - // div#KeyH.keymap-key, - // div#KeyJ.keymap-key, - // div#KeyK.keymap-key, - // div#KeyL.keymap-key { - // transform: rotate(-10deg); - // margin-left: -35%; - // } - // div#KeyN.keymap-key, - // div#KeyM.keymap-key, - // div#KeyComma.keymap-key { - // transform: rotate(-10deg); - // margin-left: -16%; - // } - // div#KeyP.keymap-key, - // div#KeyLeftBracket.keymap-key, - // div#KeyRightBracket.keymap-key { - // margin-left: 5%; - // } - // div#KeySemicolon.keymap-key, - // div#KeyQuote.keymap-key { - // margin-left: -25%; - // } - // div#KeyPeriod.keymap-key, - // div#KeySlash.keymap-key { - // margin-left: -3px; - // } - // div#KeyO.keymap-key, - // div#KeyComma.keymap-key { - // margin-top: 3px; - // } - // div#KeyL.keymap-key { - // margin-top: 1px; - // } - // div#KeyI.keymap-key, - // div#KeyM.keymap-key { - // margin-top: 9px; - // } - // div#KeyK.keymap-key { - // margin-top: 7px; - // } - // div#KeyU.keymap-key, - // div#KeyN.keymap-key { - // margin-top: 15px; - // } - // div#KeyJ.keymap-key { - // margin-top: 13px; - // } - // div#KeyY.keymap-key { - // margin-top: 21px; - // } - // div#KeyH.keymap-key { - // margin-top: 19px; - // } - div#KeySpace.keymap-key { - transform: rotate(10deg); - margin-left: -5%; - margin-top: 21%; - } - div#KeySpace2.keymap-key { - transform: rotate(-10deg); - margin-left: -33%; - margin-top: 20%; - } - div#KeyBackslash.keymap-key { - visibility: hidden; - } - - div.extraKey { - margin-top: 25px; - transform: rotate(-10deg) !important; - margin-left: -7px !important; - display: flex; - background-color: transparent; - color: var(--sub-color); - border-radius: var(--roundness); - border: 0.05rem solid; - border-color: var(--sub-color); - text-align: center; - justify-content: center; - align-items: center; - width: 2rem; - height: 2rem; - position: relative; - } - // div#KeySpace.keymap-key:after { - // content: 'Alice'; - // text-indent: 0; - // font-weight: 600!important; - // margin: auto; - // font-size: 0.9rem; - // color: var(--bg-color) - // } - } -} - -==> monkeytype/src/sass/nav.scss <== -#menu { - font-size: 1rem; - line-height: 1rem; - color: var(--sub-color); - display: grid; - grid-auto-flow: column; - gap: 0.5rem; - // margin-bottom: -0.4rem; - width: fit-content; - width: -moz-fit-content; - - .icon-button { - // .icon { - // display: grid; - // align-items: center; - // justify-items: center; - // text-align: center; - // width: 1.25rem; - // height: 1.25rem; - // } - text-decoration: none; - - .text { - font-size: 0.65rem; - line-height: 0.65rem; - align-self: center; - margin-left: 0.25rem; - } - - // &:hover { - // cursor: pointer; - // color: var(--main-color); - // } - } - - .separator { - width: 2px; - height: 1rem; - background-color: var(--sub-color); - } -} - -#top.focus #menu .icon-button.discord::after { - background: transparent; -} - -#top.focus #menu { - color: transparent !important; -} - -#top.focus #menu .icon-button { - color: transparent !important; -} - -#top { - grid-template-areas: "logo menu config"; - line-height: 2.3rem; - font-size: 2.3rem; - /* text-align: center; */ - // transition: 0.25s; - padding: 0 5px; - display: grid; - grid-auto-flow: column; - grid-template-columns: auto 1fr auto; - z-index: 2; - align-items: center; - gap: 0.5rem; - user-select: none; - - .logo { - // margin-bottom: 0.6rem; - cursor: pointer; - display: grid; - grid-template-columns: auto 1fr; - gap: 0.5rem; - - .icon { - width: 2.5rem; - display: grid; - align-items: center; - background-color: transparent; - // margin-bottom: 0.15rem; - svg path { - transition: 0.25s; - fill: var(--main-color); - } - } - .text { - .top { - position: absolute; - left: 0.25rem; - top: -0.1rem; - font-size: 0.65rem; - line-height: 0.65rem; - color: var(--sub-color); - transition: 0.25s; - } - position: relative; - font-size: 2rem; - margin-bottom: 0.4rem; - font-family: "Lexend Deca"; - transition: 0.25s; - } - white-space: nowrap; - user-select: none; - - .bottom { - margin-left: -0.15rem; - color: var(--main-color); - transition: 0.25s; - cursor: pointer; - } - } - - .config { - grid-area: config; - transition: 0.125s; - .mobileConfig { - display: none; - .icon-button { - display: grid; - grid-auto-flow: column; - align-content: center; - transition: 0.25s; - margin-right: -1rem; - padding: 0.5rem 1rem; - font-size: 2rem; - border-radius: var(--roundness); - cursor: pointer; - color: var(--sub-color); - &:hover { - color: var(--text-color); - } - } - } - - .desktopConfig { - justify-self: right; - display: grid; - // grid-auto-flow: row; - grid-template-rows: 0.7rem 0.7rem 0.7rem; - grid-gap: 0.2rem; - // width: min-content; - // width: -moz-min-content; - // transition: 0.25s; - /* margin-bottom: 0.1rem; */ - justify-items: self-end; - - .group { - // transition: 0.25s; - - .title { - color: var(--sub-color); - font-size: 0.5rem; - line-height: 0.5rem; - margin-bottom: 0.15rem; - } - - .buttons { - font-size: 0.7rem; - line-height: 0.7rem; - display: flex; - } - &.disabled { - pointer-events: none; - opacity: 0.25; - } - } - - .punctuationMode { - margin-bottom: -0.1rem; - } - - .numbersMode { - margin-bottom: -0.1rem; - } - } - } - - .result { - display: grid; - grid-auto-flow: column; - grid-gap: 1rem; - width: min-content; - width: -moz-min-content; - transition: 0.25s; - grid-column: 3/4; - grid-row: 1/2; - - .group { - .title { - font-size: 0.65rem; - line-height: 0.65rem; - color: var(--sub-color); - } - - .val { - font-size: 1.7rem; - line-height: 1.7rem; - color: var(--main-color); - transition: 0.25s; - } - } - } - - //top focus - &.focus { - color: var(--sub-color) !important; - - .result { - opacity: 0 !important; - } - - .icon svg path { - fill: var(--sub-color) !important; - } - - .logo .text { - color: var(--sub-color) !important; - // opacity: 0 !important; - } - - .logo .top { - opacity: 0 !important; - } - - .config { - opacity: 0 !important; - } - } -} - -==> monkeytype/src/sass/scroll.scss <== -/* width */ -::-webkit-scrollbar { - width: 7px; -} - -/* Track */ -::-webkit-scrollbar-track { - background: transparent; -} - -/* Handle */ -::-webkit-scrollbar-thumb { - background: var(--sub-color); - transition: 0.25s; - border-radius: 2px !important; -} - -/* Handle on hover */ -::-webkit-scrollbar-thumb:hover { - background: var(--main-color); -} - -::-webkit-scrollbar-corner { - background: var(--sub-color); -} - -==> monkeytype/src/sass/settings.scss <== -.pageSettings { - display: grid; - // grid-template-columns: 1fr 1fr; - gap: 2rem; - - .tip { - color: var(--sub-color); - } - - .sectionGroupTitle { - font-size: 2rem; - color: var(--sub-color); - line-height: 2rem; - cursor: pointer; - transition: 0.25s; - - &:hover { - color: var(--text-color); - } - - .fas { - margin-left: 0.5rem; - - &.rotate { - transform: rotate(-90deg); - } - } - } - - .sectionSpacer { - height: 1.5rem; - } - - .settingsGroup { - display: grid; - gap: 2rem; - &.quickNav .links { - display: grid; - grid-auto-flow: column; - text-align: center; - a { - text-decoration: none; - width: 100%; - cursor: pointer; - // opacity: 0.5; - &:hover { - opacity: 1; - } - } - } - } - - .section { - display: grid; - // gap: .5rem; - grid-template-areas: - "title title" - "text buttons"; - grid-template-columns: 2fr 1fr; - column-gap: 2rem; - align-items: center; - - .button.danger { - box-shadow: 0px 0px 0px 2px var(--error-color); - color: var(--text-color); - &:hover { - background: var(--text-color); - color: var(--bg-color); - } - } - - .inputAndButton { - display: grid; - grid-template-columns: 8fr 1fr; - gap: 0.5rem; - margin-bottom: 0.5rem; - - .button { - height: auto; - - .fas { - margin-right: 0rem; - vertical-align: sub; - } - } - } - - &.themes .tabContainer [tabcontent="custom"] { - label.button:first-child { - color: var(--text-color); - } - label.button { - color: var(--bg-color); - } - } - - &.customBackgroundFilter { - .groups { - grid-area: buttons; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin-top: 2rem; - .group { - display: grid; - grid-template-columns: 1fr auto 2fr; - gap: 1rem; - .title, - .value { - color: var(--text-color); - } - } - } - .saveContainer { - grid-column: -1/-3; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 1rem; - } - .fas { - margin-right: 0rem; - } - } - - &.customTheme { - grid-template-columns: 1fr 1fr 1fr 1fr; - justify-items: stretch; - gap: 0.5rem 2rem; - - & p { - grid-area: unset; - grid-column: 1 / span 4; - } - - & .spacer { - grid-column: 3 / 5; - } - } - - h1 { - font-size: 1rem; - line-height: 1rem; - color: var(--sub-color); - margin: 0; - grid-area: title; - font-weight: 300; - } - - p { - grid-area: text; - color: var(--sub-color); - margin: 0; - } - - & > .text { - align-self: normal; - color: var(--text-color); - grid-area: text; - } - - .buttons { - display: grid; - grid-auto-flow: column; - grid-auto-columns: 1fr; - gap: 0.5rem; - grid-area: buttons; - &.vertical { - grid-auto-flow: unset; - } - } - - &.discordIntegration { - .info { - grid-area: buttons; - text-align: center; - color: var(--main-color); - } - - #unlinkDiscordButton { - margin-top: 0.5rem; - font-size: 0.75rem; - color: var(--sub-color); - &:hover { - color: var(--text-color); - } - } - - .howto { - margin-top: 1rem; - color: var(--text-color); - } - } - - &.tags { - .tagsListAndButton { - grid-area: buttons; - } - - .tag { - grid-template-columns: 6fr 1fr 1fr 1fr; - margin-bottom: 0.5rem; - } - - .addTagButton { - margin-top: 0.5rem; - color: var(--text-color); - cursor: pointer; - transition: 0.25s; - padding: 0.2rem 0.5rem; - border-radius: var(--roundness); - - background: rgba(0, 0, 0, 0.1); - text-align: center; - -webkit-user-select: none; - display: grid; - align-content: center; - height: min-content; - height: -moz-min-content; - - &.active { - background: var(--main-color); - color: var(--bg-color); - } - - &:hover, - &:focus { - color: var(--bg-color); - background: var(--text-color); - outline: none; - } - } - } - - &.presets { - .presetsListAndButton { - grid-area: buttons; - } - - .preset { - grid-template-columns: 7fr 1fr 1fr; - margin-bottom: 0.5rem; - } - - .addPresetButton { - margin-top: 0.5rem; - color: var(--text-color); - cursor: pointer; - transition: 0.25s; - padding: 0.2rem 0.5rem; - border-radius: var(--roundness); - - background: rgba(0, 0, 0, 0.1); - text-align: center; - -webkit-user-select: none; - display: grid; - align-content: center; - height: min-content; - height: -moz-min-content; - - &.active { - background: var(--main-color); - color: var(--bg-color); - } - - &:hover, - &:focus { - color: var(--bg-color); - background: var(--text-color); - outline: none; - } - } - } - - &.fontSize .buttons { - grid-template-columns: 1fr 1fr 1fr 1fr; - } - - &.themes { - .tabContainer { - position: relative; - grid-area: buttons; - - .tabContent { - overflow: revert; - height: auto; - - &.customTheme { - margin-top: 0.5rem; - .colorText { - color: var(--text-color); - } - } - - .text { - align-self: center; - } - } - } - - .theme.button { - display: grid; - grid-template-columns: auto 1fr auto; - .text { - color: inherit; - } - .activeIndicator { - overflow: hidden; - width: 1.25rem; - transition: 0.25s; - opacity: 0; - color: inherit; - .far { - margin: 0; - } - &.active { - width: 1.25rem; - opacity: 1; - } - } - .favButton { - overflow: hidden; - width: 1.25rem; - transition: 0.25s; - opacity: 0; - .far, - .fas { - margin: 0; - pointer-events: none; - } - &:hover { - cursor: pointer; - } - &.active { - width: 1.25rem; - opacity: 1; - } - } - &:hover { - .favButton { - width: 1.25rem; - opacity: 1; - } - } - &.active { - .activeIndicator { - opacity: 1; - } - } - } - } - - &.themes { - grid-template-columns: 2fr 1fr; - grid-template-areas: - "title tabs" - "text text" - "buttons buttons"; - column-gap: 2rem; - // row-gap: 0.5rem; - - .tabs { - display: grid; - grid-auto-flow: column; - grid-auto-columns: 1fr; - gap: 0.5rem; - grid-area: tabs; - } - - .buttons { - margin-left: 0; - grid-auto-flow: dense; - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 0.5rem; - margin-top: 0.5rem; - } - } - - &.fullWidth { - grid-template-columns: 2fr 1fr; - grid-template-areas: - "title tabs" - "text text" - "buttons buttons"; - column-gap: 2rem; - // row-gap: 0.5rem; - - .buttons { - margin-left: 0; - grid-auto-flow: dense; - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 0.5rem; - margin-top: 1rem; - } - } - - &.randomTheme .buttons { - grid-template-columns: 1fr 1fr 1fr 1fr 1fr; - } - } -} - -.buttons div.theme:hover { - transform: scale(1.1); -} - -==> monkeytype/src/sass/animations.scss <== -@keyframes loader { - 0% { - width: 0; - left: 0; - } - - 50% { - width: 100%; - left: 0; - } - - 100% { - width: 0; - left: 100%; - } -} - -@keyframes caretFlashSmooth { - 0%, - 100% { - opacity: 0; - } - - 50% { - opacity: 1; - } -} - -@keyframes caretFlashHard { - 0%, - 50% { - opacity: 1; - } - - 51%, - 100% { - opacity: 0; - } -} - -@keyframes flashKey { - from { - color: var(--bg-color); - background-color: var(--main-color); - border-color: var(--main-color); - } - - to { - color: var(--sub-color); - background-color: var(--bg-color); - border-color: var(--sub-color); - } -} - -@keyframes shake { - 0% { - transform: translate(4px, 0) rotate(0deg); - } - 50% { - transform: translate(-4px, 0) rotate(0deg); - } - 100% { - transform: translate(4px, 0) rotate(0deg); - } -} - -@keyframes flashHighlight { - 0% { - background-color: var(--bg-color); - } - 10% { - background-color: var(--main-color); - } - 40% { - background-color: var(--main-color); - } - 100% { - background-color: var(--bg-color); - } -} - -==> monkeytype/src/sass/footer.scss <== -#bottom { - position: relative; - text-align: center; - line-height: 1rem; - font-size: 0.75rem; - color: var(--sub-color); - // transition: 0.25s; - padding: 0 5px; - - // margin-bottom: 2rem; - .keyTips { - transition: 0.25s; - margin-bottom: 2rem; - } - - #supportMeButton { - transition: 0.25s; - &:hover { - color: var(--text-color); - cursor: pointer; - } - } - - #commandLineMobileButton { - display: none; - top: -4rem; - left: 0; - position: absolute; - font-size: 1rem; - width: 3rem; - height: 3rem; - text-align: center; - line-height: 3rem; - background: var(--main-color); - border-radius: 99rem; - z-index: 99; - cursor: pointer; - color: var(--bg-color); - transition: 0.25s; - } - - .leftright { - display: grid; - grid-template-columns: auto auto; - gap: 1rem; - a { - text-decoration: none; - } - .left { - text-align: left; - display: grid; - grid-auto-flow: column; - width: fit-content; - gap: 1rem; - width: -moz-fit-content; - } - .right { - text-align: right; - display: grid; - grid-auto-flow: column; - width: fit-content; - width: -moz-fit-content; - justify-self: right; - gap: 1rem; - // align-items: center; - } - .left a, - .right a { - display: grid; - grid-auto-flow: column; - gap: 0.25rem; - align-items: baseline; - width: max-content; - width: -moz-available; - &:hover { - color: var(--text-color); - cursor: pointer; - } - } - } - - .version { - opacity: 0; - } -} - -#bottom.focus { - .keyTips { - opacity: 0 !important; - } - a { - opacity: 0 !important; - } - #commandLineMobileButton { - opacity: 0 !important; - pointer-events: none !important; - } -} - -==> ./monkeytype/src/js/theme-colors.js <== -// export let bg = "#323437"; -// export let main = "#e2b714"; -// export let caret = "#e2b714"; -// export let sub = "#646669"; -// export let text = "#d1d0c5"; -// export let error = "#ca4754"; -// export let errorExtra = "#7e2a33"; -// export let colorfulError = "#ca4754"; -// export let colorfulErrorExtra = "#7e2a33"; - -let colors = { - bg: "#323437", - main: "#e2b714", - caret: "#e2b714", - sub: "#646669", - text: "#d1d0c5", - error: "#ca4754", - errorExtra: "#7e2a33", - colorfulError: "#ca4754", - colorfulErrorExtra: "#7e2a33", -}; - -export async function get(color) { - let ret; - - if (color === undefined) { - ret = colors; - } else { - ret = colors[color]; - } - - return ret; - - // return check(); - - // async function run() { - // return new Promise(function (resolve, reject) { - // window.setTimeout(() => { - // update(); - // if (color === undefined) { - // ret = colors; - // } else { - // ret = colors[color]; - // } - // resolve(check()); - // }, 250); - // }); - // } - // async function check() { - // if (color === undefined) { - // if (ret.bg === "") { - // return await run(); - // } else { - // return ret; - // } - // } else { - // if (ret === "") { - // return await run(); - // } else { - // return ret; - // } - // } - // } -} - -export function reset() { - colors = { - bg: "", - main: "", - caret: "", - sub: "", - text: "", - error: "", - errorExtra: "", - colorfulError: "", - colorfulErrorExtra: "", - }; -} - -export function update() { - let st = getComputedStyle(document.body); - colors.bg = st.getPropertyValue("--bg-color").replace(" ", ""); - colors.main = st.getPropertyValue("--main-color").replace(" ", ""); - colors.caret = st.getPropertyValue("--caret-color").replace(" ", ""); - colors.sub = st.getPropertyValue("--sub-color").replace(" ", ""); - colors.text = st.getPropertyValue("--text-color").replace(" ", ""); - colors.error = st.getPropertyValue("--error-color").replace(" ", ""); - colors.errorExtra = st - .getPropertyValue("--error-extra-color") - .replace(" ", ""); - colors.colorfulError = st - .getPropertyValue("--colorful-error-color") - .replace(" ", ""); - colors.colorfulErrorExtra = st - .getPropertyValue("--colorful-error-extra-color") - .replace(" ", ""); -} - -==> ./monkeytype/src/js/simple-popups.js <== -import * as Loader from "./loader"; -import * as Notifications from "./notifications"; -import * as AccountController from "./account-controller"; -import * as DB from "./db"; -import * as Settings from "./settings"; -import axiosInstance from "./axios-instance"; -import * as UpdateConfig from "./config"; - -export let list = {}; -class SimplePopup { - constructor( - id, - type, - title, - inputs = [], - text = "", - buttonText = "Confirm", - execFn, - beforeShowFn - ) { - this.parameters = []; - this.id = id; - this.type = type; - this.execFn = execFn; - this.title = title; - this.inputs = inputs; - this.text = text; - this.wrapper = $("#simplePopupWrapper"); - this.element = $("#simplePopup"); - this.buttonText = buttonText; - this.beforeShowFn = beforeShowFn; - } - reset() { - this.element.html(` -
-
-
-
`); - } - - init() { - let el = this.element; - el.find("input").val(""); - // if (el.attr("popupId") !== this.id) { - this.reset(); - el.attr("popupId", this.id); - el.find(".title").text(this.title); - el.find(".text").text(this.text); - - this.initInputs(); - - if (!this.buttonText) { - el.find(".button").remove(); - } else { - el.find(".button").text(this.buttonText); - } - - // } - } - - initInputs() { - let el = this.element; - if (this.inputs.length > 0) { - if (this.type === "number") { - this.inputs.forEach((input) => { - el.find(".inputs").append(` - - `); - }); - } else if (this.type === "text") { - this.inputs.forEach((input) => { - if (input.type) { - el.find(".inputs").append(` - - `); - } else { - el.find(".inputs").append(` - - `); - } - }); - } - el.find(".inputs").removeClass("hidden"); - } else { - el.find(".inputs").addClass("hidden"); - } - } - - exec() { - let vals = []; - $.each($("#simplePopup input"), (index, el) => { - vals.push($(el).val()); - }); - this.execFn(...vals); - this.hide(); - } - - show(parameters) { - this.parameters = parameters; - this.beforeShowFn(); - this.init(); - this.wrapper - .stop(true, true) - .css("opacity", 0) - .removeClass("hidden") - .animate({ opacity: 1 }, 125, () => { - $($("#simplePopup").find("input")[0]).focus(); - }); - } - - hide() { - this.wrapper - .stop(true, true) - .css("opacity", 1) - .removeClass("hidden") - .animate({ opacity: 0 }, 125, () => { - this.wrapper.addClass("hidden"); - }); - } -} - -export function hide() { - $("#simplePopupWrapper") - .stop(true, true) - .css("opacity", 1) - .removeClass("hidden") - .animate({ opacity: 0 }, 125, () => { - $("#simplePopupWrapper").addClass("hidden"); - }); -} - -$("#simplePopupWrapper").mousedown((e) => { - if ($(e.target).attr("id") === "simplePopupWrapper") { - $("#simplePopupWrapper") - .stop(true, true) - .css("opacity", 1) - .removeClass("hidden") - .animate({ opacity: 0 }, 125, () => { - $("#simplePopupWrapper").addClass("hidden"); - }); - } -}); - -$(document).on("click", "#simplePopupWrapper .button", (e) => { - let id = $("#simplePopup").attr("popupId"); - list[id].exec(); -}); - -$(document).on("keyup", "#simplePopupWrapper input", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - let id = $("#simplePopup").attr("popupId"); - list[id].exec(); - } -}); - -list.updateEmail = new SimplePopup( - "updateEmail", - "text", - "Update Email", - [ - { - placeholder: "Password", - type: "password", - initVal: "", - }, - { - placeholder: "New email", - initVal: "", - }, - { - placeholder: "Confirm new email", - initVal: "", - }, - ], - "", - "Update", - async (password, email, emailConfirm) => { - try { - const user = firebase.auth().currentUser; - if (email !== emailConfirm) { - Notifications.add("Emails don't match", 0); - return; - } - if (user.providerData[0].providerId === "password") { - const credential = firebase.auth.EmailAuthProvider.credential( - user.email, - password - ); - await user.reauthenticateWithCredential(credential); - } - Loader.show(); - let response; - try { - response = await axiosInstance.post("/user/updateEmail", { - uid: user.uid, - previousEmail: user.email, - newEmail: email, - }); - } catch (e) { - Loader.hide(); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to update email: " + msg, -1); - return; - } - Loader.hide(); - if (response.status !== 200) { - Notifications.add(response.data.message); - return; - } else { - Notifications.add("Email updated", 1); - } - } catch (e) { - if (e.code == "auth/wrong-password") { - Notifications.add("Incorrect password", -1); - } else { - Notifications.add("Something went wrong: " + e, -1); - } - } - }, - () => { - const user = firebase.auth().currentUser; - if (!user.providerData.find((p) => p.providerId === "password")) { - eval(`this.inputs = []`); - eval(`this.buttonText = undefined`); - eval(`this.text = "Password authentication is not enabled";`); - } - } -); - -list.updateName = new SimplePopup( - "updateName", - "text", - "Update Name", - [ - { - placeholder: "Password", - type: "password", - initVal: "", - }, - { - placeholder: "New name", - type: "text", - initVal: "", - }, - ], - "", - "Update", - async (pass, newName) => { - try { - const user = firebase.auth().currentUser; - if (user.providerData[0].providerId === "password") { - const credential = firebase.auth.EmailAuthProvider.credential( - user.email, - pass - ); - await user.reauthenticateWithCredential(credential); - } else if (user.providerData[0].providerId === "google.com") { - await user.reauthenticateWithPopup(AccountController.gmailProvider); - } - Loader.show(); - - let response; - try { - response = await axiosInstance.post("/user/checkName", { - name: newName, - }); - } catch (e) { - Loader.hide(); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to check name: " + msg, -1); - return; - } - Loader.hide(); - if (response.status !== 200) { - Notifications.add(response.data.message); - return; - } - try { - response = await axiosInstance.post("/user/updateName", { - name: newName, - }); - } catch (e) { - Loader.hide(); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to update name: " + msg, -1); - return; - } - Loader.hide(); - if (response.status !== 200) { - Notifications.add(response.data.message); - return; - } else { - Notifications.add("Name updated", 1); - DB.getSnapshot().name = newName; - $("#menu .icon-button.account .text").text(newName); - } - } catch (e) { - Loader.hide(); - if (e.code == "auth/wrong-password") { - Notifications.add("Incorrect password", -1); - } else { - Notifications.add("Something went wrong: " + e, -1); - } - } - }, - () => { - const user = firebase.auth().currentUser; - if (user.providerData[0].providerId === "google.com") { - eval(`this.inputs[0].hidden = true`); - eval(`this.buttonText = "Reauthenticate to update"`); - } - } -); - -list.updatePassword = new SimplePopup( - "updatePassword", - "text", - "Update Password", - [ - { - placeholder: "Password", - type: "password", - initVal: "", - }, - { - placeholder: "New password", - type: "password", - initVal: "", - }, - { - placeholder: "Confirm new password", - type: "password", - initVal: "", - }, - ], - "", - "Update", - async (previousPass, newPass, newPassConfirm) => { - try { - const user = firebase.auth().currentUser; - const credential = firebase.auth.EmailAuthProvider.credential( - user.email, - previousPass - ); - if (newPass !== newPassConfirm) { - Notifications.add("New passwords don't match", 0); - return; - } - Loader.show(); - await user.reauthenticateWithCredential(credential); - await user.updatePassword(newPass); - Loader.hide(); - Notifications.add("Password updated", 1); - } catch (e) { - Loader.hide(); - if (e.code == "auth/wrong-password") { - Notifications.add("Incorrect password", -1); - } else { - Notifications.add("Something went wrong: " + e, -1); - } - } - }, - () => { - const user = firebase.auth().currentUser; - if (!user.providerData.find((p) => p.providerId === "password")) { - eval(`this.inputs = []`); - eval(`this.buttonText = undefined`); - eval(`this.text = "Password authentication is not enabled";`); - } - } -); - -list.addPasswordAuth = new SimplePopup( - "addPasswordAuth", - "text", - "Add Password Authentication", - [ - { - placeholder: "email", - type: "email", - initVal: "", - }, - { - placeholder: "confirm email", - type: "email", - initVal: "", - }, - { - placeholder: "new password", - type: "password", - initVal: "", - }, - { - placeholder: "confirm new password", - type: "password", - initVal: "", - }, - ], - "", - "Add", - async (email, emailConfirm, pass, passConfirm) => { - if (email !== emailConfirm) { - Notifications.add("Emails don't match", 0); - return; - } - - if (pass !== passConfirm) { - Notifications.add("Passwords don't match", 0); - return; - } - - AccountController.addPasswordAuth(email, pass); - }, - () => {} -); - -list.deleteAccount = new SimplePopup( - "deleteAccount", - "text", - "Delete Account", - [ - { - placeholder: "Password", - type: "password", - initVal: "", - }, - ], - "This is the last time you can change your mind. After pressing the button everything is gone.", - "Delete", - async (password) => { - // - try { - const user = firebase.auth().currentUser; - if (user.providerData[0].providerId === "password") { - const credential = firebase.auth.EmailAuthProvider.credential( - user.email, - password - ); - await user.reauthenticateWithCredential(credential); - } else if (user.providerData[0].providerId === "google.com") { - await user.reauthenticateWithPopup(AccountController.gmailProvider); - } - Loader.show(); - - Notifications.add("Deleting stats...", 0); - let response; - try { - response = await axiosInstance.post("/user/delete"); - } catch (e) { - Loader.hide(); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to delete user stats: " + msg, -1); - return; - } - if (response.status !== 200) { - throw response.data.message; - } - - Notifications.add("Deleting results...", 0); - try { - response = await axiosInstance.post("/results/deleteAll"); - } catch (e) { - Loader.hide(); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to delete user results: " + msg, -1); - return; - } - if (response.status !== 200) { - throw response.data.message; - } - - Notifications.add("Deleting login information...", 0); - await firebase.auth().currentUser.delete(); - - Notifications.add("Goodbye", 1, 5); - - setTimeout(() => { - location.reload(); - }, 3000); - } catch (e) { - Loader.hide(); - if (e.code == "auth/wrong-password") { - Notifications.add("Incorrect password", -1); - } else { - Notifications.add("Something went wrong: " + e, -1); - } - } - }, - () => { - const user = firebase.auth().currentUser; - if (user.providerData[0].providerId === "google.com") { - eval(`this.inputs = []`); - eval(`this.buttonText = "Reauthenticate to delete"`); - } - } -); - -list.clearTagPb = new SimplePopup( - "clearTagPb", - "text", - "Clear Tag PB", - [], - `Are you sure you want to clear this tags PB?`, - "Clear", - () => { - let tagid = eval("this.parameters[0]"); - Loader.show(); - axiosInstance - .post("/user/tags/clearPb", { - tagid: tagid, - }) - .then((res) => { - Loader.hide(); - if (res.data.resultCode === 1) { - let tag = DB.getSnapshot().tags.filter((t) => t.id === tagid)[0]; - tag.pb = 0; - $( - `.pageSettings .section.tags .tagsList .tag[id="${tagid}"] .clearPbButton` - ).attr("aria-label", "No PB found"); - Notifications.add("Tag PB cleared.", 0); - } else { - Notifications.add("Something went wrong: " + res.data.message, -1); - } - }) - .catch((e) => { - Loader.hide(); - if (e.code == "auth/wrong-password") { - Notifications.add("Incorrect password", -1); - } else { - Notifications.add("Something went wrong: " + e, -1); - } - }); - // console.log(`clearing for ${eval("this.parameters[0]")} ${eval("this.parameters[1]")}`); - }, - () => { - eval( - "this.text = `Are you sure you want to clear PB for tag ${eval('this.parameters[1]')}?`" - ); - } -); - -list.applyCustomFont = new SimplePopup( - "applyCustomFont", - "text", - "Custom font", - [{ placeholder: "Font name", initVal: "" }], - "Make sure you have the font installed on your computer before applying.", - "Apply", - (fontName) => { - if (fontName === "") return; - Settings.groups.fontFamily?.setValue(fontName.replace(/\s/g, "_")); - }, - () => {} -); - -list.resetPersonalBests = new SimplePopup( - "resetPersonalBests", - "text", - "Reset Personal Bests", - [ - { - placeholder: "Password", - type: "password", - initVal: "", - }, - ], - "", - "Reset", - async (password) => { - try { - const user = firebase.auth().currentUser; - if (user.providerData[0].providerId === "password") { - const credential = firebase.auth.EmailAuthProvider.credential( - user.email, - password - ); - await user.reauthenticateWithCredential(credential); - } else if (user.providerData[0].providerId === "google.com") { - await user.reauthenticateWithPopup(AccountController.gmailProvider); - } - Loader.show(); - - let response; - try { - response = await axiosInstance.post("/user/clearPb"); - } catch (e) { - Loader.hide(); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to reset personal bests: " + msg, -1); - return; - } - Loader.hide(); - if (response.status !== 200) { - Notifications.add(response.data.message); - } else { - Notifications.add("Personal bests have been reset", 1); - DB.getSnapshot().personalBests = {}; - } - } catch (e) { - Loader.hide(); - Notifications.add(e, -1); - } - }, - () => { - const user = firebase.auth().currentUser; - if (user.providerData[0].providerId === "google.com") { - eval(`this.inputs = []`); - eval(`this.buttonText = "Reauthenticate to reset"`); - } - } -); - -list.resetSettings = new SimplePopup( - "resetSettings", - "text", - "Reset Settings", - [], - "Are you sure you want to reset all your settings?", - "Reset", - () => { - UpdateConfig.reset(); - // setTimeout(() => { - // location.reload(); - // }, 1000); - }, - () => {} -); - -list.unlinkDiscord = new SimplePopup( - "unlinkDiscord", - "text", - "Unlink Discord", - [], - "Are you sure you want to unlink your Discord account?", - "Unlink", - async () => { - Loader.show(); - let response; - try { - response = await axiosInstance.post("/user/discord/unlink", {}); - } catch (e) { - Loader.hide(); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to unlink Discord: " + msg, -1); - return; - } - Loader.hide(); - if (response.status !== 200) { - Notifications.add(response.data.message); - } else { - Notifications.add("Accounts unlinked", 1); - DB.getSnapshot().discordId = undefined; - Settings.updateDiscordSection(); - } - }, - () => {} -); - -==> ./monkeytype/src/js/settings/settings-group.js <== -import Config from "./config"; - -export default class SettingsGroup { - constructor( - configName, - toggleFunction, - setCallback = null, - updateCallback = null - ) { - this.configName = configName; - this.configValue = Config[configName]; - this.onOff = typeof this.configValue === "boolean"; - this.toggleFunction = toggleFunction; - this.setCallback = setCallback; - this.updateCallback = updateCallback; - - this.updateButton(); - - $(document).on( - "click", - `.pageSettings .section.${this.configName} .button`, - (e) => { - let target = $(e.currentTarget); - if (target.hasClass("disabled") || target.hasClass("no-auto-handle")) - return; - if (this.onOff) { - if (target.hasClass("on")) { - this.setValue(true); - } else { - this.setValue(false); - } - this.updateButton(); - if (this.setCallback !== null) this.setCallback(); - } else { - const value = target.attr(configName); - const params = target.attr("params"); - if (!value && !params) return; - this.setValue(value, params); - } - } - ); - } - - setValue(value, params = undefined) { - if (params === undefined) { - this.toggleFunction(value); - } else { - this.toggleFunction(value, ...params); - } - this.updateButton(); - if (this.setCallback !== null) this.setCallback(); - } - - updateButton() { - this.configValue = Config[this.configName]; - $(`.pageSettings .section.${this.configName} .button`).removeClass( - "active" - ); - if (this.onOff) { - const onOffString = this.configValue ? "on" : "off"; - $( - `.pageSettings .section.${this.configName} .buttons .button.${onOffString}` - ).addClass("active"); - } else { - $( - `.pageSettings .section.${this.configName} .button[${this.configName}='${this.configValue}']` - ).addClass("active"); - } - if (this.updateCallback !== null) this.updateCallback(); - } -} - -==> ./monkeytype/src/js/settings/language-picker.js <== -import * as Misc from "./misc"; -import Config, * as UpdateConfig from "./config"; - -export async function setActiveGroup(groupName, clicked = false) { - let currentGroup; - if (groupName === undefined) { - currentGroup = await Misc.findCurrentGroup(Config.language); - } else { - let groups = await Misc.getLanguageGroups(); - groups.forEach((g) => { - if (g.name === groupName) { - currentGroup = g; - } - }); - } - $(`.pageSettings .section.languageGroups .button`).removeClass("active"); - $( - `.pageSettings .section.languageGroups .button[group='${currentGroup.name}']` - ).addClass("active"); - - let langEl = $(".pageSettings .section.language .buttons").empty(); - currentGroup.languages.forEach((language) => { - langEl.append( - `
${language.replace( - /_/g, - " " - )}
` - ); - }); - - if (clicked) { - $($(`.pageSettings .section.language .buttons .button`)[0]).addClass( - "active" - ); - UpdateConfig.setLanguage(currentGroup.languages[0]); - } else { - $( - `.pageSettings .section.language .buttons .button[language=${Config.language}]` - ).addClass("active"); - } -} - -==> ./monkeytype/src/js/settings/theme-picker.js <== -import Config, * as UpdateConfig from "./config"; -import * as ThemeController from "./theme-controller"; -import * as Misc from "./misc"; -import * as Notifications from "./notifications"; -import * as CommandlineLists from "./commandline-lists"; -import * as ThemeColors from "./theme-colors"; -import * as ChartController from "./chart-controller"; - -export function updateActiveButton() { - let activeThemeName = Config.theme; - if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) { - activeThemeName = ThemeController.randomTheme; - } - $(`.pageSettings .section.themes .theme`).removeClass("active"); - $(`.pageSettings .section.themes .theme[theme=${activeThemeName}]`).addClass( - "active" - ); -} - -function updateColors(colorPicker, color, onlyStyle, noThemeUpdate = false) { - if (onlyStyle) { - let colorid = colorPicker.find("input[type=color]").attr("id"); - if (!noThemeUpdate) - document.documentElement.style.setProperty(colorid, color); - let pickerButton = colorPicker.find("label"); - pickerButton.val(color); - pickerButton.attr("value", color); - if (pickerButton.attr("for") !== "--bg-color") - pickerButton.css("background-color", color); - colorPicker.find("input[type=text]").val(color); - colorPicker.find("input[type=color]").attr("value", color); - return; - } - let colorREGEX = [ - { - rule: /\b[0-9]{1,3},\s?[0-9]{1,3},\s?[0-9]{1,3}\s*\b/, - start: "rgb(", - end: ")", - }, - { - rule: /\b[A-Z, a-z, 0-9]{6}\b/, - start: "#", - end: "", - }, - { - rule: /\b[0-9]{1,3},\s?[0-9]{1,3}%,\s?[0-9]{1,3}%?\s*\b/, - start: "hsl(", - end: ")", - }, - ]; - - color = color.replace("°", ""); - - for (let regex of colorREGEX) { - if (color.match(regex.rule)) { - color = regex.start + color + regex.end; - break; - } - } - - $(".colorConverter").css("color", color); - color = Misc.convertRGBtoHEX($(".colorConverter").css("color")); - if (!color) { - return; - } - - let colorid = colorPicker.find("input[type=color]").attr("id"); - - if (!noThemeUpdate) - document.documentElement.style.setProperty(colorid, color); - - let pickerButton = colorPicker.find("label"); - - pickerButton.val(color); - pickerButton.attr("value", color); - if (pickerButton.attr("for") !== "--bg-color") - pickerButton.css("background-color", color); - colorPicker.find("input[type=text]").val(color); - colorPicker.find("input[type=color]").attr("value", color); -} - -export function refreshButtons() { - let favThemesEl = $( - ".pageSettings .section.themes .favThemes.buttons" - ).empty(); - let themesEl = $(".pageSettings .section.themes .allThemes.buttons").empty(); - - let activeThemeName = Config.theme; - if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) { - activeThemeName = ThemeController.randomTheme; - } - - Misc.getSortedThemesList().then((themes) => { - //first show favourites - if (Config.favThemes.length > 0) { - favThemesEl.css({ paddingBottom: "1rem" }); - themes.forEach((theme) => { - if (Config.favThemes.includes(theme.name)) { - let activeTheme = activeThemeName === theme.name ? "active" : ""; - favThemesEl.append( - `
-
-
${theme.name.replace(/_/g, " ")}
-
` - ); - } - }); - } else { - favThemesEl.css({ paddingBottom: "0" }); - } - //then the rest - themes.forEach((theme) => { - if (!Config.favThemes.includes(theme.name)) { - let activeTheme = activeThemeName === theme.name ? "active" : ""; - themesEl.append( - `
-
-
${theme.name.replace(/_/g, " ")}
-
` - ); - } - }); - updateActiveButton(); - }); -} - -export function setCustomInputs(noThemeUpdate) { - $( - ".pageSettings .section.themes .tabContainer .customTheme .colorPicker" - ).each((n, index) => { - let currentColor = - Config.customThemeColors[ - ThemeController.colorVars.indexOf( - $(index).find("input[type=color]").attr("id") - ) - ]; - - //todo check if needed - // $(index).find("input[type=color]").val(currentColor); - // $(index).find("input[type=color]").attr("value", currentColor); - // $(index).find("input[type=text]").val(currentColor); - updateColors($(index), currentColor, false, noThemeUpdate); - }); -} - -function toggleFavourite(themename) { - if (Config.favThemes.includes(themename)) { - //already favourite, remove - UpdateConfig.setFavThemes( - Config.favThemes.filter((t) => { - if (t !== themename) { - return t; - } - }) - ); - } else { - //add to favourites - let newlist = Config.favThemes; - newlist.push(themename); - UpdateConfig.setFavThemes(newlist); - } - UpdateConfig.saveToLocalStorage(); - refreshButtons(); - // showFavouriteThemesAtTheTop(); - CommandlineLists.updateThemeCommands(); -} - -export function updateActiveTab() { - $(".pageSettings .section.themes .tabs .button").removeClass("active"); - if (!Config.customTheme) { - $(".pageSettings .section.themes .tabs .button[tab='preset']").addClass( - "active" - ); - - // UI.swapElements( - // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), - // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), - // 250 - // ); - } else { - $(".pageSettings .section.themes .tabs .button[tab='custom']").addClass( - "active" - ); - - // UI.swapElements( - // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), - // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), - // 250 - // ); - } -} - -$(".pageSettings .section.themes .tabs .button").click((e) => { - $(".pageSettings .section.themes .tabs .button").removeClass("active"); - var $target = $(e.currentTarget); - $target.addClass("active"); - setCustomInputs(); - if ($target.attr("tab") == "preset") { - UpdateConfig.setCustomTheme(false); - // ThemeController.set(Config.theme); - // applyCustomThemeColors(); - // UI.swapElements( - // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), - // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), - // 250 - // ); - } else { - UpdateConfig.setCustomTheme(true); - // ThemeController.set("custom"); - // applyCustomThemeColors(); - // UI.swapElements( - // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), - // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), - // 250 - // ); - } -}); - -$(document).on( - "click", - ".pageSettings .section.themes .theme .favButton", - (e) => { - let theme = $(e.currentTarget).parents(".theme.button").attr("theme"); - toggleFavourite(theme); - } -); - -$(document).on("click", ".pageSettings .section.themes .theme.button", (e) => { - let theme = $(e.currentTarget).attr("theme"); - if (!$(e.target).hasClass("favButton")) { - UpdateConfig.setTheme(theme); - // ThemePicker.refreshButtons(); - updateActiveButton(); - } -}); - -$( - ".pageSettings .section.themes .tabContainer .customTheme input[type=color]" -).on("input", (e) => { - // UpdateConfig.setCustomTheme(true, true); - let $colorVar = $(e.currentTarget).attr("id"); - let $pickedColor = $(e.currentTarget).val(); - - //todo check if needed - // document.documentElement.style.setProperty($colorVar, $pickedColor); - // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); - // $(".colorPicker #" + $colorVar).val($pickedColor); - // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); - // }); - - // $( - // ".pageSettings .section.themes .tabContainer .customTheme input[type=text]" - // ).on("input", (e) => { - // // UpdateConfig.setCustomTheme(true, true); - // let $colorVar = $(e.currentTarget).attr("id").replace("-txt", ""); - // let $pickedColor = $(e.currentTarget).val(); - - // document.documentElement.style.setProperty($colorVar, $pickedColor); - // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); - // $(".colorPicker #" + $colorVar).val($pickedColor); - // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); - updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor, true); -}); - -$( - ".pageSettings .section.themes .tabContainer .customTheme input[type=color]" -).on("change", (e) => { - // UpdateConfig.setCustomTheme(true, true); - let $colorVar = $(e.currentTarget).attr("id"); - let $pickedColor = $(e.currentTarget).val(); - - //todo check if needed - // document.documentElement.style.setProperty($colorVar, $pickedColor); - // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); - // $(".colorPicker #" + $colorVar).val($pickedColor); - // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); - // }); - - // $( - // ".pageSettings .section.themes .tabContainer .customTheme input[type=text]" - // ).on("input", (e) => { - // // UpdateConfig.setCustomTheme(true, true); - // let $colorVar = $(e.currentTarget).attr("id").replace("-txt", ""); - // let $pickedColor = $(e.currentTarget).val(); - - // document.documentElement.style.setProperty($colorVar, $pickedColor); - // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); - // $(".colorPicker #" + $colorVar).val($pickedColor); - // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); - updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); -}); - -$(".pageSettings .section.themes .tabContainer .customTheme input[type=text]") - .on("blur", (e) => { - let $colorVar = $(e.currentTarget).attr("id"); - let $pickedColor = $(e.currentTarget).val(); - - updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); - }) - .on("keypress", function (e) { - if (e.which === 13) { - $(this).attr("disabled", "disabled"); - let $colorVar = $(e.currentTarget).attr("id"); - let $pickedColor = $(e.currentTarget).val(); - - updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); - $(this).removeAttr("disabled"); - } - }); - -$(".pageSettings .saveCustomThemeButton").click((e) => { - let save = []; - $.each( - $(".pageSettings .section.customTheme [type='color']"), - (index, element) => { - save.push($(element).attr("value")); - } - ); - UpdateConfig.setCustomThemeColors(save); - ThemeController.set("custom"); - Notifications.add("Custom theme colors saved", 1); -}); - -$(".pageSettings #loadCustomColorsFromPreset").click((e) => { - // previewTheme(Config.theme); - $("#currentTheme").attr("href", `themes/${Config.theme}.css`); - - ThemeController.colorVars.forEach((e) => { - document.documentElement.style.setProperty(e, ""); - }); - - setTimeout(async () => { - ChartController.updateAllChartColors(); - - let themecolors = await ThemeColors.get(); - - ThemeController.colorVars.forEach((colorName) => { - let color; - if (colorName === "--bg-color") { - color = themecolors.bg; - } else if (colorName === "--main-color") { - color = themecolors.main; - } else if (colorName === "--sub-color") { - color = themecolors.sub; - } else if (colorName === "--caret-color") { - color = themecolors.caret; - } else if (colorName === "--text-color") { - color = themecolors.text; - } else if (colorName === "--error-color") { - color = themecolors.error; - } else if (colorName === "--error-extra-color") { - color = themecolors.errorExtra; - } else if (colorName === "--colorful-error-color") { - color = themecolors.colorfulError; - } else if (colorName === "--colorful-error-extra-color") { - color = themecolors.colorfulErrorExtra; - } - - updateColors($(".colorPicker #" + colorName).parent(), color); - }); - }, 250); -}); - -==> ./monkeytype/src/js/challenge-controller.js <== -import * as Misc from "./misc"; -import * as Notifications from "./notifications"; -import * as ManualRestart from "./manual-restart-tracker"; -import * as CustomText from "./custom-text"; -import * as TestLogic from "./test-logic"; -import * as Funbox from "./funbox"; -import Config, * as UpdateConfig from "./config"; -import * as UI from "./ui"; -import * as TestUI from "./test-ui"; - -export let active = null; -let challengeLoading = false; - -export function clearActive() { - if (active && !challengeLoading && !TestUI.testRestarting) { - Notifications.add("Challenge cleared", 0); - active = null; - } -} - -export function verify(result) { - try { - if (active) { - let afk = (result.afkDuration / result.testDuration) * 100; - - if (afk > 10) { - Notifications.add(`Challenge failed: AFK time is greater than 10%`, 0); - return null; - } - - if (!active.requirements) { - Notifications.add(`${active.display} challenge passed!`, 1); - return active.name; - } else { - let requirementsMet = true; - let failReasons = []; - for (let requirementType in active.requirements) { - if (requirementsMet == false) return; - let requirementValue = active.requirements[requirementType]; - if (requirementType == "wpm") { - let wpmMode = Object.keys(requirementValue)[0]; - if (wpmMode == "exact") { - if (Math.round(result.wpm) != requirementValue.exact) { - requirementsMet = false; - failReasons.push(`WPM not ${requirementValue.exact}`); - } - } else if (wpmMode == "min") { - if (result.wpm < requirementValue.min) { - requirementsMet = false; - failReasons.push(`WPM below ${requirementValue.min}`); - } - } - } else if (requirementType == "acc") { - let accMode = Object.keys(requirementValue)[0]; - if (accMode == "exact") { - if (result.acc != requirementValue.exact) { - requirementsMet = false; - failReasons.push(`Accuracy not ${requirementValue.exact}`); - } - } else if (accMode == "min") { - if (result.acc < requirementValue.min) { - requirementsMet = false; - failReasons.push(`Accuracy below ${requirementValue.min}`); - } - } - } else if (requirementType == "afk") { - let afkMode = Object.keys(requirementValue)[0]; - if (afkMode == "max") { - if (Math.round(afk) > requirementValue.max) { - requirementsMet = false; - failReasons.push( - `AFK percentage above ${requirementValue.max}` - ); - } - } - } else if (requirementType == "time") { - let timeMode = Object.keys(requirementValue)[0]; - if (timeMode == "min") { - if (Math.round(result.testDuration) < requirementValue.min) { - requirementsMet = false; - failReasons.push(`Test time below ${requirementValue.min}`); - } - } - } else if (requirementType == "funbox") { - let funboxMode = requirementValue; - if (funboxMode != result.funbox) { - requirementsMet = false; - failReasons.push(`${funboxMode} funbox not active`); - } - } else if (requirementType == "raw") { - let rawMode = Object.keys(requirementValue)[0]; - if (rawMode == "exact") { - if (Math.round(result.rawWpm) != requirementValue.exact) { - requirementsMet = false; - failReasons.push(`Raw WPM not ${requirementValue.exact}`); - } - } - } else if (requirementType == "con") { - let conMode = Object.keys(requirementValue)[0]; - if (conMode == "exact") { - if (Math.round(result.consistency) != requirementValue.exact) { - requirementsMet = false; - failReasons.push(`Consistency not ${requirementValue.exact}`); - } - } - } else if (requirementType == "config") { - for (let configKey in requirementValue) { - let configValue = requirementValue[configKey]; - if (Config[configKey] != configValue) { - requirementsMet = false; - failReasons.push(`${configKey} not set to ${configValue}`); - } - } - } - } - if (requirementsMet) { - if (active.autoRole) { - Notifications.add( - "You will receive a role shortly. Please don't post a screenshot in challenge submissions.", - 1, - 5 - ); - } - Notifications.add(`${active.display} challenge passed!`, 1); - return active.name; - } else { - Notifications.add( - `${active.display} challenge failed: ${failReasons.join(", ")}`, - 0 - ); - return null; - } - } - } else { - return null; - } - } catch (e) { - console.error(e); - Notifications.add( - `Something went wrong when verifying challenge: ${e.message}`, - 0 - ); - return null; - } -} - -export async function setup(challengeName) { - challengeLoading = true; - if (UI.getActivePage() !== "pageTest") { - UI.changePage("", true); - } - - let list = await Misc.getChallengeList(); - let challenge = list.filter((c) => c.name === challengeName)[0]; - let notitext; - try { - if (challenge === undefined) { - Notifications.add("Challenge not found", 0); - ManualRestart.set(); - TestLogic.restart(false, true); - setTimeout(() => { - $("#top .config").removeClass("hidden"); - $(".page.pageTest").removeClass("hidden"); - }, 250); - return; - } - if (challenge.type === "customTime") { - UpdateConfig.setTimeConfig(challenge.parameters[0], true); - UpdateConfig.setMode("time", true); - UpdateConfig.setDifficulty("normal", true); - if (challenge.name === "englishMaster") { - UpdateConfig.setLanguage("english_10k", true); - UpdateConfig.setNumbers(true, true); - UpdateConfig.setPunctuation(true, true); - } - } else if (challenge.type === "customWords") { - UpdateConfig.setWordCount(challenge.parameters[0], true); - UpdateConfig.setMode("words", true); - UpdateConfig.setDifficulty("normal", true); - } else if (challenge.type === "customText") { - CustomText.setText(challenge.parameters[0].split(" ")); - CustomText.setIsWordRandom(challenge.parameters[1]); - CustomText.setWord(parseInt(challenge.parameters[2])); - UpdateConfig.setMode("custom", true); - UpdateConfig.setDifficulty("normal", true); - } else if (challenge.type === "script") { - let scriptdata = await fetch("/challenges/" + challenge.parameters[0]); - scriptdata = await scriptdata.text(); - let text = scriptdata.trim(); - text = text.replace(/[\n\rt ]/gm, " "); - text = text.replace(/ +/gm, " "); - CustomText.setText(text.split(" ")); - CustomText.setIsWordRandom(false); - UpdateConfig.setMode("custom", true); - UpdateConfig.setDifficulty("normal", true); - if (challenge.parameters[1] != null) { - UpdateConfig.setTheme(challenge.parameters[1]); - } - if (challenge.parameters[2] != null) { - Funbox.activate(challenge.parameters[2]); - } - } else if (challenge.type === "accuracy") { - UpdateConfig.setTimeConfig(0, true); - UpdateConfig.setMode("time", true); - UpdateConfig.setDifficulty("master", true); - } else if (challenge.type === "funbox") { - UpdateConfig.setFunbox(challenge.parameters[0], true); - UpdateConfig.setDifficulty("normal", true); - if (challenge.parameters[1] === "words") { - UpdateConfig.setWordCount(challenge.parameters[2], true); - } else if (challenge.parameters[1] === "time") { - UpdateConfig.setTimeConfig(challenge.parameters[2], true); - } - UpdateConfig.setMode(challenge.parameters[1], true); - if (challenge.parameters[3] !== undefined) { - UpdateConfig.setDifficulty(challenge.parameters[3], true); - } - } else if (challenge.type === "special") { - if (challenge.name === "semimak") { - // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout? - UpdateConfig.setMode("time", true); - UpdateConfig.setTimeConfig(120, true); - UpdateConfig.setLanguage("english_10k", true); - UpdateConfig.setPunctuation(true, true); - UpdateConfig.setStopOnError("word", true); - UpdateConfig.setLayout("semimak", true); - UpdateConfig.setKeymapLayout("overrideSync", true); - UpdateConfig.setKeymapMode("static", true); - } - } - ManualRestart.set(); - TestLogic.restart(false, true); - notitext = challenge.message; - $("#top .config").removeClass("hidden"); - $(".page.pageTest").removeClass("hidden"); - - if (notitext === undefined) { - Notifications.add(`Challenge '${challenge.display}' loaded.`, 0); - } else { - Notifications.add("Challenge loaded. " + notitext, 0); - } - active = challenge; - challengeLoading = false; - } catch (e) { - Notifications.add("Something went wrong: " + e, -1); - } -} - -==> ./monkeytype/src/js/ui.js <== -import Config, * as UpdateConfig from "./config"; -import * as Notifications from "./notifications"; -import * as Caret from "./caret"; -import * as TestLogic from "./test-logic"; -import * as CustomText from "./custom-text"; -import * as CommandlineLists from "./commandline-lists"; -import * as Commandline from "./commandline"; -import * as TestUI from "./test-ui"; -import * as TestConfig from "./test-config"; -import * as SignOutButton from "./sign-out-button"; -import * as TestStats from "./test-stats"; -import * as ManualRestart from "./manual-restart-tracker"; -import * as Settings from "./settings"; -import * as Account from "./account"; -import * as Leaderboards from "./leaderboards"; -import * as Funbox from "./funbox"; -import * as About from "./about-page"; - -export let pageTransition = true; -let activePage = "pageLoading"; - -export function getActivePage() { - return activePage; -} - -export function setActivePage(active) { - activePage = active; -} - -export function setPageTransition(val) { - pageTransition = val; -} - -export function updateKeytips() { - if (Config.swapEscAndTab) { - $(".pageSettings .tip").html(` - tip: You can also change all these settings quickly using the - command line ( - tab - )`); - $("#bottom .keyTips").html(` - esc - restart test
- tab - command line`); - } else { - $(".pageSettings .tip").html(` - tip: You can also change all these settings quickly using the - command line ( - esc - )`); - $("#bottom .keyTips").html(` - tab - restart test
- esc or ctrl/cmd+shift+p - command line`); - } -} - -export function swapElements( - el1, - el2, - totalDuration, - callback = function () { - return; - }, - middleCallback = function () { - return; - } -) { - if ( - (el1.hasClass("hidden") && !el2.hasClass("hidden")) || - (!el1.hasClass("hidden") && el2.hasClass("hidden")) - ) { - //one of them is hidden and the other is visible - if (el1.hasClass("hidden")) { - callback(); - return false; - } - $(el1) - .removeClass("hidden") - .css("opacity", 1) - .animate( - { - opacity: 0, - }, - totalDuration / 2, - () => { - middleCallback(); - $(el1).addClass("hidden"); - $(el2) - .removeClass("hidden") - .css("opacity", 0) - .animate( - { - opacity: 1, - }, - totalDuration / 2, - () => { - callback(); - } - ); - } - ); - } else if (el1.hasClass("hidden") && el2.hasClass("hidden")) { - //both are hidden, only fade in the second - $(el2) - .removeClass("hidden") - .css("opacity", 0) - .animate( - { - opacity: 1, - }, - totalDuration, - () => { - callback(); - } - ); - } else { - callback(); - } -} - -export function changePage(page, norestart = false) { - if (pageTransition) { - console.log(`change page ${page} stopped`); - return; - } - - if (page == undefined) { - //use window loacation - let pages = { - "/": "test", - "/login": "login", - "/settings": "settings", - "/about": "about", - "/account": "account", - }; - let path = pages[window.location.pathname]; - if (!path) { - path = "test"; - } - page = path; - } - - console.log(`change page ${page}`); - let activePageElement = $(".page.active"); - let check = activePage + ""; - setTimeout(() => { - if (check === "pageAccount" && page !== "account") { - Account.reset(); - } else if (check === "pageSettings" && page !== "settings") { - Settings.reset(); - } else if (check === "pageAbout" && page !== "about") { - About.reset(); - } - }, 250); - - activePage = undefined; - $(".page").removeClass("active"); - $("#wordsInput").focusout(); - if (page == "test" || page == "") { - setPageTransition(true); - swapElements( - activePageElement, - $(".page.pageTest"), - 250, - () => { - setPageTransition(false); - TestUI.focusWords(); - $(".page.pageTest").addClass("active"); - activePage = "pageTest"; - history.pushState("/", null, "/"); - }, - () => { - TestConfig.show(); - } - ); - SignOutButton.hide(); - // restartCount = 0; - // incompleteTestSeconds = 0; - TestStats.resetIncomplete(); - ManualRestart.set(); - if (!norestart) TestLogic.restart(); - Funbox.activate(Config.funbox); - } else if (page == "about") { - setPageTransition(true); - TestLogic.restart(); - swapElements(activePageElement, $(".page.pageAbout"), 250, () => { - setPageTransition(false); - history.pushState("about", null, "about"); - $(".page.pageAbout").addClass("active"); - activePage = "pageAbout"; - }); - About.fill(); - Funbox.activate("none"); - TestConfig.hide(); - SignOutButton.hide(); - } else if (page == "settings") { - setPageTransition(true); - TestLogic.restart(); - swapElements(activePageElement, $(".page.pageSettings"), 250, () => { - setPageTransition(false); - history.pushState("settings", null, "settings"); - $(".page.pageSettings").addClass("active"); - activePage = "pageSettings"; - }); - Funbox.activate("none"); - Settings.fillSettingsPage().then(() => { - Settings.update(); - }); - // Settings.update(); - TestConfig.hide(); - SignOutButton.hide(); - } else if (page == "account") { - if (!firebase.auth().currentUser) { - console.log( - `current user is ${firebase.auth().currentUser}, going back to login` - ); - changePage("login"); - } else { - setPageTransition(true); - TestLogic.restart(); - swapElements(activePageElement, $(".page.pageAccount"), 250, () => { - setPageTransition(false); - history.pushState("account", null, "account"); - $(".page.pageAccount").addClass("active"); - activePage = "pageAccount"; - }); - Funbox.activate("none"); - Account.update(); - TestConfig.hide(); - } - } else if (page == "login") { - if (firebase.auth().currentUser != null) { - changePage("account"); - } else { - setPageTransition(true); - TestLogic.restart(); - swapElements(activePageElement, $(".page.pageLogin"), 250, () => { - setPageTransition(false); - history.pushState("login", null, "login"); - $(".page.pageLogin").addClass("active"); - activePage = "pageLogin"; - }); - Funbox.activate("none"); - TestConfig.hide(); - SignOutButton.hide(); - } - } -} - -//checking if the project is the development site -/* -if (firebase.app().options.projectId === "monkey-type-dev-67af4") { - $("#top .logo .bottom").text("monkey-dev"); - $("head title").text("Monkey Dev"); - $("body").append( - `
DEV
DEV
` - ); -} -*/ - -if (window.location.hostname === "localhost") { - window.onerror = function (error) { - Notifications.add(error, -1); - }; - $("#top .logo .top").text("localhost"); - $("head title").text($("head title").text() + " (localhost)"); - //firebase.functions().useFunctionsEmulator("http://localhost:5001"); - $("body").append( - `
local
local
` - ); - $(".pageSettings .discordIntegration .buttons a").attr( - "href", - "https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fverify&response_type=token&scope=identify" - ); -} - -//stop space scrolling -window.addEventListener("keydown", function (e) { - if (e.keyCode == 32 && e.target == document.body) { - e.preventDefault(); - } -}); - -$(document).on("click", "#bottom .leftright .right .current-theme", (e) => { - if (e.shiftKey) { - UpdateConfig.toggleCustomTheme(); - } else { - // if (Config.customTheme) { - // toggleCustomTheme(); - // } - CommandlineLists.pushCurrent(CommandlineLists.themeCommands); - Commandline.show(); - } -}); - -$(document.body).on("click", ".pageAbout .aboutEnableAds", () => { - CommandlineLists.pushCurrent(CommandlineLists.commandsEnableAds); - Commandline.show(); -}); - -window.addEventListener("beforeunload", (event) => { - // Cancel the event as stated by the standard. - if ( - (Config.mode === "words" && Config.words < 1000) || - (Config.mode === "time" && Config.time < 3600) || - Config.mode === "quote" || - (Config.mode === "custom" && - CustomText.isWordRandom && - CustomText.word < 1000) || - (Config.mode === "custom" && - CustomText.isTimeRandom && - CustomText.time < 1000) || - (Config.mode === "custom" && - !CustomText.isWordRandom && - CustomText.text.length < 1000) - ) { - //ignore - } else { - if (TestLogic.active) { - event.preventDefault(); - // Chrome requires returnValue to be set. - event.returnValue = ""; - } - } -}); - -$(window).resize(() => { - Caret.updatePosition(); -}); - -$(document).on("click", "#top .logo", (e) => { - changePage("test"); -}); - -$(document).on("click", "#top #menu .icon-button", (e) => { - if ($(e.currentTarget).hasClass("leaderboards")) { - Leaderboards.show(); - } else { - const href = $(e.currentTarget).attr("href"); - ManualRestart.set(); - changePage(href.replace("/", "")); - } - return false; -}); - -==> ./monkeytype/src/js/axios-instance.js <== -import axios from "axios"; - -let apiPath = ""; - -let baseURL; -if (window.location.hostname === "localhost") { - baseURL = "http://localhost:5005" + apiPath; -} else { - baseURL = "https://api.monkeytype.com" + apiPath; -} - -const axiosInstance = axios.create({ - baseURL: baseURL, - timeout: 10000, -}); - -// Request interceptor for API calls -axiosInstance.interceptors.request.use( - async (config) => { - let idToken; - if (firebase.auth().currentUser != null) { - idToken = await firebase.auth().currentUser.getIdToken(); - } else { - idToken = null; - } - if (idToken) { - config.headers = { - Authorization: `Bearer ${idToken}`, - Accept: "application/json", - "Content-Type": "application/json", - }; - } else { - config.headers = { - Accept: "application/json", - "Content-Type": "application/json", - }; - } - return config; - }, - (error) => { - Promise.reject(error); - } -); - -axiosInstance.interceptors.response.use( - (response) => response, - (error) => { - // whatever you want to do with the error - // console.log('interctepted'); - // if(error.response.data.message){ - // Notifications.add(`${error.response.data.message}`); - // }else{ - // Notifications.add(`${error.response.status} ${error.response.statusText}`); - // } - // return error.response; - throw error; - } -); - -export default axiosInstance; - -==> ./monkeytype/src/js/commandline.js <== -import * as Leaderboards from "./leaderboards"; -import * as ThemeController from "./theme-controller"; -import Config, * as UpdateConfig from "./config"; -import * as Focus from "./focus"; -import * as CommandlineLists from "./commandline-lists"; -import * as TestUI from "./test-ui"; -import * as PractiseWords from "./practise-words"; -import * as SimplePopups from "./simple-popups"; -import * as CustomWordAmountPopup from "./custom-word-amount-popup"; -import * as CustomTestDurationPopup from "./custom-test-duration-popup"; -import * as CustomTextPopup from "./custom-text-popup"; -import * as QuoteSearchPopupWrapper from "./quote-search-popup"; - -let commandLineMouseMode = false; - -function showInput(command, placeholder, defaultValue = "") { - $("#commandLineWrapper").removeClass("hidden"); - $("#commandLine").addClass("hidden"); - $("#commandInput").removeClass("hidden"); - $("#commandInput input").attr("placeholder", placeholder); - $("#commandInput input").val(defaultValue); - $("#commandInput input").focus(); - $("#commandInput input").attr("command", ""); - $("#commandInput input").attr("command", command); - if (defaultValue != "") { - $("#commandInput input").select(); - } -} - -export function isSingleListCommandLineActive() { - return $("#commandLine").hasClass("allCommands"); -} - -function showFound() { - $("#commandLine .suggestions").empty(); - let commandsHTML = ""; - let list = CommandlineLists.current[CommandlineLists.current.length - 1]; - $.each(list.list, (index, obj) => { - if (obj.found && (obj.available !== undefined ? obj.available() : true)) { - let icon = obj.icon ?? "fa-chevron-right"; - let faIcon = /^fa-/g.test(icon); - if (!faIcon) { - icon = `
${icon}
`; - } else { - icon = ``; - } - if (list.configKey) { - if ( - (obj.configValueMode && - obj.configValueMode === "include" && - Config[list.configKey].includes(obj.configValue)) || - Config[list.configKey] === obj.configValue - ) { - icon = ``; - } else { - icon = ``; - } - } - let iconHTML = `
${icon}
`; - if (obj.noIcon && !isSingleListCommandLineActive()) { - iconHTML = ""; - } - commandsHTML += `
${iconHTML}
${obj.display}
`; - } - }); - $("#commandLine .suggestions").html(commandsHTML); - if ($("#commandLine .suggestions .entry").length == 0) { - $("#commandLine .separator").css({ height: 0, margin: 0 }); - } else { - $("#commandLine .separator").css({ - height: "1px", - "margin-bottom": ".5rem", - }); - } - let entries = $("#commandLine .suggestions .entry"); - if (entries.length > 0) { - $(entries[0]).addClass("activeKeyboard"); - try { - $.each(list.list, (index, obj) => { - if (obj.found) { - if ( - (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && - !ThemeController.randomTheme - ) - ThemeController.clearPreview(); - if (!/font/gi.test(obj.id)) - UpdateConfig.previewFontFamily(Config.fontFamily); - obj.hover(); - return false; - } - }); - } catch (e) {} - } - $("#commandLine .listTitle").remove(); -} - -function updateSuggested() { - let inputVal = $("#commandLine input") - .val() - .toLowerCase() - .split(" ") - .filter((s, i) => s || i == 0); //remove empty entries after first - let list = CommandlineLists.current[CommandlineLists.current.length - 1]; - if ( - inputVal[0] === "" && - Config.singleListCommandLine === "on" && - CommandlineLists.current.length === 1 - ) { - $.each(list.list, (index, obj) => { - obj.found = false; - }); - showFound(); - return; - } - //ignore the preceeding ">"s in the command line input - if (inputVal[0] && inputVal[0][0] == ">") - inputVal[0] = inputVal[0].replace(/^>+/, ""); - if (inputVal[0] == "" && inputVal.length == 1) { - $.each(list.list, (index, obj) => { - if (obj.visible !== false) obj.found = true; - }); - } else { - $.each(list.list, (index, obj) => { - let foundcount = 0; - $.each(inputVal, (index2, obj2) => { - if (obj2 == "") return; - let escaped = obj2.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - let re = new RegExp("\\b" + escaped, "g"); - let res = obj.display.toLowerCase().match(re); - let res2 = - obj.alias !== undefined ? obj.alias.toLowerCase().match(re) : null; - if ( - (res != null && res.length > 0) || - (res2 != null && res2.length > 0) - ) { - foundcount++; - } else { - foundcount--; - } - }); - if (foundcount > inputVal.length - 1) { - obj.found = true; - } else { - obj.found = false; - } - }); - } - showFound(); -} - -export let show = () => { - if (!$(".page.pageLoading").hasClass("hidden")) return; - Focus.set(false); - $("#commandLine").removeClass("hidden"); - $("#commandInput").addClass("hidden"); - if ($("#commandLineWrapper").hasClass("hidden")) { - $("#commandLineWrapper") - .stop(true, true) - .css("opacity", 0) - .removeClass("hidden") - .animate( - { - opacity: 1, - }, - 100 - ); - } - $("#commandLine input").val(""); - updateSuggested(); - $("#commandLine input").focus(); -}; - -function hide() { - UpdateConfig.previewFontFamily(Config.fontFamily); - // applyCustomThemeColors(); - if (!ThemeController.randomTheme) { - ThemeController.clearPreview(); - } - $("#commandLineWrapper") - .stop(true, true) - .css("opacity", 1) - .animate( - { - opacity: 0, - }, - 100, - () => { - $("#commandLineWrapper").addClass("hidden"); - $("#commandLine").removeClass("allCommands"); - TestUI.focusWords(); - } - ); - TestUI.focusWords(); -} - -function trigger(command) { - let subgroup = false; - let input = false; - let list = CommandlineLists.current[CommandlineLists.current.length - 1]; - let sticky = false; - $.each(list.list, (i, obj) => { - if (obj.id == command) { - if (obj.input) { - input = true; - let escaped = obj.display.split("")[1] ?? obj.display; - showInput(obj.id, escaped, obj.defaultValue); - } else if (obj.subgroup) { - subgroup = true; - if (obj.beforeSubgroup) { - obj.beforeSubgroup(); - } - CommandlineLists.current.push(obj.subgroup); - show(); - } else { - obj.exec(); - if (obj.sticky === true) { - sticky = true; - } - } - } - }); - if (!subgroup && !input && !sticky) { - try { - firebase.analytics().logEvent("usedCommandLine", { - command: command, - }); - } catch (e) { - console.log("Analytics unavailable"); - } - hide(); - } -} - -function addChildCommands( - unifiedCommands, - commandItem, - parentCommandDisplay = "", - parentCommand = "" -) { - let commandItemDisplay = commandItem.display.replace(/\s?\.\.\.$/g, ""); - let icon = ``; - if ( - commandItem.configValue !== undefined && - Config[parentCommand.configKey] === commandItem.configValue - ) { - icon = ``; - } - if (commandItem.noIcon) { - icon = ""; - } - - if (parentCommandDisplay) - commandItemDisplay = - parentCommandDisplay + " > " + icon + commandItemDisplay; - if (commandItem.subgroup) { - if (commandItem.beforeSubgroup) commandItem.beforeSubgroup(); - try { - commandItem.subgroup.list.forEach((cmd) => { - commandItem.configKey = commandItem.subgroup.configKey; - addChildCommands(unifiedCommands, cmd, commandItemDisplay, commandItem); - }); - // commandItem.exec(); - // const currentCommandsIndex = CommandlineLists.current.length - 1; - // CommandlineLists.current[currentCommandsIndex].list.forEach((cmd) => { - // if (cmd.alias === undefined) cmd.alias = commandItem.alias; - // addChildCommands(unifiedCommands, cmd, commandItemDisplay); - // }); - // CommandlineLists.current.pop(); - } catch (e) {} - } else { - let tempCommandItem = { ...commandItem }; - tempCommandItem.icon = parentCommand.icon; - if (parentCommandDisplay) tempCommandItem.display = commandItemDisplay; - unifiedCommands.push(tempCommandItem); - } -} - -function generateSingleListOfCommands() { - const allCommands = []; - const oldShowCommandLine = show; - show = () => {}; - CommandlineLists.defaultCommands.list.forEach((c) => - addChildCommands(allCommands, c) - ); - show = oldShowCommandLine; - return { - title: "All Commands", - list: allCommands, - }; -} - -function useSingleListCommandLine(sshow = true) { - let allCommands = generateSingleListOfCommands(); - // if (Config.singleListCommandLine == "manual") { - // CommandlineLists.pushCurrent(allCommands); - // } else if (Config.singleListCommandLine == "on") { - CommandlineLists.setCurrent([allCommands]); - // } - if (Config.singleListCommandLine != "off") - $("#commandLine").addClass("allCommands"); - if (sshow) show(); -} - -function restoreOldCommandLine(sshow = true) { - if (isSingleListCommandLineActive()) { - $("#commandLine").removeClass("allCommands"); - CommandlineLists.setCurrent( - CommandlineLists.current.filter((l) => l.title != "All Commands") - ); - if (CommandlineLists.current.length < 1) - CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); - } - if (sshow) show(); -} - -$("#commandLine input").keyup((e) => { - commandLineMouseMode = false; - $("#commandLineWrapper #commandLine .suggestions .entry").removeClass( - "activeMouse" - ); - if ( - e.key === "ArrowUp" || - e.key === "ArrowDown" || - e.key === "Enter" || - e.key === "Tab" || - e.code == "AltLeft" || - (e.key.length > 1 && e.key !== "Backspace" && e.key !== "Delete") - ) - return; - updateSuggested(); -}); - -$(document).ready((e) => { - $(document).keydown((event) => { - // opens command line if escape, ctrl/cmd + shift + p, or tab is pressed if the setting swapEscAndTab is enabled - if ( - event.key === "Escape" || - (event.key && - event.key.toLowerCase() === "p" && - (event.metaKey || event.ctrlKey) && - event.shiftKey) || - (event.key === "Tab" && Config.swapEscAndTab) - ) { - event.preventDefault(); - if (!$("#leaderboardsWrapper").hasClass("hidden")) { - //maybe add more condition for closing other dialogs in the future as well - event.preventDefault(); - Leaderboards.hide(); - } else if (!$("#practiseWordsPopupWrapper").hasClass("hidden")) { - event.preventDefault(); - PractiseWords.hide(); - } else if (!$("#simplePopupWrapper").hasClass("hidden")) { - event.preventDefault(); - SimplePopups.hide(); - } else if (!$("#customWordAmountPopupWrapper").hasClass("hidden")) { - event.preventDefault(); - CustomWordAmountPopup.hide(); - } else if (!$("#customTestDurationPopupWrapper").hasClass("hidden")) { - event.preventDefault(); - CustomTestDurationPopup.hide(); - } else if (!$("#customTextPopupWrapper").hasClass("hidden")) { - event.preventDefault(); - CustomTextPopup.hide(); - } else if (!$("#quoteSearchPopupWrapper").hasClass("hidden")) { - event.preventDefault(); - QuoteSearchPopupWrapper.hide(); - } else if (!$("#commandLineWrapper").hasClass("hidden")) { - if (CommandlineLists.current.length > 1) { - CommandlineLists.current.pop(); - $("#commandLine").removeClass("allCommands"); - show(); - } else { - hide(); - } - UpdateConfig.setFontFamily(Config.fontFamily, true); - } else if (event.key === "Tab" || !Config.swapEscAndTab) { - if (Config.singleListCommandLine == "on") { - useSingleListCommandLine(false); - } else { - CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); - } - show(); - } - } - }); -}); - -$("#commandInput input").keydown((e) => { - if (e.key === "Enter") { - //enter - e.preventDefault(); - let command = $("#commandInput input").attr("command"); - let value = $("#commandInput input").val(); - let list = CommandlineLists.current[CommandlineLists.current.length - 1]; - $.each(list.list, (i, obj) => { - if (obj.id == command) { - obj.exec(value); - if (obj.subgroup !== null && obj.subgroup !== undefined) { - //TODO: what is this for? - // subgroup = obj.subgroup; - } - } - }); - try { - firebase.analytics().logEvent("usedCommandLine", { - command: command, - }); - } catch (e) { - console.log("Analytics unavailable"); - } - hide(); - } - return; -}); - -$(document).on("mousemove", () => { - if (!commandLineMouseMode) commandLineMouseMode = true; -}); - -$(document).on( - "mouseenter", - "#commandLineWrapper #commandLine .suggestions .entry", - (e) => { - if (!commandLineMouseMode) return; - $(e.target).addClass("activeMouse"); - } -); - -$(document).on( - "mouseleave", - "#commandLineWrapper #commandLine .suggestions .entry", - (e) => { - if (!commandLineMouseMode) return; - $(e.target).removeClass("activeMouse"); - } -); - -$("#commandLineWrapper #commandLine .suggestions").on("mouseover", (e) => { - if (!commandLineMouseMode) return; - // console.log("clearing keyboard active"); - $("#commandLineWrapper #commandLine .suggestions .entry").removeClass( - "activeKeyboard" - ); - let hoverId = $(e.target).attr("command"); - try { - let list = CommandlineLists.current[CommandlineLists.current.length - 1]; - $.each(list.list, (index, obj) => { - if (obj.id == hoverId) { - if ( - (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && - !ThemeController.randomTheme - ) - ThemeController.clearPreview(); - if (!/font/gi.test(obj.id)) - UpdateConfig.previewFontFamily(Config.fontFamily); - obj.hover(); - } - }); - } catch (e) {} -}); - -$(document).on( - "click", - "#commandLineWrapper #commandLine .suggestions .entry", - (e) => { - $(".suggestions .entry").removeClass("activeKeyboard"); - trigger($(e.currentTarget).attr("command")); - } -); - -$("#commandLineWrapper").click((e) => { - if ($(e.target).attr("id") === "commandLineWrapper") { - hide(); - UpdateConfig.setFontFamily(Config.fontFamily, true); - // if (Config.customTheme === true) { - // applyCustomThemeColors(); - // } else { - // setTheme(Config.theme, true); - // } - } -}); - -//might come back to it later -// function shiftCommand(){ -// let activeEntries = $("#commandLineWrapper #commandLine .suggestions .entry.activeKeyboard, #commandLineWrapper #commandLine .suggestions .entry.activeMouse"); -// activeEntries.each((index, activeEntry) => { -// let commandId = activeEntry.getAttribute('command'); -// let foundCommand = null; -// CommandlineLists.defaultCommands.list.forEach(command => { -// if(foundCommand === null && command.id === commandId){ -// foundCommand = command; -// } -// }) -// if(foundCommand.shift){ -// $(activeEntry).find('div').text(foundCommand.shift.display); -// } -// }) -// } - -// let shiftedCommands = false; -// $(document).keydown((e) => { -// if (e.key === "Shift") { -// if(shiftedCommands === false){ -// shiftedCommands = true; -// shiftCommand(); -// } - -// } -// }); - -// $(document).keyup((e) => { -// if (e.key === "Shift") { -// shiftedCommands = false; -// } -// }); - -$(document).keydown((e) => { - // if (isPreviewingTheme) { - // console.log("applying theme"); - // applyCustomThemeColors(); - // previewTheme(Config.theme, false); - // } - if (!$("#commandLineWrapper").hasClass("hidden")) { - $("#commandLine input").focus(); - if (e.key == ">" && Config.singleListCommandLine == "manual") { - if (!isSingleListCommandLineActive()) { - useSingleListCommandLine(false); - return; - } else if ($("#commandLine input").val() == ">") { - //so that it will ignore succeeding ">" when input is already ">" - e.preventDefault(); - return; - } - } - - if (e.key === "Backspace" || e.key === "Delete") { - setTimeout(() => { - let inputVal = $("#commandLine input").val(); - if ( - Config.singleListCommandLine == "manual" && - isSingleListCommandLineActive() && - inputVal[0] !== ">" - ) { - restoreOldCommandLine(false); - } - }, 1); - } - - if (e.key === "Enter") { - //enter - e.preventDefault(); - let command = $(".suggestions .entry.activeKeyboard").attr("command"); - trigger(command); - return; - } - if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Tab") { - e.preventDefault(); - $("#commandLineWrapper #commandLine .suggestions .entry").unbind( - "mouseenter mouseleave" - ); - let entries = $(".suggestions .entry"); - let activenum = -1; - let hoverId; - $.each(entries, (index, obj) => { - if ($(obj).hasClass("activeKeyboard")) activenum = index; - }); - if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { - entries.removeClass("activeKeyboard"); - if (activenum == 0) { - $(entries[entries.length - 1]).addClass("activeKeyboard"); - hoverId = $(entries[entries.length - 1]).attr("command"); - } else { - $(entries[--activenum]).addClass("activeKeyboard"); - hoverId = $(entries[activenum]).attr("command"); - } - } - if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) { - entries.removeClass("activeKeyboard"); - if (activenum + 1 == entries.length) { - $(entries[0]).addClass("activeKeyboard"); - hoverId = $(entries[0]).attr("command"); - } else { - $(entries[++activenum]).addClass("activeKeyboard"); - hoverId = $(entries[activenum]).attr("command"); - } - } - try { - let scroll = - Math.abs( - $(".suggestions").offset().top - - $(".entry.activeKeyboard").offset().top - - $(".suggestions").scrollTop() - ) - - $(".suggestions").outerHeight() / 2 + - $($(".entry")[0]).outerHeight(); - $(".suggestions").scrollTop(scroll); - } catch (e) { - console.log("could not scroll suggestions: " + e.message); - } - // console.log(`scrolling to ${scroll}`); - try { - let list = - CommandlineLists.current[CommandlineLists.current.length - 1]; - $.each(list.list, (index, obj) => { - if (obj.id == hoverId) { - if ( - (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && - !ThemeController.randomTheme - ) - ThemeController.clearPreview(); - if (!/font/gi.test(obj.id)) - UpdateConfig.previewFontFamily(Config.fontFamily); - obj.hover(); - } - }); - } catch (e) {} - - return false; - } - } -}); - -$(document).on("click", "#commandLineMobileButton", () => { - CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); - show(); -}); - -==> ./monkeytype/src/js/test/poetry.js <== -const bannedChars = ["—", "_", " "]; -const maxWords = 100; -const apiURL = "https://poetrydb.org/random"; - -export class Poem { - constructor(title, author, words) { - this.title = title; - this.author = author; - this.words = words; - - this.cleanUpText(); - } - - cleanUpText() { - var count = 0; - var scrubbedWords = []; - for (var i = 0; i < this.words.length; i++) { - let scrubbed = ""; - for (var j = 0; j < this.words[i].length; j++) { - if (!bannedChars.includes(this.words[i][j])) - scrubbed += this.words[i][j]; - } - - if (scrubbed == "") continue; - - scrubbedWords.push(scrubbed); - count++; - - if (count == maxWords) break; - } - - this.words = scrubbedWords; - } -} - -export async function getPoem() { - return new Promise((res, rej) => { - console.log("Getting poem"); - var poemReq = new XMLHttpRequest(); - poemReq.onload = () => { - if (poemReq.readyState == 4) { - if (poemReq.status == 200) { - let poemObj = JSON.parse(poemReq.responseText)[0]; - let words = []; - poemObj.lines.forEach((line) => { - line.split(" ").forEach((word) => { - words.push(word); - }); - }); - - let poem = new Poem(poemObj.title, poemObj.author, words); - res(poem); - } else { - rej(poemReq.status); - } - } - }; - poemReq.open("GET", apiURL); - poemReq.send(); - }); -} - -==> ./monkeytype/src/js/test/test-timer.js <== -//most of the code is thanks to -//https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript - -import Config, * as UpdateConfig from "./config"; -import * as CustomText from "./custom-text"; -import * as TimerProgress from "./timer-progress"; -import * as LiveWpm from "./live-wpm"; -import * as TestStats from "./test-stats"; -import * as Monkey from "./monkey"; -import * as Misc from "./misc"; -import * as Notifications from "./notifications"; -import * as TestLogic from "./test-logic"; -import * as Caret from "./caret"; - -export let slowTimer = false; -let slowTimerCount = 0; -export let time = 0; -let timer = null; -const interval = 1000; -let expected = 0; - -function setSlowTimer() { - if (slowTimer) return; - slowTimer = true; - console.error("Slow timer, disabling animations"); - // Notifications.add("Slow timer detected", -1, 5); -} - -function clearSlowTimer() { - slowTimer = false; - slowTimerCount = 0; -} - -let timerDebug = false; -export function enableTimerDebug() { - timerDebug = true; -} - -export function clear() { - time = 0; - clearTimeout(timer); -} - -function premid() { - if (timerDebug) console.time("premid"); - document.querySelector("#premidSecondsLeft").innerHTML = Config.time - time; - if (timerDebug) console.timeEnd("premid"); -} - -function updateTimer() { - if (timerDebug) console.time("timer progress update"); - if ( - Config.mode === "time" || - (Config.mode === "custom" && CustomText.isTimeRandom) - ) { - TimerProgress.update(time); - } - if (timerDebug) console.timeEnd("timer progress update"); -} - -function calculateWpmRaw() { - if (timerDebug) console.time("calculate wpm and raw"); - let wpmAndRaw = TestLogic.calculateWpmAndRaw(); - if (timerDebug) console.timeEnd("calculate wpm and raw"); - if (timerDebug) console.time("update live wpm"); - LiveWpm.update(wpmAndRaw.wpm, wpmAndRaw.raw); - if (timerDebug) console.timeEnd("update live wpm"); - if (timerDebug) console.time("push to history"); - TestStats.pushToWpmHistory(wpmAndRaw.wpm); - TestStats.pushToRawHistory(wpmAndRaw.raw); - if (timerDebug) console.timeEnd("push to history"); - return wpmAndRaw; -} - -function monkey(wpmAndRaw) { - if (timerDebug) console.time("update monkey"); - Monkey.updateFastOpacity(wpmAndRaw.wpm); - if (timerDebug) console.timeEnd("update monkey"); -} - -function calculateAcc() { - if (timerDebug) console.time("calculate acc"); - let acc = Misc.roundTo2(TestStats.calculateAccuracy()); - if (timerDebug) console.timeEnd("calculate acc"); - return acc; -} - -function layoutfluid() { - if (timerDebug) console.time("layoutfluid"); - if (Config.funbox === "layoutfluid" && Config.mode === "time") { - const layouts = Config.customLayoutfluid - ? Config.customLayoutfluid.split("#") - : ["qwerty", "dvorak", "colemak"]; - // console.log(Config.customLayoutfluid); - // console.log(layouts); - const numLayouts = layouts.length; - let index = 0; - index = Math.floor(time / (Config.time / numLayouts)); - - if ( - time == Math.floor(Config.time / numLayouts) - 3 || - time == (Config.time / numLayouts) * 2 - 3 - ) { - Notifications.add("3", 0, 1); - } - if ( - time == Math.floor(Config.time / numLayouts) - 2 || - time == Math.floor(Config.time / numLayouts) * 2 - 2 - ) { - Notifications.add("2", 0, 1); - } - if ( - time == Math.floor(Config.time / numLayouts) - 1 || - time == Math.floor(Config.time / numLayouts) * 2 - 1 - ) { - Notifications.add("1", 0, 1); - } - - if (Config.layout !== layouts[index] && layouts[index] !== undefined) { - Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0); - UpdateConfig.setLayout(layouts[index], true); - UpdateConfig.setKeymapLayout(layouts[index], true); - } - } - if (timerDebug) console.timeEnd("layoutfluid"); -} - -function checkIfFailed(wpmAndRaw, acc) { - if (timerDebug) console.time("fail conditions"); - TestStats.pushKeypressesToHistory(); - if ( - Config.minWpm === "custom" && - wpmAndRaw.wpm < parseInt(Config.minWpmCustomSpeed) && - TestLogic.words.currentIndex > 3 - ) { - clearTimeout(timer); - TestLogic.fail("min wpm"); - clearSlowTimer(); - return; - } - if ( - Config.minAcc === "custom" && - acc < parseInt(Config.minAccCustom) && - TestLogic.words.currentIndex > 3 - ) { - clearTimeout(timer); - TestLogic.fail("min accuracy"); - clearSlowTimer(); - return; - } - if (timerDebug) console.timeEnd("fail conditions"); -} - -function checkIfTimeIsUp() { - if (timerDebug) console.time("times up check"); - if ( - Config.mode == "time" || - (Config.mode === "custom" && CustomText.isTimeRandom) - ) { - if ( - (time >= Config.time && Config.time !== 0 && Config.mode === "time") || - (time >= CustomText.time && - CustomText.time !== 0 && - Config.mode === "custom") - ) { - //times up - clearTimeout(timer); - Caret.hide(); - TestLogic.input.pushHistory(); - TestLogic.corrected.pushHistory(); - TestLogic.finish(); - clearSlowTimer(); - return; - } - } - if (timerDebug) console.timeEnd("times up check"); -} - -// --------------------------------------- - -let timerStats = []; - -export function getTimerStats() { - return timerStats; -} - -async function timerStep() { - if (timerDebug) console.time("timer step -----------------------------"); - time++; - premid(); - updateTimer(); - let wpmAndRaw = calculateWpmRaw(); - let acc = calculateAcc(); - monkey(wpmAndRaw); - layoutfluid(); - checkIfFailed(wpmAndRaw, acc); - checkIfTimeIsUp(); - if (timerDebug) console.timeEnd("timer step -----------------------------"); -} - -export async function start() { - clearSlowTimer(); - timerStats = []; - expected = TestStats.start + interval; - (function loop() { - const delay = expected - performance.now(); - timerStats.push({ - dateNow: Date.now(), - now: performance.now(), - expected: expected, - nextDelay: delay, - }); - if ( - (Config.mode === "time" && Config.time < 130 && Config.time > 0) || - (Config.mode === "words" && Config.words < 250 && Config.words > 0) - ) { - if (delay < interval / 2) { - //slow timer - setSlowTimer(); - } - if (delay < interval / 10) { - slowTimerCount++; - if (slowTimerCount > 5) { - //slow timer - Notifications.add( - "Stopping the test due to bad performance. This would cause test calculations to be incorrect. If this happens a lot, please report this.", - -1 - ); - TestLogic.fail("slow timer"); - } - } - } - timer = setTimeout(function () { - // time++; - - if (!TestLogic.active) { - clearTimeout(timer); - clearSlowTimer(); - return; - } - - timerStep(); - - expected += interval; - loop(); - }, delay); - })(); -} - -==> ./monkeytype/src/js/test/caps-warning.js <== -import Config from "./config"; - -function show() { - if ($("#capsWarning").hasClass("hidden")) { - $("#capsWarning").removeClass("hidden"); - } -} - -function hide() { - if (!$("#capsWarning").hasClass("hidden")) { - $("#capsWarning").addClass("hidden"); - } -} - -$(document).keydown(function (event) { - try { - if ( - Config.capsLockWarning && - event.originalEvent.getModifierState("CapsLock") - ) { - show(); - } else { - hide(); - } - } catch {} -}); - -$(document).keyup(function (event) { - try { - if ( - Config.capsLockWarning && - event.originalEvent.getModifierState("CapsLock") - ) { - show(); - } else { - hide(); - } - } catch {} -}); - -==> ./monkeytype/src/js/test/shift-tracker.js <== -import Config from "./config"; -import Layouts from "./layouts"; - -export let leftState = false; -export let rightState = false; - -let keymapStrings = { - left: null, - right: null, - keymap: null, -}; - -function buildKeymapStrings() { - if (keymapStrings.keymap === Config.keymapLayout) return; - - let layout = Layouts[Config.keymapLayout]?.keys; - - if (!layout) { - keymapStrings = { - left: null, - right: null, - keymap: Config.keymapLayout, - }; - } else { - keymapStrings.left = ( - layout.slice(0, 7).join(" ") + - " " + - layout.slice(13, 19).join(" ") + - " " + - layout.slice(26, 31).join(" ") + - " " + - layout.slice(38, 43).join(" ") - ).replace(/ /g, ""); - keymapStrings.right = ( - layout.slice(6, 13).join(" ") + - " " + - layout.slice(18, 26).join(" ") + - " " + - layout.slice(31, 38).join(" ") + - " " + - layout.slice(42, 48).join(" ") - ).replace(/ /g, ""); - keymapStrings.keymap = Config.keymapLayout; - } -} - -$(document).keydown((e) => { - if (e.code === "ShiftLeft") { - leftState = true; - rightState = false; - } else if (e.code === "ShiftRight") { - leftState = false; - rightState = true; - } -}); - -$(document).keyup((e) => { - if (e.code === "ShiftLeft" || e.code === "ShiftRight") { - leftState = false; - rightState = false; - } -}); - -export function reset() { - leftState = false; - rightState = false; -} - -let leftSideKeys = [ - "KeyQ", - "KeyW", - "KeyE", - "KeyR", - "KeyT", - - "KeyA", - "KeyS", - "KeyD", - "KeyF", - "KeyG", - - "KeyZ", - "KeyX", - "KeyC", - "KeyV", - - "Backquote", - "Digit1", - "Digit2", - "Digit3", - "Digit4", - "Digit5", -]; - -let rightSideKeys = [ - "KeyU", - "KeyI", - "KeyO", - "KeyP", - - "KeyH", - "KeyJ", - "KeyK", - "KeyL", - - "KeyN", - "KeyM", - - "Digit7", - "Digit8", - "Digit9", - "Digit0", - - "Backslash", - "BracketLeft", - "BracketRight", - "Semicolon", - "Quote", - "Comma", - "Period", - "Slash", -]; - -export function isUsingOppositeShift(event) { - if (!leftState && !rightState) return null; - - if (Config.oppositeShiftMode === "on") { - if ( - !rightSideKeys.includes(event.code) && - !leftSideKeys.includes(event.code) - ) - return null; - - if ( - (leftState && rightSideKeys.includes(event.code)) || - (rightState && leftSideKeys.includes(event.code)) - ) { - return true; - } else { - return false; - } - } else if (Config.oppositeShiftMode === "keymap") { - buildKeymapStrings(); - - if (!keymapStrings.left || !keymapStrings.right) return null; - - if ( - (leftState && keymapStrings.right.includes(event.key)) || - (rightState && keymapStrings.left.includes(event.key)) - ) { - return true; - } else { - return false; - } - } -} - -==> ./monkeytype/src/js/test/keymap.js <== -import Config, * as UpdateConfig from "./config"; -import * as ThemeColors from "./theme-colors"; -import layouts from "./layouts"; -import * as CommandlineLists from "./commandline-lists"; -import * as Commandline from "./commandline"; -import * as TestTimer from "./test-timer"; - -export function highlightKey(currentKey) { - if (Config.mode === "zen") return; - try { - if ($(".active-key") != undefined) { - $(".active-key").removeClass("active-key"); - } - - let highlightKey; - switch (currentKey) { - case "\\": - case "|": - highlightKey = "#KeyBackslash"; - break; - case "}": - case "]": - highlightKey = "#KeyRightBracket"; - break; - case "{": - case "[": - highlightKey = "#KeyLeftBracket"; - break; - case '"': - case "'": - highlightKey = "#KeyQuote"; - break; - case ":": - case ";": - highlightKey = "#KeySemicolon"; - break; - case "<": - case ",": - highlightKey = "#KeyComma"; - break; - case ">": - case ".": - highlightKey = "#KeyPeriod"; - break; - case "?": - case "/": - highlightKey = "#KeySlash"; - break; - case "": - highlightKey = "#KeySpace"; - break; - default: - highlightKey = `#Key${currentKey}`; - } - - $(highlightKey).addClass("active-key"); - if (highlightKey === "#KeySpace") { - $("#KeySpace2").addClass("active-key"); - } - } catch (e) { - console.log("could not update highlighted keymap key: " + e.message); - } -} - -export async function flashKey(key, correct) { - if (key == undefined) return; - switch (key) { - case "\\": - case "|": - key = "#KeyBackslash"; - break; - case "}": - case "]": - key = "#KeyRightBracket"; - break; - case "{": - case "[": - key = "#KeyLeftBracket"; - break; - case '"': - case "'": - key = "#KeyQuote"; - break; - case ":": - case ";": - key = "#KeySemicolon"; - break; - case "<": - case ",": - key = "#KeyComma"; - break; - case ">": - case ".": - key = "#KeyPeriod"; - break; - case "?": - case "/": - key = "#KeySlash"; - break; - case "" || "Space": - key = "#KeySpace"; - break; - default: - key = `#Key${key.toUpperCase()}`; - } - - if (key == "#KeySpace") { - key = ".key-split-space"; - } - - let themecolors = await ThemeColors.get(); - - try { - if (correct || Config.blindMode) { - $(key) - .stop(true, true) - .css({ - color: themecolors.bg, - backgroundColor: themecolors.main, - borderColor: themecolors.main, - }) - .animate( - { - color: themecolors.sub, - backgroundColor: "transparent", - borderColor: themecolors.sub, - }, - TestTimer.slowTimer ? 0 : 500, - "easeOutExpo" - ); - } else { - $(key) - .stop(true, true) - .css({ - color: themecolors.bg, - backgroundColor: themecolors.error, - borderColor: themecolors.error, - }) - .animate( - { - color: themecolors.sub, - backgroundColor: "transparent", - borderColor: themecolors.sub, - }, - TestTimer.slowTimer ? 0 : 500, - "easeOutExpo" - ); - } - } catch (e) {} -} - -export function hide() { - $(".keymap").addClass("hidden"); -} - -export function show() { - $(".keymap").removeClass("hidden"); -} - -export function refreshKeys(layout) { - try { - let lts = layouts[layout]; //layout to show - let layoutString = layout; - if (Config.keymapLayout === "overrideSync") { - if (Config.layout === "default") { - lts = layouts["qwerty"]; - layoutString = "default"; - } else { - lts = layouts[Config.layout]; - layoutString = Config.layout; - } - } - - if (lts.keymapShowTopRow) { - $(".keymap .r1").removeClass("hidden"); - } else { - $(".keymap .r1").addClass("hidden"); - } - - if (Config.keymapStyle === "alice") { - $(".keymap .extraKey").removeClass("hidden"); - } else { - $(".keymap .extraKey").addClass("hidden"); - } - - $($(".keymap .r5 .keymap-key .letter")[0]).text( - layoutString.replace(/_/g, " ") - ); - - if (lts.iso) { - $(".keymap .r4 .keymap-key.first").removeClass("hidden-key"); - } else { - $(".keymap .r4 .keymap-key.first").addClass("hidden-key"); - } - - var toReplace = lts.keys.slice(1, 48); - var count = 0; - - // let repeatB = false; - $(".keymap .keymap-key .letter") - .map(function () { - if (count < toReplace.length) { - var key = toReplace[count].charAt(0); - this.innerHTML = key; - - switch (key) { - case "\\": - case "|": - this.parentElement.id = "KeyBackslash"; - break; - case "}": - case "]": - this.parentElement.id = "KeyRightBracket"; - break; - case "{": - case "[": - this.parentElement.id = "KeyLeftBracket"; - break; - case '"': - case "'": - this.parentElement.id = "KeyQuote"; - break; - case ":": - case ";": - this.parentElement.id = "KeySemicolon"; - break; - case "<": - case ",": - this.parentElement.id = "KeyComma"; - break; - case ">": - case ".": - this.parentElement.id = "KeyPeriod"; - break; - case "?": - case "/": - this.parentElement.id = "KeySlash"; - break; - case "": - this.parentElement.id = "KeySpace"; - break; - default: - this.parentElement.id = `Key${key.toUpperCase()}`; - } - } - - // if (count == 41 && !repeatB) { - // repeatB = true; - // }else{ - // repeatB = false; - // count++; - // } - - count++; - - // } - }) - .get(); - } catch (e) { - console.log( - "something went wrong when changing layout, resettings: " + e.message - ); - UpdateConfig.setKeymapLayout("qwerty", true); - } -} - -$(document).on("click", ".keymap .r5 #KeySpace", (e) => { - CommandlineLists.setCurrent([CommandlineLists.commandsKeymapLayouts]); - Commandline.show(); -}); - -==> ./monkeytype/src/js/test/wordset.js <== -import Config from "./config"; - -let currentWordset = null; -let currentWordGenerator = null; - -class Wordset { - constructor(words) { - this.words = words; - this.length = this.words.length; - } - - randomWord() { - return this.words[Math.floor(Math.random() * this.length)]; - } -} - -const prefixSize = 2; - -class CharDistribution { - constructor() { - this.chars = {}; - this.count = 0; - } - - addChar(char) { - this.count++; - if (char in this.chars) { - this.chars[char]++; - } else { - this.chars[char] = 1; - } - } - - randomChar() { - const randomIndex = Math.floor(Math.random() * this.count); - let runningCount = 0; - for (const [char, charCount] of Object.entries(this.chars)) { - runningCount += charCount; - if (runningCount > randomIndex) { - return char; - } - } - } -} - -class WordGenerator extends Wordset { - constructor(words) { - super(words); - // Can generate an unbounded number of words in theory. - this.length = Infinity; - - this.ngrams = {}; - for (let word of words) { - // Mark the end of each word with a space. - word += " "; - let prefix = ""; - for (const c of word) { - // Add `c` to the distribution of chars that can come after `prefix`. - if (!(prefix in this.ngrams)) { - this.ngrams[prefix] = new CharDistribution(); - } - this.ngrams[prefix].addChar(c); - prefix = (prefix + c).substr(-prefixSize); - } - } - } - - randomWord() { - let word = ""; - for (;;) { - const prefix = word.substr(-prefixSize); - let charDistribution = this.ngrams[prefix]; - if (!charDistribution) { - // This shouldn't happen if this.ngrams is complete. If it does - // somehow, start generating a new word. - word = ""; - continue; - } - // Pick a random char from the distribution that comes after `prefix`. - const nextChar = charDistribution.randomChar(); - if (nextChar == " ") { - // A space marks the end of the word, so stop generating and return. - break; - } - word += nextChar; - } - return word; - } -} - -export function withWords(words) { - if (Config.funbox == "pseudolang") { - if (currentWordGenerator == null || words !== currentWordGenerator.words) { - currentWordGenerator = new WordGenerator(words); - } - return currentWordGenerator; - } else { - if (currentWordset == null || words !== currentWordset.words) { - currentWordset = new Wordset(words); - } - return currentWordset; - } -} - -==> ./monkeytype/src/js/test/live-acc.js <== -import Config from "./config"; -import * as TestLogic from "./test-logic"; - -export function update(acc) { - let number = Math.floor(acc); - if (Config.blindMode) { - number = 100; - } - document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = number + "%"; - document.querySelector("#liveAcc").innerHTML = number + "%"; -} - -export function show() { - if (!Config.showLiveAcc) return; - if (!TestLogic.active) return; - if (Config.timerStyle === "mini") { - // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity); - if (!$("#miniTimerAndLiveWpm .acc").hasClass("hidden")) return; - $("#miniTimerAndLiveWpm .acc") - .removeClass("hidden") - .css("opacity", 0) - .animate( - { - opacity: Config.timerOpacity, - }, - 125 - ); - } else { - // $("#liveWpm").css("opacity", Config.timerOpacity); - if (!$("#liveAcc").hasClass("hidden")) return; - $("#liveAcc").removeClass("hidden").css("opacity", 0).animate( - { - opacity: Config.timerOpacity, - }, - 125 - ); - } -} - -export function hide() { - // $("#liveWpm").css("opacity", 0); - // $("#miniTimerAndLiveWpm .wpm").css("opacity", 0); - $("#liveAcc").animate( - { - opacity: Config.timerOpacity, - }, - 125, - () => { - $("#liveAcc").addClass("hidden"); - } - ); - $("#miniTimerAndLiveWpm .acc").animate( - { - opacity: Config.timerOpacity, - }, - 125, - () => { - $("#miniTimerAndLiveWpm .acc").addClass("hidden"); - } - ); -} - -==> ./monkeytype/src/js/test/weak-spot.js <== -import * as TestStats from "./test-stats"; - -// Changes how quickly it 'learns' scores - very roughly the score for a char -// is based on last perCharCount occurrences. Make it smaller to adjust faster. -const perCharCount = 50; - -// Choose the highest scoring word from this many random words. Higher values -// will choose words with more weak letters on average. -const wordSamples = 20; - -// Score penatly (in milliseconds) for getting a letter wrong. -const incorrectPenalty = 5000; - -let scores = {}; - -class Score { - constructor() { - this.average = 0.0; - this.count = 0; - } - - update(score) { - if (this.count < perCharCount) { - this.count++; - } - const adjustRate = 1.0 / this.count; - // Keep an exponential moving average of the score over time. - this.average = score * adjustRate + this.average * (1 - adjustRate); - } -} - -export function updateScore(char, isCorrect) { - const timings = TestStats.keypressTimings.spacing.array; - if (timings.length == 0) { - return; - } - let score = timings[timings.length - 1]; - if (!isCorrect) { - score += incorrectPenalty; - } - if (!(char in scores)) { - scores[char] = new Score(); - } - scores[char].update(score); -} - -function score(word) { - let total = 0.0; - let numChars = 0; - for (const c of word) { - if (c in scores) { - total += scores[c].average; - numChars++; - } - } - return numChars == 0 ? 0.0 : total / numChars; -} - -export function getWord(wordset) { - let highScore; - let randomWord; - for (let i = 0; i < wordSamples; i++) { - let newWord = wordset.randomWord(); - let newScore = score(newWord); - if (i == 0 || newScore > highScore) { - randomWord = newWord; - highScore = newScore; - } - } - return randomWord; -} - -==> ./monkeytype/src/js/test/tts.js <== -import Config from "./config"; -import * as Misc from "./misc"; - -let voice; - -export async function setLanguage(lang = Config.language) { - if (!voice) return; - let language = await Misc.getLanguage(lang); - let bcp = language.bcp47 ? language.bcp47 : "en-US"; - voice.lang = bcp; -} - -export async function init() { - voice = new SpeechSynthesisUtterance(); - setLanguage(); -} - -export function clear() { - voice = undefined; -} - -export function speak(text) { - if (!voice) init(); - voice.text = text; - window.speechSynthesis.cancel(); - window.speechSynthesis.speak(voice); -} - -==> ./monkeytype/src/js/test/test-logic.js <== -import * as TestUI from "./test-ui"; -import * as ManualRestart from "./manual-restart-tracker"; -import Config, * as UpdateConfig from "./config"; -import * as Misc from "./misc"; -import * as Notifications from "./notifications"; -import * as CustomText from "./custom-text"; -import * as TestStats from "./test-stats"; -import * as PractiseWords from "./practise-words"; -import * as ShiftTracker from "./shift-tracker"; -import * as Focus from "./focus"; -import * as Funbox from "./funbox"; -import * as Keymap from "./keymap"; -import * as ThemeController from "./theme-controller"; -import * as PaceCaret from "./pace-caret"; -import * as Caret from "./caret"; -import * as LiveWpm from "./live-wpm"; -import * as LiveAcc from "./live-acc"; -import * as LiveBurst from "./live-burst"; -import * as TimerProgress from "./timer-progress"; -import * as UI from "./ui"; -import * as QuoteSearchPopup from "./quote-search-popup"; -import * as QuoteSubmitPopup from "./quote-submit-popup"; -import * as PbCrown from "./pb-crown"; -import * as TestTimer from "./test-timer"; -import * as OutOfFocus from "./out-of-focus"; -import * as AccountButton from "./account-button"; -import * as DB from "./db"; -import * as Replay from "./replay.js"; -import axiosInstance from "./axios-instance"; -import * as MonkeyPower from "./monkey-power"; -import * as Poetry from "./poetry.js"; -import * as Wikipedia from "./wikipedia.js"; -import * as TodayTracker from "./today-tracker"; -import * as WeakSpot from "./weak-spot"; -import * as Wordset from "./wordset"; -import * as ChallengeContoller from "./challenge-controller"; -import * as RateQuotePopup from "./rate-quote-popup"; -import * as BritishEnglish from "./british-english"; -import * as LazyMode from "./lazy-mode"; -import * as Result from "./result"; - -const objecthash = require("object-hash"); - -export let glarsesMode = false; - -let failReason = ""; - -export function toggleGlarses() { - glarsesMode = true; - console.log( - "Glarses Mode On - test result will be hidden. You can check the stats in the console (here)" - ); - console.log("To disable Glarses Mode refresh the page."); -} - -export let notSignedInLastResult = null; - -export function clearNotSignedInResult() { - notSignedInLastResult = null; -} - -export function setNotSignedInUid(uid) { - notSignedInLastResult.uid = uid; - delete notSignedInLastResult.hash; - notSignedInLastResult.hash = objecthash(notSignedInLastResult); -} - -class Words { - constructor() { - this.list = []; - this.length = 0; - this.currentIndex = 0; - } - get(i, raw = false) { - if (i === undefined) { - return this.list; - } else { - if (raw) { - return this.list[i]?.replace(/[.?!":\-,]/g, "")?.toLowerCase(); - } else { - return this.list[i]; - } - } - } - getCurrent() { - return this.list[this.currentIndex]; - } - getLast() { - return this.list[this.list.length - 1]; - } - push(word) { - this.list.push(word); - this.length = this.list.length; - } - reset() { - this.list = []; - this.currentIndex = 0; - this.length = this.list.length; - } - resetCurrentIndex() { - this.currentIndex = 0; - } - decreaseCurrentIndex() { - this.currentIndex--; - } - increaseCurrentIndex() { - this.currentIndex++; - } - clean() { - for (let s of this.list) { - if (/ +/.test(s)) { - let id = this.list.indexOf(s); - let tempList = s.split(" "); - this.list.splice(id, 1); - for (let i = 0; i < tempList.length; i++) { - this.list.splice(id + i, 0, tempList[i]); - } - } - } - } -} - -class Input { - constructor() { - this.current = ""; - this.history = []; - this.length = 0; - } - - reset() { - this.current = ""; - this.history = []; - this.length = 0; - } - - resetHistory() { - this.history = []; - this.length = 0; - } - - setCurrent(val) { - this.current = val; - this.length = this.current.length; - } - - appendCurrent(val) { - this.current += val; - this.length = this.current.length; - } - - resetCurrent() { - this.current = ""; - } - - getCurrent() { - return this.current; - } - - pushHistory() { - this.history.push(this.current); - this.historyLength = this.history.length; - this.resetCurrent(); - } - - popHistory() { - return this.history.pop(); - } - - getHistory(i) { - if (i === undefined) { - return this.history; - } else { - return this.history[i]; - } - } - - getHistoryLast() { - return this.history[this.history.length - 1]; - } -} - -class Corrected { - constructor() { - this.current = ""; - this.history = []; - } - setCurrent(val) { - this.current = val; - } - - appendCurrent(val) { - this.current += val; - } - - resetCurrent() { - this.current = ""; - } - - resetHistory() { - this.history = []; - } - - reset() { - this.resetCurrent(); - this.resetHistory(); - } - - getHistory(i) { - return this.history[i]; - } - - popHistory() { - return this.history.pop(); - } - - pushHistory() { - this.history.push(this.current); - this.current = ""; - } -} - -export let active = false; -export let words = new Words(); -export let input = new Input(); -export let corrected = new Corrected(); -export let currentWordIndex = 0; -export let isRepeated = false; -export let isPaceRepeat = false; -export let lastTestWpm = 0; -export let hasTab = false; -export let randomQuote = null; -export let bailout = false; - -export function setActive(tf) { - active = tf; - if (!tf) MonkeyPower.reset(); -} - -export function setRepeated(tf) { - isRepeated = tf; -} - -export function setPaceRepeat(tf) { - isPaceRepeat = tf; -} - -export function setHasTab(tf) { - hasTab = tf; -} - -export function setBailout(tf) { - bailout = tf; -} - -export function setRandomQuote(rq) { - randomQuote = rq; -} - -let spanishSentenceTracker = ""; -export function punctuateWord(previousWord, currentWord, index, maxindex) { - let word = currentWord; - - let currentLanguage = Config.language.split("_")[0]; - - let lastChar = Misc.getLastChar(previousWord); - - if (Config.funbox === "58008") { - if (currentWord.length > 3) { - if (Math.random() < 0.75) { - let special = ["/", "*", "-", "+"][Math.floor(Math.random() * 4)]; - word = Misc.setCharAt(word, Math.floor(word.length / 2), special); - } - } - } else { - if ( - (index == 0 || lastChar == "." || lastChar == "?" || lastChar == "!") && - currentLanguage != "code" - ) { - //always capitalise the first word or if there was a dot unless using a code alphabet - - word = Misc.capitalizeFirstLetter(word); - - if (currentLanguage == "spanish" || currentLanguage == "catalan") { - let rand = Math.random(); - if (rand > 0.9) { - word = "¿" + word; - spanishSentenceTracker = "?"; - } else if (rand > 0.8) { - word = "¡" + word; - spanishSentenceTracker = "!"; - } - } - } else if ( - (Math.random() < 0.1 && - lastChar != "." && - lastChar != "," && - index != maxindex - 2) || - index == maxindex - 1 - ) { - if (currentLanguage == "spanish" || currentLanguage == "catalan") { - if (spanishSentenceTracker == "?" || spanishSentenceTracker == "!") { - word += spanishSentenceTracker; - spanishSentenceTracker = ""; - } - } else { - let rand = Math.random(); - if (rand <= 0.8) { - word += "."; - } else if (rand > 0.8 && rand < 0.9) { - if (currentLanguage == "french") { - word = "?"; - } else if ( - currentLanguage == "arabic" || - currentLanguage == "persian" || - currentLanguage == "urdu" - ) { - word += "؟"; - } else if (currentLanguage == "greek") { - word += ";"; - } else { - word += "?"; - } - } else { - if (currentLanguage == "french") { - word = "!"; - } else { - word += "!"; - } - } - } - } else if ( - Math.random() < 0.01 && - lastChar != "," && - lastChar != "." && - currentLanguage !== "russian" - ) { - word = `"${word}"`; - } else if ( - Math.random() < 0.011 && - lastChar != "," && - lastChar != "." && - currentLanguage !== "russian" && - currentLanguage !== "ukrainian" - ) { - word = `'${word}'`; - } else if (Math.random() < 0.012 && lastChar != "," && lastChar != ".") { - if (currentLanguage == "code") { - let r = Math.random(); - if (r < 0.25) { - word = `(${word})`; - } else if (r < 0.5) { - word = `{${word}}`; - } else if (r < 0.75) { - word = `[${word}]`; - } else { - word = `<${word}>`; - } - } else { - word = `(${word})`; - } - } else if ( - Math.random() < 0.013 && - lastChar != "," && - lastChar != "." && - lastChar != ";" && - lastChar != "؛" && - lastChar != ":" - ) { - if (currentLanguage == "french") { - word = ":"; - } else if (currentLanguage == "greek") { - word = "·"; - } else { - word += ":"; - } - } else if ( - Math.random() < 0.014 && - lastChar != "," && - lastChar != "." && - previousWord != "-" - ) { - word = "-"; - } else if ( - Math.random() < 0.015 && - lastChar != "," && - lastChar != "." && - lastChar != ";" && - lastChar != "؛" && - lastChar != ":" - ) { - if (currentLanguage == "french") { - word = ";"; - } else if (currentLanguage == "greek") { - word = "·"; - } else if (currentLanguage == "arabic") { - word += "؛"; - } else { - word += ";"; - } - } else if (Math.random() < 0.2 && lastChar != ",") { - if ( - currentLanguage == "arabic" || - currentLanguage == "urdu" || - currentLanguage == "persian" - ) { - word += "،"; - } else { - word += ","; - } - } else if (Math.random() < 0.25 && currentLanguage == "code") { - let specials = ["{", "}", "[", "]", "(", ")", ";", "=", "+", "%", "/"]; - - word = specials[Math.floor(Math.random() * 10)]; - } - } - return word; -} - -export function startTest() { - if (UI.pageTransition) { - return false; - } - if (!Config.dbConfigLoaded) { - UpdateConfig.setChangedBeforeDb(true); - } - try { - if (firebase.auth().currentUser != null) { - firebase.analytics().logEvent("testStarted"); - } else { - firebase.analytics().logEvent("testStartedNoLogin"); - } - } catch (e) { - console.log("Analytics unavailable"); - } - setActive(true); - Replay.startReplayRecording(); - Replay.replayGetWordsList(words.list); - TestStats.resetKeypressTimings(); - TimerProgress.restart(); - TimerProgress.show(); - $("#liveWpm").text("0"); - LiveWpm.show(); - LiveAcc.show(); - LiveBurst.show(); - TimerProgress.update(TestTimer.time); - TestTimer.clear(); - - if (Config.funbox === "memory") { - Funbox.resetMemoryTimer(); - $("#wordsWrapper").addClass("hidden"); - } - - try { - if (Config.paceCaret !== "off" || (Config.repeatedPace && isPaceRepeat)) - PaceCaret.start(); - } catch (e) {} - //use a recursive self-adjusting timer to avoid time drift - TestStats.setStart(performance.now()); - TestTimer.start(); - return true; -} - -export function restart( - withSameWordset = false, - nosave = false, - event, - practiseMissed = false -) { - if (TestUI.testRestarting || TestUI.resultCalculating) { - try { - event.preventDefault(); - } catch {} - return; - } - if (UI.getActivePage() == "pageTest" && !TestUI.resultVisible) { - if (!ManualRestart.get()) { - if (hasTab) { - try { - if (!event.shiftKey) return; - } catch {} - } - try { - if (Config.mode !== "zen") event.preventDefault(); - } catch {} - if ( - !Misc.canQuickRestart( - Config.mode, - Config.words, - Config.time, - CustomText - ) - ) { - let message = "Use your mouse to confirm."; - if (Config.quickTab) - message = "Press shift + tab or use your mouse to confirm."; - Notifications.add("Quick restart disabled. " + message, 0, 3); - return; - } - // }else{ - // return; - // } - } - } - if (active) { - TestStats.pushKeypressesToHistory(); - let testSeconds = TestStats.calculateTestSeconds(performance.now()); - let afkseconds = TestStats.calculateAfkSeconds(testSeconds); - // incompleteTestSeconds += ; - let tt = testSeconds - afkseconds; - if (tt < 0) tt = 0; - console.log( - `increasing incomplete time by ${tt}s (${testSeconds}s - ${afkseconds}s afk)` - ); - TestStats.incrementIncompleteSeconds(tt); - TestStats.incrementRestartCount(); - if (tt > 600) { - Notifications.add( - `Your time typing just increased by ${Misc.roundTo2( - tt / 60 - )} minutes. If you think this is incorrect please contact Miodec and dont refresh the website.`, - -1 - ); - } - // restartCount++; - } - - if (Config.mode == "zen") { - $("#words").empty(); - } - - if ( - PractiseWords.before.mode !== null && - !withSameWordset && - !practiseMissed - ) { - Notifications.add("Reverting to previous settings.", 0); - UpdateConfig.setPunctuation(PractiseWords.before.punctuation); - UpdateConfig.setNumbers(PractiseWords.before.numbers); - UpdateConfig.setMode(PractiseWords.before.mode); - PractiseWords.resetBefore(); - } - - let repeatWithPace = false; - if (TestUI.resultVisible && Config.repeatedPace && withSameWordset) { - repeatWithPace = true; - } - - ManualRestart.reset(); - TestTimer.clear(); - TestStats.restart(); - corrected.reset(); - ShiftTracker.reset(); - Caret.hide(); - setActive(false); - Replay.stopReplayRecording(); - LiveWpm.hide(); - LiveAcc.hide(); - LiveBurst.hide(); - TimerProgress.hide(); - Replay.pauseReplay(); - setBailout(false); - PaceCaret.reset(); - $("#showWordHistoryButton").removeClass("loaded"); - $("#restartTestButton").blur(); - Funbox.resetMemoryTimer(); - RateQuotePopup.clearQuoteStats(); - if (UI.getActivePage() == "pageTest" && window.scrollY > 0) - window.scrollTo({ top: 0, behavior: "smooth" }); - $("#wordsInput").val(" "); - - TestUI.reset(); - - $("#timerNumber").css("opacity", 0); - let el = null; - if (TestUI.resultVisible) { - //results are being displayed - el = $("#result"); - } else { - //words are being displayed - el = $("#typingTest"); - } - if (TestUI.resultVisible) { - if ( - Config.randomTheme !== "off" && - !UI.pageTransition && - !Config.customTheme - ) { - ThemeController.randomizeTheme(); - } - } - TestUI.setResultVisible(false); - UI.setPageTransition(true); - TestUI.setTestRestarting(true); - el.stop(true, true).animate( - { - opacity: 0, - }, - 125, - async () => { - if (UI.getActivePage() == "pageTest") Focus.set(false); - TestUI.focusWords(); - $("#monkey .fast").stop(true, true).css("opacity", 0); - $("#monkey").stop(true, true).css({ animationDuration: "0s" }); - $("#typingTest").css("opacity", 0).removeClass("hidden"); - $("#wordsInput").val(" "); - let shouldQuoteRepeat = false; - if ( - Config.mode === "quote" && - Config.repeatQuotes === "typing" && - failReason !== "" - ) { - shouldQuoteRepeat = true; - } - if (Config.funbox === "arrows") { - UpdateConfig.setPunctuation(false, true); - UpdateConfig.setNumbers(false, true); - } else if (Config.funbox === "58008") { - UpdateConfig.setNumbers(false, true); - } else if (Config.funbox === "specials") { - UpdateConfig.setPunctuation(false, true); - UpdateConfig.setNumbers(false, true); - } else if (Config.funbox === "ascii") { - UpdateConfig.setPunctuation(false, true); - UpdateConfig.setNumbers(false, true); - } - if (!withSameWordset && !shouldQuoteRepeat) { - setRepeated(false); - setPaceRepeat(repeatWithPace); - setHasTab(false); - await init(); - PaceCaret.init(nosave); - } else { - setRepeated(true); - setPaceRepeat(repeatWithPace); - setActive(false); - Replay.stopReplayRecording(); - words.resetCurrentIndex(); - input.reset(); - if (Config.funbox === "plus_one" || Config.funbox === "plus_two") { - Notifications.add( - "Sorry, this funbox won't work with repeated tests.", - 0 - ); - await Funbox.activate("none"); - } else { - await Funbox.activate(); - } - TestUI.showWords(); - PaceCaret.init(); - } - failReason = ""; - if (Config.mode === "quote") { - setRepeated(false); - } - if (Config.keymapMode !== "off") { - Keymap.show(); - } else { - Keymap.hide(); - } - document.querySelector("#miniTimerAndLiveWpm .wpm").innerHTML = "0"; - document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = "100%"; - document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = "0"; - document.querySelector("#liveWpm").innerHTML = "0"; - document.querySelector("#liveAcc").innerHTML = "100%"; - document.querySelector("#liveBurst").innerHTML = "0"; - - if (Config.funbox === "memory") { - Funbox.startMemoryTimer(); - if (Config.keymapMode === "next") { - UpdateConfig.setKeymapMode("react"); - } - } - - let mode2 = Misc.getMode2(); - let fbtext = ""; - if (Config.funbox !== "none") { - fbtext = " " + Config.funbox; - } - $(".pageTest #premidTestMode").text( - `${Config.mode} ${mode2} ${Config.language.replace(/_/g, " ")}${fbtext}` - ); - $(".pageTest #premidSecondsLeft").text(Config.time); - - if (Config.funbox === "layoutfluid") { - UpdateConfig.setLayout( - Config.customLayoutfluid - ? Config.customLayoutfluid.split("#")[0] - : "qwerty", - true - ); - UpdateConfig.setKeymapLayout( - Config.customLayoutfluid - ? Config.customLayoutfluid.split("#")[0] - : "qwerty", - true - ); - Keymap.highlightKey( - words - .getCurrent() - .substring(input.current.length, input.current.length + 1) - .toString() - .toUpperCase() - ); - } - - $("#result").addClass("hidden"); - $("#testModesNotice").removeClass("hidden").css({ - opacity: 1, - }); - // resetPaceCaret(); - $("#typingTest") - .css("opacity", 0) - .removeClass("hidden") - .stop(true, true) - .animate( - { - opacity: 1, - }, - 125, - () => { - TestUI.setTestRestarting(false); - // resetPaceCaret(); - PbCrown.hide(); - TestTimer.clear(); - if ($("#commandLineWrapper").hasClass("hidden")) - TestUI.focusWords(); - // ChartController.result.update(); - TestUI.updateModesNotice(); - UI.setPageTransition(false); - // console.log(TestStats.incompleteSeconds); - // console.log(TestStats.restartCount); - } - ); - } - ); -} - -async function getNextWord(wordset, language, wordsBound) { - let randomWord = wordset.randomWord(); - const previousWord = words.get(words.length - 1, true); - const previousWord2 = words.get(words.length - 2, true); - if (Config.mode === "quote") { - randomWord = randomQuote.textSplit[words.length]; - } else if ( - Config.mode == "custom" && - !CustomText.isWordRandom && - !CustomText.isTimeRandom - ) { - randomWord = CustomText.text[words.length]; - } else if ( - Config.mode == "custom" && - (CustomText.isWordRandom || CustomText.isTimeRandom) && - (wordset.length < 3 || PractiseWords.before.mode !== null) - ) { - randomWord = wordset.randomWord(); - } else { - let regenarationCount = 0; //infinite loop emergency stop button - while ( - regenarationCount < 100 && - (previousWord == randomWord || - previousWord2 == randomWord || - (!Config.punctuation && randomWord == "I")) - ) { - regenarationCount++; - randomWord = wordset.randomWord(); - } - } - - if (randomWord === undefined) { - randomWord = wordset.randomWord(); - } - - if (Config.lazyMode === true && !language.noLazyMode) { - randomWord = LazyMode.replaceAccents(randomWord, language.accents); - } - - randomWord = randomWord.replace(/ +/gm, " "); - randomWord = randomWord.replace(/^ | $/gm, ""); - - if (Config.funbox === "rAnDoMcAsE") { - let randomcaseword = ""; - for (let i = 0; i < randomWord.length; i++) { - if (i % 2 != 0) { - randomcaseword += randomWord[i].toUpperCase(); - } else { - randomcaseword += randomWord[i]; - } - } - randomWord = randomcaseword; - } else if (Config.funbox === "capitals") { - randomWord = Misc.capitalizeFirstLetter(randomWord); - } else if (Config.funbox === "gibberish") { - randomWord = Misc.getGibberish(); - } else if (Config.funbox === "arrows") { - randomWord = Misc.getArrows(); - } else if (Config.funbox === "58008") { - randomWord = Misc.getNumbers(7); - } else if (Config.funbox === "specials") { - randomWord = Misc.getSpecials(); - } else if (Config.funbox === "ascii") { - randomWord = Misc.getASCII(); - } else if (Config.funbox === "weakspot") { - randomWord = WeakSpot.getWord(wordset); - } - - if (Config.punctuation) { - randomWord = punctuateWord( - words.get(words.length - 1), - randomWord, - words.length, - wordsBound - ); - } - if (Config.numbers) { - if (Math.random() < 0.1) { - randomWord = Misc.getNumbers(4); - } - } - - if (Config.britishEnglish && /english/.test(Config.language)) { - randomWord = await BritishEnglish.replace(randomWord); - } - - return randomWord; -} - -export async function init() { - setActive(false); - Replay.stopReplayRecording(); - words.reset(); - TestUI.setCurrentWordElementIndex(0); - // accuracy = { - // correct: 0, - // incorrect: 0, - // }; - - input.resetHistory(); - input.resetCurrent(); - - let language = await Misc.getLanguage(Config.language); - if (language && language.name !== Config.language) { - UpdateConfig.setLanguage("english"); - } - - if (!language) { - UpdateConfig.setLanguage("english"); - language = await Misc.getLanguage(Config.language); - } - - if (Config.lazyMode === true && language.noLazyMode) { - Notifications.add("This language does not support lazy mode.", 0); - UpdateConfig.setLazyMode(false); - } - - let wordsBound = 100; - if (Config.showAllLines) { - if (Config.mode === "quote") { - wordsBound = 100; - } else if (Config.mode === "custom") { - if (CustomText.isWordRandom) { - wordsBound = CustomText.word; - } else if (CustomText.isTimeRandom) { - wordsBound = 100; - } else { - wordsBound = CustomText.text.length; - } - } else if (Config.mode != "time") { - wordsBound = Config.words; - } - } else { - if (Config.mode === "words" && Config.words < wordsBound) { - wordsBound = Config.words; - } - if ( - Config.mode == "custom" && - CustomText.isWordRandom && - CustomText.word < wordsBound - ) { - wordsBound = CustomText.word; - } - if (Config.mode == "custom" && CustomText.isTimeRandom) { - wordsBound = 100; - } - if ( - Config.mode == "custom" && - !CustomText.isWordRandom && - !CustomText.isTimeRandom && - CustomText.text.length < wordsBound - ) { - wordsBound = CustomText.text.length; - } - } - - if ( - (Config.mode === "custom" && - CustomText.isWordRandom && - CustomText.word == 0) || - (Config.mode === "custom" && - CustomText.isTimeRandom && - CustomText.time == 0) - ) { - wordsBound = 100; - } - - if (Config.mode === "words" && Config.words === 0) { - wordsBound = 100; - } - if (Config.funbox === "plus_one") { - wordsBound = 2; - if (Config.mode === "words" && Config.words < wordsBound) { - wordsBound = Config.words; - } - } - if (Config.funbox === "plus_two") { - wordsBound = 3; - if (Config.mode === "words" && Config.words < wordsBound) { - wordsBound = Config.words; - } - } - - if ( - Config.mode == "time" || - Config.mode == "words" || - Config.mode == "custom" - ) { - let wordList = language.words; - if (Config.mode == "custom") { - wordList = CustomText.text; - } - const wordset = Wordset.withWords(wordList); - - if ( - (Config.funbox == "wikipedia" || Config.funbox == "poetry") && - Config.mode != "custom" - ) { - let wordCount = 0; - - // If mode is words, get as many sections as you need until the wordCount is fullfilled - while ( - (Config.mode == "words" && Config.words >= wordCount) || - (Config.mode === "time" && wordCount < 100) - ) { - let section = - Config.funbox == "wikipedia" - ? await Wikipedia.getSection() - : await Poetry.getPoem(); - for (let word of section.words) { - if (wordCount >= Config.words && Config.mode == "words") { - wordCount++; - break; - } - wordCount++; - words.push(word); - } - } - } else { - for (let i = 0; i < wordsBound; i++) { - let randomWord = await getNextWord(wordset, language, wordsBound); - - if (/t/g.test(randomWord)) { - setHasTab(true); - } - - if (/ +/.test(randomWord)) { - let randomList = randomWord.split(" "); - let id = 0; - while (id < randomList.length) { - words.push(randomList[id]); - id++; - - if ( - words.length == wordsBound && - Config.mode == "custom" && - CustomText.isWordRandom - ) { - break; - } - } - if ( - Config.mode == "custom" && - !CustomText.isWordRandom && - !CustomText.isTimeRandom - ) { - // - } else { - i = words.length - 1; - } - } else { - words.push(randomWord); - } - } - } - } else if (Config.mode == "quote") { - // setLanguage(Config.language.replace(/_\d*k$/g, ""), true); - - let quotes = await Misc.getQuotes(Config.language.replace(/_\d*k$/g, "")); - - if (quotes.length === 0) { - TestUI.setTestRestarting(false); - Notifications.add( - `No ${Config.language.replace(/_\d*k$/g, "")} quotes found`, - 0 - ); - if (firebase.auth().currentUser) { - QuoteSubmitPopup.show(false); - } - UpdateConfig.setMode("words"); - restart(); - return; - } - - let rq; - if (Config.quoteLength != -2) { - let quoteLengths = Config.quoteLength; - let groupIndex; - if (quoteLengths.length > 1) { - groupIndex = - quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; - while (quotes.groups[groupIndex].length === 0) { - groupIndex = - quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; - } - } else { - groupIndex = quoteLengths[0]; - if (quotes.groups[groupIndex].length === 0) { - Notifications.add("No quotes found for selected quote length", 0); - TestUI.setTestRestarting(false); - return; - } - } - - rq = - quotes.groups[groupIndex][ - Math.floor(Math.random() * quotes.groups[groupIndex].length) - ]; - if (randomQuote != null && rq.id === randomQuote.id) { - rq = - quotes.groups[groupIndex][ - Math.floor(Math.random() * quotes.groups[groupIndex].length) - ]; - } - } else { - quotes.groups.forEach((group) => { - let filtered = group.filter( - (quote) => quote.id == QuoteSearchPopup.selectedId - ); - if (filtered.length > 0) { - rq = filtered[0]; - } - }); - if (rq == undefined) { - rq = quotes.groups[0][0]; - Notifications.add("Quote Id Does Not Exist", 0); - } - } - rq.text = rq.text.replace(/ +/gm, " "); - rq.text = rq.text.replace(/t/gm, "t"); - rq.text = rq.text.replace(/\\\\n/gm, "\n"); - rq.text = rq.text.replace(/t/gm, "t"); - rq.text = rq.text.replace(/\\n/gm, "\n"); - rq.text = rq.text.replace(/( *(\r\n|\r|\n) *)/g, "\n "); - rq.text = rq.text.replace(/…/g, "..."); - rq.text = rq.text.trim(); - rq.textSplit = rq.text.split(" "); - rq.language = Config.language.replace(/_\d*k$/g, ""); - - setRandomQuote(rq); - - let w = randomQuote.textSplit; - - wordsBound = Math.min(wordsBound, w.length); - - for (let i = 0; i < wordsBound; i++) { - if (/t/g.test(w[i])) { - setHasTab(true); - } - if ( - Config.britishEnglish && - Config.language.replace(/_\d*k$/g, "") === "english" - ) { - w[i] = await BritishEnglish.replace(w[i]); - } - - if (Config.lazyMode === true && !language.noLazyMode) { - w[i] = LazyMode.replaceAccents(w[i], language.accents); - } - - words.push(w[i]); - } - } - //handle right-to-left languages - if (language.leftToRight) { - TestUI.arrangeCharactersLeftToRight(); - } else { - TestUI.arrangeCharactersRightToLeft(); - } - if (language.ligatures) { - $("#words").addClass("withLigatures"); - $("#resultWordsHistory .words").addClass("withLigatures"); - $("#resultReplay .words").addClass("withLigatures"); - } else { - $("#words").removeClass("withLigatures"); - $("#resultWordsHistory .words").removeClass("withLigatures"); - $("#resultReplay .words").removeClass("withLigatures"); - } - // if (Config.mode == "zen") { - // // Creating an empty active word element for zen mode - // $("#words").append('
'); - // $("#words").css("height", "auto"); - // $("#wordsWrapper").css("height", "auto"); - // } else { - if (UI.getActivePage() == "pageTest") { - await Funbox.activate(); - } - TestUI.showWords(); - // } -} - -export function calculateWpmAndRaw() { - let chars = 0; - let correctWordChars = 0; - let spaces = 0; - //check input history - for (let i = 0; i < input.history.length; i++) { - let word = Config.mode == "zen" ? input.getHistory(i) : words.get(i); - if (input.getHistory(i) == word) { - //the word is correct - //+1 for space - correctWordChars += word.length; - if ( - i < input.history.length - 1 && - Misc.getLastChar(input.getHistory(i)) !== "\n" - ) { - spaces++; - } - } - chars += input.getHistory(i).length; - } - if (input.current !== "") { - let word = Config.mode == "zen" ? input.current : words.getCurrent(); - //check whats currently typed - let toAdd = { - correct: 0, - incorrect: 0, - missed: 0, - }; - for (let c = 0; c < word.length; c++) { - if (c < input.current.length) { - //on char that still has a word list pair - if (input.current[c] == word[c]) { - toAdd.correct++; - } else { - toAdd.incorrect++; - } - } else { - //on char that is extra - toAdd.missed++; - } - } - chars += toAdd.correct; - chars += toAdd.incorrect; - chars += toAdd.missed; - if (toAdd.incorrect == 0) { - //word is correct so far, add chars - correctWordChars += toAdd.correct; - } - } - if (Config.funbox === "nospace" || Config.funbox === "arrows") { - spaces = 0; - } - chars += input.current.length; - let testSeconds = TestStats.calculateTestSeconds(performance.now()); - let wpm = Math.round(((correctWordChars + spaces) * (60 / testSeconds)) / 5); - let raw = Math.round(((chars + spaces) * (60 / testSeconds)) / 5); - return { - wpm: wpm, - raw: raw, - }; -} - -export async function addWord() { - let bound = 100; - if (Config.funbox === "wikipedia" || Config.funbox == "poetry") { - if (Config.mode == "time" && words.length - words.currentIndex < 20) { - let section = - Config.funbox == "wikipedia" - ? await Wikipedia.getSection() - : await Poetry.getPoem(); - let wordCount = 0; - for (let word of section.words) { - if (wordCount >= Config.words && Config.mode == "words") { - break; - } - wordCount++; - words.push(word); - TestUI.addWord(word); - } - } else { - return; - } - } - - if (Config.funbox === "plus_one") bound = 1; - if (Config.funbox === "plus_two") bound = 2; - if ( - words.length - input.history.length > bound || - (Config.mode === "words" && - words.length >= Config.words && - Config.words > 0) || - (Config.mode === "custom" && - CustomText.isWordRandom && - words.length >= CustomText.word && - CustomText.word != 0) || - (Config.mode === "custom" && - !CustomText.isWordRandom && - !CustomText.isTimeRandom && - words.length >= CustomText.text.length) || - (Config.mode === "quote" && words.length >= randomQuote.textSplit.length) - ) - return; - const language = - Config.mode !== "custom" - ? await Misc.getCurrentLanguage() - : { - //borrow the direction of the current language - leftToRight: await Misc.getCurrentLanguage().leftToRight, - words: CustomText.text, - }; - const wordset = Wordset.withWords(language.words); - - let randomWord = await getNextWord(wordset, language, bound); - - let split = randomWord.split(" "); - if (split.length > 1) { - split.forEach((word) => { - words.push(word); - TestUI.addWord(word); - }); - } else { - words.push(randomWord); - TestUI.addWord(randomWord); - } -} - -var retrySaving = { - completedEvent: null, - canRetry: false, -}; - -export function retrySavingResult() { - if (!retrySaving.completedEvent) { - Notifications.add( - "Could not retry saving the result as the result no longer exists.", - 0, - -1 - ); - } - if (!retrySaving.canRetry) { - return; - } - - retrySaving.canRetry = false; - $("#retrySavingResultButton").addClass("hidden"); - - AccountButton.loading(true); - - Notifications.add("Retrying to save..."); - - var { completedEvent } = retrySaving; - - axiosInstance - .post("/results/add", { - result: completedEvent, - }) - .then((response) => { - AccountButton.loading(false); - Result.hideCrown(); - - if (response.status !== 200) { - Notifications.add("Result not saved. " + response.data.message, -1); - } else { - completedEvent._id = response.data.insertedId; - if (response.data.isPb) { - completedEvent.isPb = true; - } - - DB.saveLocalResult(completedEvent); - DB.updateLocalStats({ - time: - completedEvent.testDuration + - completedEvent.incompleteTestSeconds - - completedEvent.afkDuration, - started: TestStats.restartCount + 1, - }); - - try { - firebase.analytics().logEvent("testCompleted", completedEvent); - } catch (e) { - console.log("Analytics unavailable"); - } - - if (response.data.isPb) { - //new pb - Result.showCrown(); - Result.updateCrown(); - DB.saveLocalPB( - Config.mode, - completedEvent.mode2, - Config.punctuation, - Config.language, - Config.difficulty, - Config.lazyMode, - completedEvent.wpm, - completedEvent.acc, - completedEvent.rawWpm, - completedEvent.consistency - ); - } - } - - $("#retrySavingResultButton").addClass("hidden"); - Notifications.add("Result saved", 1); - }) - .catch((e) => { - AccountButton.loading(false); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to save result: " + msg, -1); - $("#retrySavingResultButton").removeClass("hidden"); - retrySaving.canRetry = true; - }); -} - -function buildCompletedEvent(difficultyFailed) { - //build completed event object - let completedEvent = { - wpm: undefined, - rawWpm: undefined, - charStats: undefined, - acc: undefined, - mode: Config.mode, - mode2: undefined, - quoteLength: -1, - punctuation: Config.punctuation, - numbers: Config.numbers, - lazyMode: Config.lazyMode, - timestamp: Date.now(), - language: Config.language, - restartCount: TestStats.restartCount, - incompleteTestSeconds: - TestStats.incompleteSeconds < 0 - ? 0 - : Misc.roundTo2(TestStats.incompleteSeconds), - difficulty: Config.difficulty, - blindMode: Config.blindMode, - tags: undefined, - keySpacing: TestStats.keypressTimings.spacing.array, - keyDuration: TestStats.keypressTimings.duration.array, - consistency: undefined, - keyConsistency: undefined, - funbox: Config.funbox, - bailedOut: bailout, - chartData: { - wpm: TestStats.wpmHistory, - raw: undefined, - err: undefined, - }, - customText: undefined, - testDuration: undefined, - afkDuration: undefined, - }; - - // stats - let stats = TestStats.calculateStats(); - if (stats.time % 1 != 0 && Config.mode !== "time") { - TestStats.setLastSecondNotRound(); - } - lastTestWpm = stats.wpm; - completedEvent.wpm = stats.wpm; - completedEvent.rawWpm = stats.wpmRaw; - completedEvent.charStats = [ - stats.correctChars + stats.correctSpaces, - stats.incorrectChars, - stats.extraChars, - stats.missedChars, - ]; - completedEvent.acc = stats.acc; - - // if the last second was not rounded, add another data point to the history - if (TestStats.lastSecondNotRound && !difficultyFailed) { - let wpmAndRaw = calculateWpmAndRaw(); - TestStats.pushToWpmHistory(wpmAndRaw.wpm); - TestStats.pushToRawHistory(wpmAndRaw.raw); - TestStats.pushKeypressesToHistory(); - } - - //consistency - let rawPerSecond = TestStats.keypressPerSecond.map((f) => - Math.round((f.count / 5) * 60) - ); - let stddev = Misc.stdDev(rawPerSecond); - let avg = Misc.mean(rawPerSecond); - let consistency = Misc.roundTo2(Misc.kogasa(stddev / avg)); - let keyconsistencyarray = TestStats.keypressTimings.spacing.array.slice(); - keyconsistencyarray = keyconsistencyarray.splice( - 0, - keyconsistencyarray.length - 1 - ); - let keyConsistency = Misc.roundTo2( - Misc.kogasa( - Misc.stdDev(keyconsistencyarray) / Misc.mean(keyconsistencyarray) - ) - ); - if (isNaN(consistency)) { - consistency = 0; - } - completedEvent.keyConsistency = keyConsistency; - completedEvent.consistency = consistency; - let smoothedraw = Misc.smooth(rawPerSecond, 1); - completedEvent.chartData.raw = smoothedraw; - completedEvent.chartData.unsmoothedRaw = rawPerSecond; - - //smoothed consistency - let stddev2 = Misc.stdDev(smoothedraw); - let avg2 = Misc.mean(smoothedraw); - let smoothConsistency = Misc.roundTo2(Misc.kogasa(stddev2 / avg2)); - completedEvent.smoothConsistency = smoothConsistency; - - //wpm consistency - let stddev3 = Misc.stdDev(completedEvent.chartData.wpm); - let avg3 = Misc.mean(completedEvent.chartData.wpm); - let wpmConsistency = Misc.roundTo2(Misc.kogasa(stddev3 / avg3)); - completedEvent.wpmConsistency = wpmConsistency; - - completedEvent.testDuration = parseFloat(stats.time); - completedEvent.afkDuration = TestStats.calculateAfkSeconds( - completedEvent.testDuration - ); - - completedEvent.chartData.err = []; - for (let i = 0; i < TestStats.keypressPerSecond.length; i++) { - completedEvent.chartData.err.push(TestStats.keypressPerSecond[i].errors); - } - - if (Config.mode === "quote") { - completedEvent.quoteLength = randomQuote.group; - completedEvent.lang = Config.language.replace(/_\d*k$/g, ""); - } - - completedEvent.mode2 = Misc.getMode2(); - - if (Config.mode === "custom") { - completedEvent.customText = {}; - completedEvent.customText.textLen = CustomText.text.length; - completedEvent.customText.isWordRandom = CustomText.isWordRandom; - completedEvent.customText.isTimeRandom = CustomText.isTimeRandom; - completedEvent.customText.word = - CustomText.word !== "" && !isNaN(CustomText.word) - ? CustomText.word - : null; - completedEvent.customText.time = - CustomText.time !== "" && !isNaN(CustomText.time) - ? CustomText.time - : null; - } else { - delete completedEvent.customText; - } - - //tags - let activeTagsIds = []; - try { - DB.getSnapshot().tags.forEach((tag) => { - if (tag.active === true) { - activeTagsIds.push(tag._id); - } - }); - } catch (e) {} - completedEvent.tags = activeTagsIds; - - if (completedEvent.mode != "custom") delete completedEvent.customText; - - return completedEvent; -} - -export async function finish(difficultyFailed = false) { - if (!active) return; - if (Config.mode == "zen" && input.current.length != 0) { - input.pushHistory(); - corrected.pushHistory(); - Replay.replayGetWordsList(input.history); - } - - TestStats.recordKeypressSpacing(); //this is needed in case there is afk time at the end - to make sure test duration makes sense - - TestUI.setResultCalculating(true); - TestUI.setResultVisible(true); - TestStats.setEnd(performance.now()); - setActive(false); - Replay.stopReplayRecording(); - Focus.set(false); - Caret.hide(); - LiveWpm.hide(); - PbCrown.hide(); - LiveAcc.hide(); - LiveBurst.hide(); - TimerProgress.hide(); - OutOfFocus.hide(); - TestTimer.clear(); - Funbox.activate("none", null); - - //need one more calculation for the last word if test auto ended - if (TestStats.burstHistory.length !== input.getHistory().length) { - let burst = TestStats.calculateBurst(); - TestStats.pushBurstToHistory(burst); - } - - //remove afk from zen - if (Config.mode == "zen" || bailout) { - TestStats.removeAfkData(); - } - - const completedEvent = buildCompletedEvent(difficultyFailed); - - //todo check if any fields are undefined - - ///////// completed event ready - - //afk check - let kps = TestStats.keypressPerSecond.slice(-5); - let afkDetected = kps.every((second) => second.afk); - if (bailout) afkDetected = false; - - let tooShort = false; - let dontSave = false; - //fail checks - if (difficultyFailed) { - Notifications.add(`Test failed - ${failReason}`, 0, 1); - dontSave = true; - } else if (afkDetected) { - Notifications.add("Test invalid - AFK detected", 0); - dontSave = true; - } else if (isRepeated) { - Notifications.add("Test invalid - repeated", 0); - dontSave = true; - } else if ( - (Config.mode === "time" && - completedEvent.mode2 < 15 && - completedEvent.mode2 > 0) || - (Config.mode === "time" && - completedEvent.mode2 == 0 && - completedEvent.testDuration < 15) || - (Config.mode === "words" && - completedEvent.mode2 < 10 && - completedEvent.mode2 > 0) || - (Config.mode === "words" && - completedEvent.mode2 == 0 && - completedEvent.testDuration < 15) || - (Config.mode === "custom" && - !CustomText.isWordRandom && - !CustomText.isTimeRandom && - CustomText.text.length < 10) || - (Config.mode === "custom" && - CustomText.isWordRandom && - !CustomText.isTimeRandom && - CustomText.word < 10) || - (Config.mode === "custom" && - !CustomText.isWordRandom && - CustomText.isTimeRandom && - CustomText.time < 15) || - (Config.mode === "zen" && completedEvent.testDuration < 15) - ) { - Notifications.add("Test invalid - too short", 0); - tooShort = true; - dontSave = true; - } else if (completedEvent.wpm < 0 || completedEvent.wpm > 350) { - Notifications.add("Test invalid - wpm", 0); - TestStats.setInvalid(); - dontSave = true; - } else if (completedEvent.acc < 75 || completedEvent.acc > 100) { - Notifications.add("Test invalid - accuracy", 0); - TestStats.setInvalid(); - dontSave = true; - } - - // test is valid - - if (!dontSave) { - TodayTracker.addSeconds( - completedEvent.testDuration + - (TestStats.incompleteSeconds < 0 - ? 0 - : Misc.roundTo2(TestStats.incompleteSeconds)) - - completedEvent.afkDuration - ); - Result.updateTodayTracker(); - } - - if (firebase.auth().currentUser == null) { - $(".pageTest #result #rateQuoteButton").addClass("hidden"); - try { - firebase.analytics().logEvent("testCompletedNoLogin", completedEvent); - } catch (e) { - console.log("Analytics unavailable"); - } - notSignedInLastResult = completedEvent; - dontSave = true; - } - - Result.update( - completedEvent, - difficultyFailed, - failReason, - afkDetected, - isRepeated, - tooShort, - randomQuote, - dontSave - ); - - delete completedEvent.chartData.unsmoothedRaw; - - if (completedEvent.testDuration > 122) { - completedEvent.chartData = "toolong"; - completedEvent.keySpacing = "toolong"; - completedEvent.keyDuration = "toolong"; - TestStats.setKeypressTimingsTooLong(); - } - - if (dontSave) { - try { - firebase.analytics().logEvent("testCompletedInvalid", completedEvent); - } catch (e) { - console.log("Analytics unavailable"); - } - return; - } - - // user is logged in - - if ( - Config.difficulty == "normal" || - ((Config.difficulty == "master" || Config.difficulty == "expert") && - !difficultyFailed) - ) { - TestStats.resetIncomplete(); - } - - completedEvent.uid = firebase.auth().currentUser.uid; - Result.updateRateQuote(randomQuote); - - Result.updateGraphPBLine(); - - AccountButton.loading(true); - completedEvent.challenge = ChallengeContoller.verify(completedEvent); - if (!completedEvent.challenge) delete completedEvent.challenge; - completedEvent.hash = objecthash(completedEvent); - axiosInstance - .post("/results/add", { - result: completedEvent, - }) - .then((response) => { - AccountButton.loading(false); - Result.hideCrown(); - - if (response.status !== 200) { - Notifications.add("Result not saved. " + response.data.message, -1); - } else { - completedEvent._id = response.data.insertedId; - if (response.data.isPb) { - completedEvent.isPb = true; - } - - DB.saveLocalResult(completedEvent); - DB.updateLocalStats({ - time: - completedEvent.testDuration + - completedEvent.incompleteTestSeconds - - completedEvent.afkDuration, - started: TestStats.restartCount + 1, - }); - - try { - firebase.analytics().logEvent("testCompleted", completedEvent); - } catch (e) { - console.log("Analytics unavailable"); - } - - if (response.data.isPb) { - //new pb - Result.showCrown(); - Result.updateCrown(); - DB.saveLocalPB( - Config.mode, - completedEvent.mode2, - Config.punctuation, - Config.language, - Config.difficulty, - Config.lazyMode, - completedEvent.wpm, - completedEvent.acc, - completedEvent.rawWpm, - completedEvent.consistency - ); - } - } - - $("#retrySavingResultButton").addClass("hidden"); - }) - .catch((e) => { - AccountButton.loading(false); - let msg = e?.response?.data?.message ?? e.message; - Notifications.add("Failed to save result: " + msg, -1); - $("#retrySavingResultButton").removeClass("hidden"); - - retrySaving.completedEvent = completedEvent; - retrySaving.canRetry = true; - }); -} - -export function fail(reason) { - failReason = reason; - // input.pushHistory(); - // corrected.pushHistory(); - TestStats.pushKeypressesToHistory(); - finish(true); - let testSeconds = TestStats.calculateTestSeconds(performance.now()); - let afkseconds = TestStats.calculateAfkSeconds(testSeconds); - let tt = testSeconds - afkseconds; - if (tt < 0) tt = 0; - TestStats.incrementIncompleteSeconds(tt); - TestStats.incrementRestartCount(); -} - -==> ./monkeytype/src/js/test/test-ui.js <== -import * as Notifications from "./notifications"; -import * as ThemeColors from "./theme-colors"; -import Config, * as UpdateConfig from "./config"; -import * as DB from "./db"; -import * as TestLogic from "./test-logic"; -import * as Funbox from "./funbox"; -import * as PaceCaret from "./pace-caret"; -import * as CustomText from "./custom-text"; -import * as Keymap from "./keymap"; -import * as Caret from "./caret"; -import * as CommandlineLists from "./commandline-lists"; -import * as Commandline from "./commandline"; -import * as OutOfFocus from "./out-of-focus"; -import * as ManualRestart from "./manual-restart-tracker"; -import * as PractiseWords from "./practise-words"; -import * as Replay from "./replay"; -import * as TestStats from "./test-stats"; -import * as Misc from "./misc"; -import * as TestUI from "./test-ui"; -import * as ChallengeController from "./challenge-controller"; -import * as RateQuotePopup from "./rate-quote-popup"; -import * as UI from "./ui"; -import * as TestTimer from "./test-timer"; - -export let currentWordElementIndex = 0; -export let resultVisible = false; -export let activeWordTop = 0; -export let testRestarting = false; -export let lineTransition = false; -export let currentTestLine = 0; -export let resultCalculating = false; - -export function setResultVisible(val) { - resultVisible = val; -} - -export function setCurrentWordElementIndex(val) { - currentWordElementIndex = val; -} - -export function setActiveWordTop(val) { - activeWordTop = val; -} - -export function setTestRestarting(val) { - testRestarting = val; -} - -export function setResultCalculating(val) { - resultCalculating = val; -} - -export function reset() { - currentTestLine = 0; - currentWordElementIndex = 0; -} - -export function focusWords() { - if (!$("#wordsWrapper").hasClass("hidden")) { - $("#wordsInput").focus(); - } -} - -export function updateActiveElement(backspace) { - let active = document.querySelector("#words .active"); - if (Config.mode == "zen" && backspace) { - active.remove(); - } else if (active !== null) { - if (Config.highlightMode == "word") { - active.querySelectorAll("letter").forEach((e) => { - e.classList.remove("correct"); - }); - } - active.classList.remove("active"); - } - try { - let activeWord = document.querySelectorAll("#words .word")[ - currentWordElementIndex - ]; - activeWord.classList.add("active"); - activeWord.classList.remove("error"); - activeWordTop = document.querySelector("#words .active").offsetTop; - if (Config.highlightMode == "word") { - activeWord.querySelectorAll("letter").forEach((e) => { - e.classList.add("correct"); - }); - } - } catch (e) {} -} - -function getWordHTML(word) { - let newlineafter = false; - let retval = `
`; - for (let c = 0; c < word.length; c++) { - if (Config.funbox === "arrows") { - if (word.charAt(c) === "↑") { - retval += ``; - } - if (word.charAt(c) === "↓") { - retval += ``; - } - if (word.charAt(c) === "←") { - retval += ``; - } - if (word.charAt(c) === "→") { - retval += ``; - } - } else if (word.charAt(c) === "t") { - retval += ``; - } else if (word.charAt(c) === "\n") { - newlineafter = true; - retval += ``; - } else { - retval += "" + word.charAt(c) + ""; - } - } - retval += "
"; - if (newlineafter) retval += "
"; - return retval; -} - -export function showWords() { - $("#words").empty(); - - let wordsHTML = ""; - if (Config.mode !== "zen") { - for (let i = 0; i < TestLogic.words.length; i++) { - wordsHTML += getWordHTML(TestLogic.words.get(i)); - } - } else { - wordsHTML = - '
word height
'; - } - - $("#words").html(wordsHTML); - - $("#wordsWrapper").removeClass("hidden"); - const wordHeight = $(document.querySelector(".word")).outerHeight(true); - const wordsHeight = $(document.querySelector("#words")).outerHeight(true); - console.log( - `Showing words. wordHeight: ${wordHeight}, wordsHeight: ${wordsHeight}` - ); - if ( - Config.showAllLines && - Config.mode != "time" && - !(CustomText.isWordRandom && CustomText.word == 0) && - !CustomText.isTimeRandom - ) { - $("#words").css("height", "auto"); - $("#wordsWrapper").css("height", "auto"); - let nh = wordHeight * 3; - - if (nh > wordsHeight) { - nh = wordsHeight; - } - $(".outOfFocusWarning").css("line-height", nh + "px"); - } else { - $("#words") - .css("height", wordHeight * 4 + "px") - .css("overflow", "hidden"); - $("#wordsWrapper") - .css("height", wordHeight * 3 + "px") - .css("overflow", "hidden"); - $(".outOfFocusWarning").css("line-height", wordHeight * 3 + "px"); - } - - if (Config.mode === "zen") { - $(document.querySelector(".word")).remove(); - } else { - if (Config.keymapMode === "next") { - Keymap.highlightKey( - TestLogic.words - .getCurrent() - .substring( - TestLogic.input.current.length, - TestLogic.input.current.length + 1 - ) - .toString() - .toUpperCase() - ); - } - } - - updateActiveElement(); - Funbox.toggleScript(TestLogic.words.getCurrent()); - - Caret.updatePosition(); -} - -export function addWord(word) { - $("#words").append(getWordHTML(word)); -} - -export function flipColors(tf) { - if (tf) { - $("#words").addClass("flipped"); - } else { - $("#words").removeClass("flipped"); - } -} - -export function colorful(tc) { - if (tc) { - $("#words").addClass("colorfulMode"); - } else { - $("#words").removeClass("colorfulMode"); - } -} - -export async function screenshot() { - let revealReplay = false; - function revertScreenshot() { - $("#notificationCenter").removeClass("hidden"); - $("#commandLineMobileButton").removeClass("hidden"); - $(".pageTest .ssWatermark").addClass("hidden"); - $(".pageTest .ssWatermark").text("monkeytype.com"); - $(".pageTest .buttons").removeClass("hidden"); - if (revealReplay) $("#resultReplay").removeClass("hidden"); - if (firebase.auth().currentUser == null) - $(".pageTest .loginTip").removeClass("hidden"); - } - - if (!$("#resultReplay").hasClass("hidden")) { - revealReplay = true; - Replay.pauseReplay(); - } - $("#resultReplay").addClass("hidden"); - $(".pageTest .ssWatermark").removeClass("hidden"); - $(".pageTest .ssWatermark").text( - moment(Date.now()).format("DD MMM YYYY HH:mm") + " | monkeytype.com " - ); - if (firebase.auth().currentUser != null) { - $(".pageTest .ssWatermark").text( - DB.getSnapshot().name + - " | " + - moment(Date.now()).format("DD MMM YYYY HH:mm") + - " | monkeytype.com " - ); - } - $(".pageTest .buttons").addClass("hidden"); - let src = $("#middle"); - var sourceX = src.position().left; /*X position from div#target*/ - var sourceY = src.position().top; /*Y position from div#target*/ - var sourceWidth = src.outerWidth( - true - ); /*clientWidth/offsetWidth from div#target*/ - var sourceHeight = src.outerHeight( - true - ); /*clientHeight/offsetHeight from div#target*/ - $("#notificationCenter").addClass("hidden"); - $("#commandLineMobileButton").addClass("hidden"); - $(".pageTest .loginTip").addClass("hidden"); - try { - let paddingX = 50; - let paddingY = 25; - html2canvas(document.body, { - backgroundColor: await ThemeColors.get("bg"), - width: sourceWidth + paddingX * 2, - height: sourceHeight + paddingY * 2, - x: sourceX - paddingX, - y: sourceY - paddingY, - }).then(function (canvas) { - canvas.toBlob(function (blob) { - try { - if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) { - open(URL.createObjectURL(blob)); - revertScreenshot(); - } else { - navigator.clipboard - .write([ - new ClipboardItem( - Object.defineProperty({}, blob.type, { - value: blob, - enumerable: true, - }) - ), - ]) - .then(() => { - Notifications.add("Copied to clipboard", 1, 2); - revertScreenshot(); - }); - } - } catch (e) { - Notifications.add( - "Error saving image to clipboard: " + e.message, - -1 - ); - revertScreenshot(); - } - }); - }); - } catch (e) { - Notifications.add("Error creating image: " + e.message, -1); - revertScreenshot(); - } - setTimeout(() => { - revertScreenshot(); - }, 3000); -} - -export function updateWordElement(showError = !Config.blindMode) { - let input = TestLogic.input.current; - let wordAtIndex; - let currentWord; - wordAtIndex = document.querySelector("#words .word.active"); - currentWord = TestLogic.words.getCurrent(); - let ret = ""; - - let newlineafter = false; - - if (Config.mode === "zen") { - for (let i = 0; i < TestLogic.input.current.length; i++) { - if (TestLogic.input.current[i] === "t") { - ret += ``; - } else if (TestLogic.input.current[i] === "\n") { - newlineafter = true; - ret += ``; - } else { - ret += `${TestLogic.input.current[i]}`; - } - } - } else { - let correctSoFar = false; - - // slice earlier if input has trailing compose characters - const inputWithoutComposeLength = Misc.trailingComposeChars.test(input) - ? input.search(Misc.trailingComposeChars) - : input.length; - if ( - input.search(Misc.trailingComposeChars) < currentWord.length && - currentWord.slice(0, inputWithoutComposeLength) === - input.slice(0, inputWithoutComposeLength) - ) { - correctSoFar = true; - } - - let wordHighlightClassString = correctSoFar ? "correct" : "incorrect"; - if (Config.blindMode) { - wordHighlightClassString = "correct"; - } - - for (let i = 0; i < input.length; i++) { - let charCorrect = currentWord[i] == input[i]; - - let correctClass = "correct"; - if (Config.highlightMode == "off") { - correctClass = ""; - } - - let currentLetter = currentWord[i]; - let tabChar = ""; - let nlChar = ""; - if (Config.funbox === "arrows") { - if (currentLetter === "↑") { - currentLetter = ``; - } - if (currentLetter === "↓") { - currentLetter = ``; - } - if (currentLetter === "←") { - currentLetter = ``; - } - if (currentLetter === "→") { - currentLetter = ``; - } - } else if (currentLetter === "t") { - tabChar = "tabChar"; - currentLetter = ``; - } else if (currentLetter === "\n") { - nlChar = "nlChar"; - currentLetter = ``; - } - - if ( - Misc.trailingComposeChars.test(input) && - i > input.search(Misc.trailingComposeChars) - ) - continue; - - if (charCorrect) { - ret += `${currentLetter}`; - } else if ( - currentLetter !== undefined && - Misc.trailingComposeChars.test(input) && - i === input.search(Misc.trailingComposeChars) - ) { - ret += `${currentLetter}`; - } else if (!showError) { - if (currentLetter !== undefined) { - ret += `${currentLetter}`; - } - } else if (currentLetter === undefined) { - if (!Config.hideExtraLetters) { - let letter = input[i]; - if (letter == " " || letter == "t" || letter == "\n") { - letter = "_"; - } - ret += `${letter}`; - } - } else { - ret += - `` + - currentLetter + - (Config.indicateTypos ? `${input[i]}` : "") + - ""; - } - } - - const inputWithSingleComposeLength = Misc.trailingComposeChars.test(input) - ? input.search(Misc.trailingComposeChars) + 1 - : input.length; - if (inputWithSingleComposeLength < currentWord.length) { - for (let i = inputWithSingleComposeLength; i < currentWord.length; i++) { - if (Config.funbox === "arrows") { - if (currentWord[i] === "↑") { - ret += ``; - } - if (currentWord[i] === "↓") { - ret += ``; - } - if (currentWord[i] === "←") { - ret += ``; - } - if (currentWord[i] === "→") { - ret += ``; - } - } else if (currentWord[i] === "t") { - ret += ``; - } else if (currentWord[i] === "\n") { - ret += ``; - } else { - ret += - `` + - currentWord[i] + - ""; - } - } - } - - if (Config.highlightMode === "letter" && Config.hideExtraLetters) { - if (input.length > currentWord.length && !Config.blindMode) { - $(wordAtIndex).addClass("error"); - } else if (input.length == currentWord.length) { - $(wordAtIndex).removeClass("error"); - } - } - } - wordAtIndex.innerHTML = ret; - if (newlineafter) $("#words").append("
"); -} - -export function lineJump(currentTop) { - //last word of the line - if (currentTestLine > 0) { - let hideBound = currentTop; - - let toHide = []; - let wordElements = $("#words .word"); - for (let i = 0; i < currentWordElementIndex; i++) { - if ($(wordElements[i]).hasClass("hidden")) continue; - let forWordTop = Math.floor(wordElements[i].offsetTop); - if (forWordTop < hideBound - 10) { - toHide.push($($("#words .word")[i])); - } - } - const wordHeight = $(document.querySelector(".word")).outerHeight(true); - if (Config.smoothLineScroll && toHide.length > 0) { - lineTransition = true; - $("#words").prepend( - `
` - ); - $("#words .smoothScroller").animate( - { - height: 0, - }, - TestTimer.slowTimer ? 0 : 125, - () => { - $("#words .smoothScroller").remove(); - } - ); - $("#paceCaret").animate( - { - top: document.querySelector("#paceCaret").offsetTop - wordHeight, - }, - TestTimer.slowTimer ? 0 : 125 - ); - $("#words").animate( - { - marginTop: `-${wordHeight}px`, - }, - TestTimer.slowTimer ? 0 : 125, - () => { - activeWordTop = document.querySelector("#words .active").offsetTop; - - currentWordElementIndex -= toHide.length; - lineTransition = false; - toHide.forEach((el) => el.remove()); - $("#words").css("marginTop", "0"); - } - ); - } else { - toHide.forEach((el) => el.remove()); - currentWordElementIndex -= toHide.length; - $("#paceCaret").css({ - top: document.querySelector("#paceCaret").offsetTop - wordHeight, - }); - } - } - currentTestLine++; -} - -export function updateModesNotice() { - let anim = false; - if ($(".pageTest #testModesNotice").text() === "") anim = true; - - $(".pageTest #testModesNotice").empty(); - - if (TestLogic.isRepeated && Config.mode !== "quote") { - $(".pageTest #testModesNotice").append( - `
repeated
` - ); - } - - if (TestLogic.hasTab) { - $(".pageTest #testModesNotice").append( - `
shift + tab to restart
` - ); - } - - if (ChallengeController.active) { - $(".pageTest #testModesNotice").append( - `
${ChallengeController.active.display}
` - ); - } - - if (Config.mode === "zen") { - $(".pageTest #testModesNotice").append( - `
shift + enter to finish zen
` - ); - } - - // /^[0-9a-zA-Z_.-]+$/.test(name); - - if ( - (/_\d+k$/g.test(Config.language) || - /code_/g.test(Config.language) || - Config.language == "english_commonly_misspelled") && - Config.mode !== "quote" - ) { - $(".pageTest #testModesNotice").append( - `
${Config.language.replace( - /_/g, - " " - )}
` - ); - } - - if (Config.difficulty === "expert") { - $(".pageTest #testModesNotice").append( - `
expert
` - ); - } else if (Config.difficulty === "master") { - $(".pageTest #testModesNotice").append( - `
master
` - ); - } - - if (Config.blindMode) { - $(".pageTest #testModesNotice").append( - `
blind
` - ); - } - - if (Config.lazyMode) { - $(".pageTest #testModesNotice").append( - `
lazy
` - ); - } - - if ( - Config.paceCaret !== "off" || - (Config.repeatedPace && TestLogic.isPaceRepeat) - ) { - let speed = ""; - try { - speed = ` (${Math.round(PaceCaret.settings.wpm)} wpm)`; - } catch {} - $(".pageTest #testModesNotice").append( - `
${ - Config.paceCaret === "average" - ? "average" - : Config.paceCaret === "pb" - ? "pb" - : "custom" - } pace${speed}
` - ); - } - - if (Config.minWpm !== "off") { - $(".pageTest #testModesNotice").append( - `
min ${Config.minWpmCustomSpeed} wpm
` - ); - } - - if (Config.minAcc !== "off") { - $(".pageTest #testModesNotice").append( - `
min ${Config.minAccCustom}% acc
` - ); - } - - if (Config.minBurst !== "off") { - $(".pageTest #testModesNotice").append( - `
min ${ - Config.minBurstCustomSpeed - } burst ${Config.minBurst === "flex" ? "(flex)" : ""}
` - ); - } - - if (Config.funbox !== "none") { - $(".pageTest #testModesNotice").append( - `
${Config.funbox.replace( - /_/g, - " " - )}
` - ); - } - - if (Config.confidenceMode === "on") { - $(".pageTest #testModesNotice").append( - `
confidence
` - ); - } - if (Config.confidenceMode === "max") { - $(".pageTest #testModesNotice").append( - `
max confidence
` - ); - } - - if (Config.stopOnError != "off") { - $(".pageTest #testModesNotice").append( - `
stop on ${Config.stopOnError}
` - ); - } - - if (Config.layout !== "default") { - $(".pageTest #testModesNotice").append( - `
emulating ${Config.layout.replace( - /_/g, - " " - )}
` - ); - } - - if (Config.oppositeShiftMode !== "off") { - $(".pageTest #testModesNotice").append( - `
opposite shift${ - Config.oppositeShiftMode === "keymap" ? " (keymap)" : "" - }
` - ); - } - - let tagsString = ""; - try { - DB.getSnapshot().tags.forEach((tag) => { - if (tag.active === true) { - tagsString += tag.name + ", "; - } - }); - - if (tagsString !== "") { - $(".pageTest #testModesNotice").append( - `
${tagsString.substring( - 0, - tagsString.length - 2 - )}
` - ); - } - } catch {} - - if (anim) { - $(".pageTest #testModesNotice") - .css("transition", "none") - .css("opacity", 0) - .animate( - { - opacity: 1, - }, - 125, - () => { - $(".pageTest #testModesNotice").css("transition", ".125s"); - } - ); - } -} - -export function arrangeCharactersRightToLeft() { - $("#words").addClass("rightToLeftTest"); - $("#resultWordsHistory .words").addClass("rightToLeftTest"); - $("#resultReplay .words").addClass("rightToLeftTest"); -} - -export function arrangeCharactersLeftToRight() { - $("#words").removeClass("rightToLeftTest"); - $("#resultWordsHistory .words").removeClass("rightToLeftTest"); - $("#resultReplay .words").removeClass("rightToLeftTest"); -} - -async function loadWordsHistory() { - $("#resultWordsHistory .words").empty(); - let wordsHTML = ""; - for (let i = 0; i < TestLogic.input.history.length + 2; i++) { - let input = TestLogic.input.getHistory(i); - let word = TestLogic.words.get(i); - let wordEl = ""; - try { - if (input === "") throw new Error("empty input word"); - if ( - TestLogic.corrected.getHistory(i) !== undefined && - TestLogic.corrected.getHistory(i) !== "" - ) { - wordEl = `
`; - } else { - wordEl = `
`; - } - if (i === TestLogic.input.history.length - 1) { - //last word - let wordstats = { - correct: 0, - incorrect: 0, - missed: 0, - }; - let length = Config.mode == "zen" ? input.length : word.length; - for (let c = 0; c < length; c++) { - if (c < input.length) { - //on char that still has a word list pair - if (Config.mode == "zen" || input[c] == word[c]) { - wordstats.correct++; - } else { - wordstats.incorrect++; - } - } else { - //on char that is extra - wordstats.missed++; - } - } - if (wordstats.incorrect !== 0 || Config.mode !== "time") { - if (Config.mode != "zen" && input !== word) { - wordEl = `
`; - } - } - } else { - if (Config.mode != "zen" && input !== word) { - wordEl = `
`; - } - } - - let loop; - if (Config.mode == "zen" || input.length > word.length) { - //input is longer - extra characters possible (loop over input) - loop = input.length; - } else { - //input is shorter or equal (loop over word list) - loop = word.length; - } - - for (let c = 0; c < loop; c++) { - let correctedChar; - try { - correctedChar = TestLogic.corrected.getHistory(i)[c]; - } catch (e) { - correctedChar = undefined; - } - let extraCorrected = ""; - if ( - c + 1 === loop && - TestLogic.corrected.getHistory(i) !== undefined && - TestLogic.corrected.getHistory(i).length > input.length - ) { - extraCorrected = "extraCorrected"; - } - if (Config.mode == "zen" || word[c] !== undefined) { - if (Config.mode == "zen" || input[c] === word[c]) { - if (correctedChar === input[c] || correctedChar === undefined) { - wordEl += `${input[c]}`; - } else { - wordEl += - `` + - input[c] + - ""; - } - } else { - if (input[c] === TestLogic.input.current) { - wordEl += - `` + - word[c] + - ""; - } else if (input[c] === undefined) { - wordEl += "" + word[c] + ""; - } else { - wordEl += - `` + - word[c] + - ""; - } - } - } else { - wordEl += '' + input[c] + ""; - } - } - wordEl += "
"; - } catch (e) { - try { - wordEl = "
"; - for (let c = 0; c < word.length; c++) { - wordEl += "" + word[c] + ""; - } - wordEl += "
"; - } catch {} - } - wordsHTML += wordEl; - } - $("#resultWordsHistory .words").html(wordsHTML); - $("#showWordHistoryButton").addClass("loaded"); - return true; -} - -export function toggleResultWords() { - if (resultVisible) { - if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) { - //show - - if (!$("#showWordHistoryButton").hasClass("loaded")) { - $("#words").html( - `
` - ); - loadWordsHistory().then(() => { - if (Config.burstHeatmap) { - TestUI.applyBurstHeatmap(); - } - $("#resultWordsHistory") - .removeClass("hidden") - .css("display", "none") - .slideDown(250, () => { - if (Config.burstHeatmap) { - TestUI.applyBurstHeatmap(); - } - }); - }); - } else { - if (Config.burstHeatmap) { - TestUI.applyBurstHeatmap(); - } - $("#resultWordsHistory") - .removeClass("hidden") - .css("display", "none") - .slideDown(250); - } - } else { - //hide - - $("#resultWordsHistory").slideUp(250, () => { - $("#resultWordsHistory").addClass("hidden"); - }); - } - } -} - -export function applyBurstHeatmap() { - if (Config.burstHeatmap) { - $("#resultWordsHistory .heatmapLegend").removeClass("hidden"); - - let burstlist = [...TestStats.burstHistory]; - - burstlist = burstlist.filter((x) => x !== Infinity); - burstlist = burstlist.filter((x) => x < 350); - - if ( - TestLogic.input.getHistory(TestLogic.input.getHistory().length - 1) - .length !== TestLogic.words.getCurrent()?.length - ) { - burstlist = burstlist.splice(0, burstlist.length - 1); - } - - let median = Misc.median(burstlist); - let adatm = []; - burstlist.forEach((burst) => { - adatm.push(Math.abs(median - burst)); - }); - let step = Misc.mean(adatm); - let steps = [ - { - val: 0, - class: "heatmap-0", - }, - { - val: median - step * 1.5, - class: "heatmap-1", - }, - { - val: median - step * 0.5, - class: "heatmap-2", - }, - { - val: median + step * 0.5, - class: "heatmap-3", - }, - { - val: median + step * 1.5, - class: "heatmap-4", - }, - ]; - $("#resultWordsHistory .words .word").each((index, word) => { - let wordBurstVal = parseInt($(word).attr("burst")); - let cls = ""; - steps.forEach((step) => { - if (wordBurstVal > step.val) cls = step.class; - }); - $(word).addClass(cls); - }); - } else { - $("#resultWordsHistory .heatmapLegend").addClass("hidden"); - $("#resultWordsHistory .words .word").removeClass("heatmap-0"); - $("#resultWordsHistory .words .word").removeClass("heatmap-1"); - $("#resultWordsHistory .words .word").removeClass("heatmap-2"); - $("#resultWordsHistory .words .word").removeClass("heatmap-3"); - $("#resultWordsHistory .words .word").removeClass("heatmap-4"); - } -} - -export function highlightBadWord(index, showError) { - if (!showError) return; - $($("#words .word")[index]).addClass("error"); -} - -$(document.body).on("click", "#saveScreenshotButton", () => { - screenshot(); -}); - -$(document).on("click", "#testModesNotice .text-button.restart", (event) => { - TestLogic.restart(); -}); - -$(document).on("click", "#testModesNotice .text-button.blind", (event) => { - UpdateConfig.toggleBlindMode(); -}); - -$(".pageTest #copyWordsListButton").click(async (event) => { - try { - let words; - if (Config.mode == "zen") { - words = TestLogic.input.history.join(" "); - } else { - words = TestLogic.words - .get() - .slice(0, TestLogic.input.history.length) - .join(" "); - } - await navigator.clipboard.writeText(words); - Notifications.add("Copied to clipboard", 0, 2); - } catch (e) { - Notifications.add("Could not copy to clipboard: " + e, -1); - } -}); - -$(".pageTest #rateQuoteButton").click(async (event) => { - RateQuotePopup.show(TestLogic.randomQuote); -}); - -$(".pageTest #toggleBurstHeatmap").click(async (event) => { - UpdateConfig.setBurstHeatmap(!Config.burstHeatmap); -}); - -$(".pageTest .loginTip .link").click(async (event) => { - UI.changePage("login"); -}); - -$(document).on("mouseleave", "#resultWordsHistory .words .word", (e) => { - $(".wordInputAfter").remove(); -}); - -$("#wpmChart").on("mouseleave", (e) => { - $(".wordInputAfter").remove(); -}); - -$(document).on("mouseenter", "#resultWordsHistory .words .word", (e) => { - if (resultVisible) { - let input = $(e.currentTarget).attr("input"); - let burst = $(e.currentTarget).attr("burst"); - if (input != undefined) - $(e.currentTarget).append( - `
-
- ${input - .replace(/t/g, "_") - .replace(/\n/g, "_") - .replace(//g, ">")} -
-
- ${Math.round(Config.alwaysShowCPM ? burst * 5 : burst)}${ - Config.alwaysShowCPM ? "cpm" : "wpm" - } -
-
` - ); - } -}); - -$(document).on("click", "#testModesNotice .text-button", (event) => { - // console.log("CommandlineLists."+$(event.currentTarget).attr("commands")); - let commands = CommandlineLists.getList( - $(event.currentTarget).attr("commands") - ); - let func = $(event.currentTarget).attr("function"); - if (commands !== undefined) { - if ($(event.currentTarget).attr("commands") === "commandsTags") { - CommandlineLists.updateTagCommands(); - } - CommandlineLists.pushCurrent(commands); - Commandline.show(); - } else if (func != undefined) { - eval(func); - } -}); - -$("#wordsInput").on("focus", () => { - if (!resultVisible && Config.showOutOfFocusWarning) { - OutOfFocus.hide(); - } - Caret.show(TestLogic.input.current); -}); - -$("#wordsInput").on("focusout", () => { - if (!resultVisible && Config.showOutOfFocusWarning) { - OutOfFocus.show(); - } - Caret.hide(); -}); - -$(document).on("keypress", "#restartTestButton", (event) => { - if (event.key == "Enter") { - ManualRestart.reset(); - if ( - TestLogic.active && - Config.repeatQuotes === "typing" && - Config.mode === "quote" - ) { - TestLogic.restart(true); - } else { - TestLogic.restart(); - } - } -}); - -$(document.body).on("click", "#restartTestButton", () => { - ManualRestart.set(); - if (resultCalculating) return; - if ( - TestLogic.active && - Config.repeatQuotes === "typing" && - Config.mode === "quote" - ) { - TestLogic.restart(true); - } else { - TestLogic.restart(); - } -}); - -$(document.body).on( - "click", - "#retrySavingResultButton", - TestLogic.retrySavingResult -); - -$(document).on("keypress", "#practiseWordsButton", (event) => { - if (event.keyCode == 13) { - PractiseWords.showPopup(true); - } -}); - -$(document.body).on("click", "#practiseWordsButton", () => { - // PractiseWords.init(); - PractiseWords.showPopup(); -}); - -$(document).on("keypress", "#nextTestButton", (event) => { - if (event.keyCode == 13) { - TestLogic.restart(); - } -}); - -$(document.body).on("click", "#nextTestButton", () => { - ManualRestart.set(); - TestLogic.restart(); -}); - -$(document).on("keypress", "#showWordHistoryButton", (event) => { - if (event.keyCode == 13) { - toggleResultWords(); - } -}); - -$(document.body).on("click", "#showWordHistoryButton", () => { - toggleResultWords(); -}); - -$(document.body).on("click", "#restartTestButtonWithSameWordset", () => { - if (Config.mode == "zen") { - Notifications.add("Repeat test disabled in zen mode"); - return; - } - ManualRestart.set(); - TestLogic.restart(true); -}); - -$(document).on("keypress", "#restartTestButtonWithSameWordset", (event) => { - if (Config.mode == "zen") { - Notifications.add("Repeat test disabled in zen mode"); - return; - } - if (event.keyCode == 13) { - TestLogic.restart(true); - } -}); - -$("#wordsWrapper").on("click", () => { - focusWords(); -}); - -==> ./monkeytype/src/js/test/lazy-mode.js <== -let accents = [ - ["áàâäåãąą́āą̄ă", "a"], - ["éèêëẽęę́ēę̄ėě", "e"], - ["íìîïĩįį́īį̄", "i"], - ["óòôöøõóōǫǫ́ǭő", "o"], - ["úùûüŭũúūůű", "u"], - ["ńň", "n"], - ["çĉčć", "c"], - ["ř", "r"], - ["ď", "d"], - ["ťț", "t"], - ["æ", "ae"], - ["œ", "oe"], - ["ẅ", "w"], - ["ĝğg̃", "g"], - ["ĥ", "h"], - ["ĵ", "j"], - ["ń", "n"], - ["ŝśšș", "s"], - ["żźž", "z"], - ["ÿỹýÿŷ", "y"], - ["ł", "l"], - ["أإآ", "ا"], - ["َ", ""], - ["ُ", ""], - ["ِ", ""], - ["ْ", ""], - ["ً", ""], - ["ٌ", ""], - ["ٍ", ""], - ["ّ", ""], -]; - -export function replaceAccents(word, accentsOverride) { - let newWord = word; - if (!accents && !accentsOverride) return newWord; - let regex; - let list = accentsOverride || accents; - for (let i = 0; i < list.length; i++) { - regex = new RegExp(`[${list[i][0]}]`, "gi"); - newWord = newWord.replace(regex, list[i][1]); - } - return newWord; -} - -==> ./monkeytype/src/js/test/british-english.js <== -import { capitalizeFirstLetter } from "./misc"; - -let list = null; - -export async function getList() { - if (list == null) { - return $.getJSON("languages/britishenglish.json", function (data) { - list = data; - return list; - }); - } else { - return list; - } -} - -export async function replace(word) { - let list = await getList(); - let replacement = list.find((a) => - word.match(RegExp(`^([\\W]*${a[0]}[\\W]*)$`, "gi")) - ); - return replacement - ? word.replace( - RegExp(`^(?:([\\W]*)(${replacement[0]})([\\W]*))$`, "gi"), - (_, $1, $2, $3) => - $1 + - ($2.charAt(0) === $2.charAt(0).toUpperCase() - ? $2 === $2.toUpperCase() - ? replacement[1].toUpperCase() - : capitalizeFirstLetter(replacement[1]) - : replacement[1]) + - $3 - ) - : word; -} - -==> ./monkeytype/src/js/test/custom-text.js <== -export let text = "The quick brown fox jumps over the lazy dog".split(" "); -export let isWordRandom = false; -export let isTimeRandom = false; -export let word = ""; -export let time = ""; -export let delimiter = " "; - -export function setText(txt) { - text = txt; -} - -export function setIsWordRandom(val) { - isWordRandom = val; -} - -export function setIsTimeRandom(val) { - isTimeRandom = val; -} - -export function setTime(val) { - time = val; -} - -export function setWord(val) { - word = val; -} - -export function setDelimiter(val) { - delimiter = val; -} - -==> ./monkeytype/src/js/test/live-burst.js <== -import Config from "./config"; -import * as TestLogic from "./test-logic"; - -export function update(burst) { - let number = burst; - if (Config.blindMode) { - number = 0; - } - document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = number; - document.querySelector("#liveBurst").innerHTML = number; -} - -export function show() { - if (!Config.showLiveBurst) return; - if (!TestLogic.active) return; - if (Config.timerStyle === "mini") { - if (!$("#miniTimerAndLiveWpm .burst").hasClass("hidden")) return; - $("#miniTimerAndLiveWpm .burst") - .removeClass("hidden") - .css("opacity", 0) - .animate( - { - opacity: Config.timerOpacity, - }, - 125 - ); - } else { - if (!$("#liveBurst").hasClass("hidden")) return; - $("#liveBurst").removeClass("hidden").css("opacity", 0).animate( - { - opacity: Config.timerOpacity, - }, - 125 - ); - } -} - -export function hide() { - $("#liveBurst").animate( - { - opacity: Config.timerOpacity, - }, - 125, - () => { - $("#liveBurst").addClass("hidden"); - } - ); - $("#miniTimerAndLiveWpm .burst").animate( - { - opacity: Config.timerOpacity, - }, - 125, - () => { - $("#miniTimerAndLiveWpm .burst").addClass("hidden"); - } - ); -} - -==> ./monkeytype/src/js/test/live-wpm.js <== -import Config from "./config"; -import * as TestLogic from "./test-logic"; - -let liveWpmElement = document.querySelector("#liveWpm"); -let miniLiveWpmElement = document.querySelector("#miniTimerAndLiveWpm .wpm"); - -export function update(wpm, raw) { - // if (!TestLogic.active || !Config.showLiveWpm) { - // hideLiveWpm(); - // } else { - // showLiveWpm(); - // } - let number = wpm; - if (Config.blindMode) { - number = raw; - } - if (Config.alwaysShowCPM) { - number = Math.round(number * 5); - } - miniLiveWpmElement.innerHTML = number; - liveWpmElement.innerHTML = number; -} - -export function show() { - if (!Config.showLiveWpm) return; - if (!TestLogic.active) return; - if (Config.timerStyle === "mini") { - // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity); - if (!$("#miniTimerAndLiveWpm .wpm").hasClass("hidden")) return; - $("#miniTimerAndLiveWpm .wpm") - .removeClass("hidden") - .css("opacity", 0) - .animate( - { - opacity: Config.timerOpacity, - }, - 125 - ); - } else { - // $("#liveWpm").css("opacity", Config.timerOpacity); - if (!$("#liveWpm").hasClass("hidden")) return; - $("#liveWpm").removeClass("hidden").css("opacity", 0).animate( - { - opacity: Config.timerOpacity, - }, - 125 - ); - } -} - -export function hide() { - $("#liveWpm").animate( - { - opacity: Config.timerOpacity, - }, - 125, - () => { - $("#liveWpm").addClass("hidden"); - } - ); - $("#miniTimerAndLiveWpm .wpm").animate( - { - opacity: Config.timerOpacity, - }, - 125, - () => { - $("#miniTimerAndLiveWpm .wpm").addClass("hidden"); - } - ); -} - -==> ./monkeytype/src/js/test/focus.js <== -import * as Caret from "./caret"; -import * as UI from "./ui"; - -let state = false; - -export function set(foc, withCursor = false) { - if (foc && !state) { - state = true; - Caret.stopAnimation(); - $("#top").addClass("focus"); - $("#bottom").addClass("focus"); - if (!withCursor) $("body").css("cursor", "none"); - $("#middle").addClass("focus"); - } else if (!foc && state) { - state = false; - Caret.startAnimation(); - $("#top").removeClass("focus"); - $("#bottom").removeClass("focus"); - $("body").css("cursor", "default"); - $("#middle").removeClass("focus"); - } -} - -$(document).mousemove(function (event) { - if (!state) return; - if (UI.getActivePage() == "pageLoading") return; - if (UI.getActivePage() == "pageAccount" && state == true) return; - if ( - $("#top").hasClass("focus") && - (event.originalEvent.movementX > 0 || event.originalEvent.movementY > 0) - ) { - set(false); - } -}); - -==> ./monkeytype/src/js/test/today-tracker.js <== -import * as Misc from "./misc"; -import * as DB from "./db"; - -let seconds = 0; -let addedAllToday = false; -let dayToday = null; - -export function addSeconds(s) { - if (addedAllToday) { - let nowDate = new Date(); - nowDate = nowDate.getDate(); - if (nowDate > dayToday) { - seconds = s; - return; - } - } - seconds += s; -} - -export function getString() { - let secString = Misc.secondsToString(Math.round(seconds), true, true); - return secString + (addedAllToday === true ? " today" : " session"); -} - -export async function addAllFromToday() { - let todayDate = new Date(); - todayDate.setSeconds(0); - todayDate.setMinutes(0); - todayDate.setHours(0); - todayDate.setMilliseconds(0); - dayToday = todayDate.getDate(); - todayDate = todayDate.getTime(); - - seconds = 0; - - let results = await DB.getSnapshot().results; - - results.forEach((result) => { - let resultDate = new Date(result.timestamp); - resultDate.setSeconds(0); - resultDate.setMinutes(0); - resultDate.setHours(0); - resultDate.setMilliseconds(0); - resultDate = resultDate.getTime(); - - if (resultDate >= todayDate) { - seconds += - result.testDuration + result.incompleteTestSeconds - result.afkDuration; - } - }); - - addedAllToday = true; -} - -==> ./monkeytype/src/js/test/wikipedia.js <== -import * as Loader from "./loader"; -import Config from "./config"; -import * as Misc from "./misc"; - -export class Section { - constructor(title, author, words) { - this.title = title; - this.author = author; - this.words = words; - } -} - -export async function getTLD(languageGroup) { - // language group to tld - switch (languageGroup.name) { - case "english": - return "en"; - - case "spanish": - return "es"; - - case "french": - return "fr"; - - case "german": - return "de"; - - case "portuguese": - return "pt"; - - case "italian": - return "it"; - - case "dutch": - return "nl"; - - default: - return "en"; - } -} - -export async function getSection() { - // console.log("Getting section"); - Loader.show(); - - // get TLD for wikipedia according to language group - let urlTLD = "en"; - let currentLanguageGroup = await Misc.findCurrentGroup(Config.language); - urlTLD = await getTLD(currentLanguageGroup); - - const randomPostURL = `https://${urlTLD}.wikipedia.org/api/rest_v1/page/random/summary`; - var sectionObj = {}; - var randomPostReq = await fetch(randomPostURL); - var pageid = 0; - - if (randomPostReq.status == 200) { - let postObj = await randomPostReq.json(); - sectionObj.title = postObj.title; - sectionObj.author = postObj.author; - pageid = postObj.pageid; - } - - return new Promise((res, rej) => { - if (randomPostReq.status != 200) { - Loader.hide(); - rej(randomPostReq.status); - } - - const sectionURL = `https://${urlTLD}.wikipedia.org/w/api.php?action=query&format=json&pageids=${pageid}&prop=extracts&exintro=true&origin=*`; - - var sectionReq = new XMLHttpRequest(); - sectionReq.onload = () => { - if (sectionReq.readyState == 4) { - if (sectionReq.status == 200) { - let sectionText = JSON.parse(sectionReq.responseText).query.pages[ - pageid.toString() - ].extract; - let words = []; - - // Remove double whitespaces and finally trailing whitespaces. - sectionText = sectionText.replace(/<\/p>

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

").html(sectionText).text(); - - sectionText = sectionText.replace(/\s+/g, " "); - sectionText = sectionText.trim(); - - // // Add spaces - // sectionText = sectionText.replace(/[a-zA-Z0-9]{3,}\.[a-zA-Z]/g, (x) => - // x.replace(/\./, ". ") - // ); - - sectionText.split(" ").forEach((word) => { - words.push(word); - }); - - let section = new Section(sectionObj.title, sectionObj.author, words); - Loader.hide(); - res(section); - } else { - Loader.hide(); - rej(sectionReq.status); - } - } - }; - sectionReq.open("GET", sectionURL); - sectionReq.send(); - }); -} - -==> ./monkeytype/src/js/test/timer-progress.js <== -import Config from "./config"; -import * as CustomText from "./custom-text"; -import * as Misc from "./misc"; -import * as TestLogic from "./test-logic"; -import * as TestTimer from "./test-timer"; - -export function show() { - let op = Config.showTimerProgress ? Config.timerOpacity : 0; - if (Config.mode != "zen" && Config.timerStyle === "bar") { - $("#timerWrapper").stop(true, true).removeClass("hidden").animate( - { - opacity: op, - }, - 125 - ); - } else if (Config.timerStyle === "text") { - $("#timerNumber") - .stop(true, true) - .removeClass("hidden") - .css("opacity", 0) - .animate( - { - opacity: op, - }, - 125 - ); - } else if (Config.mode == "zen" || Config.timerStyle === "mini") { - if (op > 0) { - $("#miniTimerAndLiveWpm .time") - .stop(true, true) - .removeClass("hidden") - .animate( - { - opacity: op, - }, - 125 - ); - } - } -} - -export function hide() { - $("#timerWrapper").stop(true, true).animate( - { - opacity: 0, - }, - 125 - ); - $("#miniTimerAndLiveWpm .time") - .stop(true, true) - .animate( - { - opacity: 0, - }, - 125, - () => { - $("#miniTimerAndLiveWpm .time").addClass("hidden"); - } - ); - $("#timerNumber").stop(true, true).animate( - { - opacity: 0, - }, - 125 - ); -} - -export function restart() { - if (Config.timerStyle === "bar") { - if (Config.mode === "time") { - $("#timer").stop(true, true).animate( - { - width: "100vw", - }, - 0 - ); - } else if (Config.mode === "words" || Config.mode === "custom") { - $("#timer").stop(true, true).animate( - { - width: "0vw", - }, - 0 - ); - } - } -} - -let timerNumberElement = document.querySelector("#timerNumber"); -let miniTimerNumberElement = document.querySelector( - "#miniTimerAndLiveWpm .time" -); - -export function update() { - let time = TestTimer.time; - if ( - Config.mode === "time" || - (Config.mode === "custom" && CustomText.isTimeRandom) - ) { - let maxtime = Config.time; - if (Config.mode === "custom" && CustomText.isTimeRandom) { - maxtime = CustomText.time; - } - if (Config.timerStyle === "bar") { - let percent = 100 - ((time + 1) / maxtime) * 100; - $("#timer") - .stop(true, true) - .animate( - { - width: percent + "vw", - }, - TestTimer.slowTimer ? 0 : 1000, - "linear" - ); - } else if (Config.timerStyle === "text") { - let displayTime = Misc.secondsToString(maxtime - time); - if (maxtime === 0) { - displayTime = Misc.secondsToString(time); - } - timerNumberElement.innerHTML = "
" + displayTime + "
"; - } else if (Config.timerStyle === "mini") { - let displayTime = Misc.secondsToString(maxtime - time); - if (maxtime === 0) { - displayTime = Misc.secondsToString(time); - } - miniTimerNumberElement.innerHTML = displayTime; - } - } else if ( - Config.mode === "words" || - Config.mode === "custom" || - Config.mode === "quote" - ) { - let outof = TestLogic.words.length; - if (Config.mode === "words") { - outof = Config.words; - } - if (Config.mode === "custom") { - if (CustomText.isWordRandom) { - outof = CustomText.word; - } else { - outof = CustomText.text.length; - } - } - if (Config.mode === "quote") { - outof = TestLogic?.randomQuote?.textSplit?.length ?? 1; - } - if (Config.timerStyle === "bar") { - let percent = Math.floor( - ((TestLogic.words.currentIndex + 1) / outof) * 100 - ); - $("#timer") - .stop(true, true) - .animate( - { - width: percent + "vw", - }, - TestTimer.slowTimer ? 0 : 250 - ); - } else if (Config.timerStyle === "text") { - if (outof === 0) { - timerNumberElement.innerHTML = - "
" + `${TestLogic.input.history.length}` + "
"; - } else { - timerNumberElement.innerHTML = - "
" + `${TestLogic.input.history.length}/${outof}` + "
"; - } - } else if (Config.timerStyle === "mini") { - if (Config.words === 0) { - miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}`; - } else { - miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}/${outof}`; - } - } - } else if (Config.mode == "zen") { - if (Config.timerStyle === "text") { - timerNumberElement.innerHTML = - "
" + `${TestLogic.input.history.length}` + "
"; - } else { - miniTimerNumberElement.innerHTML = `${TestLogic.input.history.length}`; - } - } -} - -export function updateStyle() { - if (!TestLogic.active) return; - hide(); - update(); - setTimeout(() => { - show(); - }, 125); -} - -==> ./monkeytype/src/js/test/pb-crown.js <== -export function hide() { - $("#result .stats .wpm .crown").css("opacity", 0).addClass("hidden"); -} - -export function show() { - $("#result .stats .wpm .crown") - .removeClass("hidden") - .css("opacity", "0") - .animate( - { - opacity: 1, - }, - 250, - "easeOutCubic" - ); -} - -==> ./monkeytype/src/js/test/out-of-focus.js <== -import * as Misc from "./misc"; - -let outOfFocusTimeouts = []; - -export function hide() { - $("#words").css("transition", "none").removeClass("blurred"); - $(".outOfFocusWarning").addClass("hidden"); - Misc.clearTimeouts(outOfFocusTimeouts); -} - -export function show() { - outOfFocusTimeouts.push( - setTimeout(() => { - $("#words").css("transition", "0.25s").addClass("blurred"); - $(".outOfFocusWarning").removeClass("hidden"); - }, 1000) - ); -} - -==> ./monkeytype/src/js/test/result.js <== -import * as TestUI from "./test-ui"; -import Config from "./config"; -import * as Misc from "./misc"; -import * as TestStats from "./test-stats"; -import * as Keymap from "./keymap"; -import * as ChartController from "./chart-controller"; -import * as UI from "./ui"; -import * as ThemeColors from "./theme-colors"; -import * as DB from "./db"; -import * as TodayTracker from "./today-tracker"; -import * as PbCrown from "./pb-crown"; -import * as RateQuotePopup from "./rate-quote-popup"; -import * as TestLogic from "./test-logic"; -import * as Notifications from "./notifications"; - -let result; -let maxChartVal; - -let useUnsmoothedRaw = false; - -export function toggleUnsmoothedRaw() { - useUnsmoothedRaw = !useUnsmoothedRaw; - Notifications.add(useUnsmoothedRaw ? "on" : "off", 1); -} - -async function updateGraph() { - ChartController.result.options.annotation.annotations = []; - let labels = []; - for (let i = 1; i <= TestStats.wpmHistory.length; i++) { - if (TestStats.lastSecondNotRound && i === TestStats.wpmHistory.length) { - labels.push(Misc.roundTo2(result.testDuration).toString()); - } else { - labels.push(i.toString()); - } - } - ChartController.result.updateColors(); - ChartController.result.data.labels = labels; - ChartController.result.options.scales.yAxes[0].scaleLabel.labelString = Config.alwaysShowCPM - ? "Character per Minute" - : "Words per Minute"; - let chartData1 = Config.alwaysShowCPM - ? TestStats.wpmHistory.map((a) => a * 5) - : TestStats.wpmHistory; - - let chartData2; - - if (useUnsmoothedRaw) { - chartData2 = Config.alwaysShowCPM - ? result.chartData.unsmoothedRaw.map((a) => a * 5) - : result.chartData.unsmoothedRaw; - } else { - chartData2 = Config.alwaysShowCPM - ? result.chartData.raw.map((a) => a * 5) - : result.chartData.raw; - } - - ChartController.result.data.datasets[0].data = chartData1; - ChartController.result.data.datasets[1].data = chartData2; - - ChartController.result.data.datasets[0].label = Config.alwaysShowCPM - ? "cpm" - : "wpm"; - - maxChartVal = Math.max(...[Math.max(...chartData2), Math.max(...chartData1)]); - if (!Config.startGraphsAtZero) { - let minChartVal = Math.min( - ...[Math.min(...chartData2), Math.min(...chartData1)] - ); - ChartController.result.options.scales.yAxes[0].ticks.min = minChartVal; - ChartController.result.options.scales.yAxes[1].ticks.min = minChartVal; - } else { - ChartController.result.options.scales.yAxes[0].ticks.min = 0; - ChartController.result.options.scales.yAxes[1].ticks.min = 0; - } - - ChartController.result.data.datasets[2].data = result.chartData.err; - - let fc = await ThemeColors.get("sub"); - if (Config.funbox !== "none") { - let content = Config.funbox; - if (Config.funbox === "layoutfluid") { - content += " " + Config.customLayoutfluid.replace(/#/g, " "); - } - ChartController.result.options.annotation.annotations.push({ - enabled: false, - type: "line", - mode: "horizontal", - scaleID: "wpm", - value: 0, - borderColor: "transparent", - borderWidth: 1, - borderDash: [2, 2], - label: { - backgroundColor: "transparent", - fontFamily: Config.fontFamily.replace(/_/g, " "), - fontSize: 11, - fontStyle: "normal", - fontColor: fc, - xPadding: 6, - yPadding: 6, - cornerRadius: 3, - position: "left", - enabled: true, - content: `${content}`, - yAdjust: -11, - }, - }); - } - - ChartController.result.options.scales.yAxes[0].ticks.max = maxChartVal; - ChartController.result.options.scales.yAxes[1].ticks.max = maxChartVal; - - ChartController.result.update({ duration: 0 }); - ChartController.result.resize(); -} - -export async function updateGraphPBLine() { - let themecolors = await ThemeColors.get(); - let lpb = await DB.getLocalPB( - result.mode, - result.mode2, - result.punctuation, - result.language, - result.difficulty, - result.lazyMode, - result.funbox - ); - if (lpb == 0) return; - let chartlpb = Misc.roundTo2(Config.alwaysShowCPM ? lpb * 5 : lpb).toFixed(2); - ChartController.result.options.annotation.annotations.push({ - enabled: false, - type: "line", - mode: "horizontal", - scaleID: "wpm", - value: chartlpb, - borderColor: themecolors["sub"], - borderWidth: 1, - borderDash: [2, 2], - label: { - backgroundColor: themecolors["sub"], - fontFamily: Config.fontFamily.replace(/_/g, " "), - fontSize: 11, - fontStyle: "normal", - fontColor: themecolors["bg"], - xPadding: 6, - yPadding: 6, - cornerRadius: 3, - position: "center", - enabled: true, - content: `PB: ${chartlpb}`, - }, - }); - if ( - maxChartVal >= parseFloat(chartlpb) - 20 && - maxChartVal <= parseFloat(chartlpb) + 20 - ) { - maxChartVal = parseFloat(chartlpb) + 20; - } - ChartController.result.options.scales.yAxes[0].ticks.max = Math.round( - maxChartVal - ); - ChartController.result.options.scales.yAxes[1].ticks.max = Math.round( - maxChartVal - ); - ChartController.result.update({ duration: 0 }); -} - -function updateWpmAndAcc() { - let inf = false; - if (result.wpm >= 1000) { - inf = true; - } - if (Config.alwaysShowDecimalPlaces) { - if (Config.alwaysShowCPM == false) { - $("#result .stats .wpm .top .text").text("wpm"); - if (inf) { - $("#result .stats .wpm .bottom").text("Infinite"); - } else { - $("#result .stats .wpm .bottom").text( - Misc.roundTo2(result.wpm).toFixed(2) - ); - } - $("#result .stats .raw .bottom").text( - Misc.roundTo2(result.rawWpm).toFixed(2) - ); - $("#result .stats .wpm .bottom").attr( - "aria-label", - Misc.roundTo2(result.wpm * 5).toFixed(2) + " cpm" - ); - } else { - $("#result .stats .wpm .top .text").text("cpm"); - if (inf) { - $("#result .stats .wpm .bottom").text("Infinite"); - } else { - $("#result .stats .wpm .bottom").text( - Misc.roundTo2(result.wpm * 5).toFixed(2) - ); - } - $("#result .stats .raw .bottom").text( - Misc.roundTo2(result.rawWpm * 5).toFixed(2) - ); - $("#result .stats .wpm .bottom").attr( - "aria-label", - Misc.roundTo2(result.wpm).toFixed(2) + " wpm" - ); - } - - $("#result .stats .acc .bottom").text( - result.acc == 100 ? "100%" : Misc.roundTo2(result.acc).toFixed(2) + "%" - ); - let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; - if (result.testDuration > 61) { - time = Misc.secondsToString(Misc.roundTo2(result.testDuration)); - } - $("#result .stats .time .bottom .text").text(time); - $("#result .stats .raw .bottom").removeAttr("aria-label"); - $("#result .stats .acc .bottom").removeAttr("aria-label"); - } else { - //not showing decimal places - if (Config.alwaysShowCPM == false) { - $("#result .stats .wpm .top .text").text("wpm"); - $("#result .stats .wpm .bottom").attr( - "aria-label", - result.wpm + ` (${Misc.roundTo2(result.wpm * 5)} cpm)` - ); - if (inf) { - $("#result .stats .wpm .bottom").text("Infinite"); - } else { - $("#result .stats .wpm .bottom").text(Math.round(result.wpm)); - } - $("#result .stats .raw .bottom").text(Math.round(result.rawWpm)); - $("#result .stats .raw .bottom").attr("aria-label", result.rawWpm); - } else { - $("#result .stats .wpm .top .text").text("cpm"); - $("#result .stats .wpm .bottom").attr( - "aria-label", - Misc.roundTo2(result.wpm * 5) + ` (${Misc.roundTo2(result.wpm)} wpm)` - ); - if (inf) { - $("#result .stats .wpm .bottom").text("Infinite"); - } else { - $("#result .stats .wpm .bottom").text(Math.round(result.wpm * 5)); - } - $("#result .stats .raw .bottom").text(Math.round(result.rawWpm * 5)); - $("#result .stats .raw .bottom").attr("aria-label", result.rawWpm * 5); - } - - $("#result .stats .acc .bottom").text(Math.floor(result.acc) + "%"); - $("#result .stats .acc .bottom").attr("aria-label", result.acc + "%"); - } -} - -function updateConsistency() { - if (Config.alwaysShowDecimalPlaces) { - $("#result .stats .consistency .bottom").text( - Misc.roundTo2(result.consistency).toFixed(2) + "%" - ); - $("#result .stats .consistency .bottom").attr( - "aria-label", - `${result.keyConsistency.toFixed(2)}% key` - ); - } else { - $("#result .stats .consistency .bottom").text( - Math.round(result.consistency) + "%" - ); - $("#result .stats .consistency .bottom").attr( - "aria-label", - `${result.consistency}% (${result.keyConsistency}% key)` - ); - } -} - -function updateTime() { - let afkSecondsPercent = Misc.roundTo2( - (result.afkDuration / result.testDuration) * 100 - ); - $("#result .stats .time .bottom .afk").text(""); - if (afkSecondsPercent > 0) { - $("#result .stats .time .bottom .afk").text(afkSecondsPercent + "% afk"); - } - $("#result .stats .time .bottom").attr( - "aria-label", - `${result.afkDuration}s afk ${afkSecondsPercent}%` - ); - if (Config.alwaysShowDecimalPlaces) { - let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; - if (result.testDuration > 61) { - time = Misc.secondsToString(Misc.roundTo2(result.testDuration)); - } - $("#result .stats .time .bottom .text").text(time); - } else { - let time = Math.round(result.testDuration) + "s"; - if (result.testDuration > 61) { - time = Misc.secondsToString(Math.round(result.testDuration)); - } - $("#result .stats .time .bottom .text").text(time); - $("#result .stats .time .bottom").attr( - "aria-label", - `${Misc.roundTo2(result.testDuration)}s (${ - result.afkDuration - }s afk ${afkSecondsPercent}%)` - ); - } -} - -export function updateTodayTracker() { - $("#result .stats .time .bottom .timeToday").text(TodayTracker.getString()); -} - -function updateKey() { - $("#result .stats .key .bottom").text( - result.charStats[0] + - "/" + - result.charStats[1] + - "/" + - result.charStats[2] + - "/" + - result.charStats[3] - ); -} - -export function showCrown() { - PbCrown.show(); -} - -export function hideCrown() { - PbCrown.hide(); - $("#result .stats .wpm .crown").attr("aria-label", ""); -} - -export async function updateCrown() { - let pbDiff = 0; - const lpb = await DB.getLocalPB( - Config.mode, - result.mode2, - Config.punctuation, - Config.language, - Config.difficulty, - Config.lazyMode, - Config.funbox - ); - pbDiff = Math.abs(result.wpm - lpb); - $("#result .stats .wpm .crown").attr( - "aria-label", - "+" + Misc.roundTo2(pbDiff) - ); -} - -function updateTags(dontSave) { - let activeTags = []; - try { - DB.getSnapshot().tags.forEach((tag) => { - if (tag.active === true) { - activeTags.push(tag); - } - }); - } catch (e) {} - - $("#result .stats .tags").addClass("hidden"); - if (activeTags.length == 0) { - $("#result .stats .tags").addClass("hidden"); - } else { - $("#result .stats .tags").removeClass("hidden"); - } - $("#result .stats .tags .bottom").text(""); - let annotationSide = "left"; - let labelAdjust = 15; - activeTags.forEach(async (tag) => { - let tpb = await DB.getLocalTagPB( - tag._id, - Config.mode, - result.mode2, - Config.punctuation, - Config.language, - Config.difficulty, - Config.lazyMode - ); - $("#result .stats .tags .bottom").append(` -
${tag.name}
- `); - if (Config.mode != "quote" && !dontSave) { - if (tpb < result.wpm) { - //new pb for that tag - DB.saveLocalTagPB( - tag._id, - Config.mode, - result.mode2, - Config.punctuation, - Config.language, - Config.difficulty, - Config.lazyMode, - result.wpm, - result.acc, - result.rawWpm, - result.consistency - ); - $( - `#result .stats .tags .bottom div[tagid="${tag._id}"] .fas` - ).removeClass("hidden"); - $(`#result .stats .tags .bottom div[tagid="${tag._id}"]`).attr( - "aria-label", - "+" + Misc.roundTo2(result.wpm - tpb) - ); - // console.log("new pb for tag " + tag.name); - } else { - let themecolors = await ThemeColors.get(); - ChartController.result.options.annotation.annotations.push({ - enabled: false, - type: "line", - mode: "horizontal", - scaleID: "wpm", - value: Config.alwaysShowCPM ? tpb * 5 : tpb, - borderColor: themecolors["sub"], - borderWidth: 1, - borderDash: [2, 2], - label: { - backgroundColor: themecolors["sub"], - fontFamily: Config.fontFamily.replace(/_/g, " "), - fontSize: 11, - fontStyle: "normal", - fontColor: themecolors["bg"], - xPadding: 6, - yPadding: 6, - cornerRadius: 3, - position: annotationSide, - xAdjust: labelAdjust, - enabled: true, - content: `${tag.name} PB: ${Misc.roundTo2( - Config.alwaysShowCPM ? tpb * 5 : tpb - ).toFixed(2)}`, - }, - }); - if (annotationSide === "left") { - annotationSide = "right"; - labelAdjust = -15; - } else { - annotationSide = "left"; - labelAdjust = 15; - } - } - } - }); -} - -function updateTestType() { - let testType = ""; - - if (Config.mode === "quote") { - let qlen = ""; - if (Config.quoteLength === 0) { - qlen = "short "; - } else if (Config.quoteLength === 1) { - qlen = "medium "; - } else if (Config.quoteLength === 2) { - qlen = "long "; - } else if (Config.quoteLength === 3) { - qlen = "thicc "; - } - testType += qlen + Config.mode; - } else { - testType += Config.mode; - } - if (Config.mode == "time") { - testType += " " + Config.time; - } else if (Config.mode == "words") { - testType += " " + Config.words; - } - if ( - Config.mode != "custom" && - Config.funbox !== "gibberish" && - Config.funbox !== "ascii" && - Config.funbox !== "58008" - ) { - testType += "
" + result.language.replace(/_/g, " "); - } - if (Config.punctuation) { - testType += "
punctuation"; - } - if (Config.numbers) { - testType += "
numbers"; - } - if (Config.blindMode) { - testType += "
blind"; - } - if (Config.lazyMode) { - testType += "
lazy"; - } - if (Config.funbox !== "none") { - testType += "
" + Config.funbox.replace(/_/g, " "); - } - if (Config.difficulty == "expert") { - testType += "
expert"; - } else if (Config.difficulty == "master") { - testType += "
master"; - } - - $("#result .stats .testType .bottom").html(testType); -} - -function updateOther( - difficultyFailed, - failReason, - afkDetected, - isRepeated, - tooShort -) { - let otherText = ""; - if (difficultyFailed) { - otherText += `
failed (${failReason})`; - } - if (afkDetected) { - otherText += "
afk detected"; - } - if (TestStats.invalid) { - otherText += "
invalid"; - let extra = ""; - if (result.wpm < 0 || result.wpm > 350) { - extra += "wpm"; - } - if (result.acc < 75 || result.acc > 100) { - if (extra.length > 0) { - extra += ", "; - } - extra += "accuracy"; - } - if (extra.length > 0) { - otherText += ` (${extra})`; - } - } - if (isRepeated) { - otherText += "
repeated"; - } - if (result.bailedOut) { - otherText += "
bailed out"; - } - if (tooShort) { - otherText += "
too short"; - } - - if (otherText == "") { - $("#result .stats .info").addClass("hidden"); - } else { - $("#result .stats .info").removeClass("hidden"); - otherText = otherText.substring(4); - $("#result .stats .info .bottom").html(otherText); - } -} - -export function updateRateQuote(randomQuote) { - if (Config.mode === "quote") { - let userqr = DB.getSnapshot().quoteRatings?.[randomQuote.language]?.[ - randomQuote.id - ]; - if (userqr) { - $(".pageTest #result #rateQuoteButton .icon") - .removeClass("far") - .addClass("fas"); - } - RateQuotePopup.getQuoteStats(randomQuote).then((quoteStats) => { - if (quoteStats !== null) { - $(".pageTest #result #rateQuoteButton .rating").text( - quoteStats.average - ); - } - $(".pageTest #result #rateQuoteButton") - .css({ opacity: 0 }) - .removeClass("hidden") - .css({ opacity: 1 }); - }); - } -} - -function updateQuoteSource(randomQuote) { - if (Config.mode === "quote") { - $("#result .stats .source").removeClass("hidden"); - $("#result .stats .source .bottom").html(randomQuote.source); - } else { - $("#result .stats .source").addClass("hidden"); - } -} - -export function update( - res, - difficultyFailed, - failReason, - afkDetected, - isRepeated, - tooShort, - randomQuote, - dontSave -) { - result = res; - $("#result #resultWordsHistory").addClass("hidden"); - $("#retrySavingResultButton").addClass("hidden"); - $(".pageTest #result #rateQuoteButton .icon") - .removeClass("fas") - .addClass("far"); - $(".pageTest #result #rateQuoteButton .rating").text(""); - $(".pageTest #result #rateQuoteButton").addClass("hidden"); - $("#testModesNotice").css("opacity", 0); - $("#words").removeClass("blurred"); - $("#wordsInput").blur(); - $("#result .stats .time .bottom .afk").text(""); - if (firebase.auth().currentUser != null) { - $("#result .loginTip").addClass("hidden"); - } else { - $("#result .loginTip").removeClass("hidden"); - } - updateWpmAndAcc(); - updateConsistency(); - updateTime(); - updateKey(); - updateTestType(); - updateQuoteSource(randomQuote); - updateGraph(); - updateGraphPBLine(); - updateTags(dontSave); - updateOther(difficultyFailed, failReason, afkDetected, isRepeated, tooShort); - - if ( - $("#result .stats .tags").hasClass("hidden") && - $("#result .stats .info").hasClass("hidden") - ) { - $("#result .stats .infoAndTags").addClass("hidden"); - } else { - $("#result .stats .infoAndTags").removeClass("hidden"); - } - - if (TestLogic.glarsesMode) { - $("#middle #result .noStressMessage").remove(); - $("#middle #result").prepend(` - -
- -
- - `); - $("#middle #result .stats").addClass("hidden"); - $("#middle #result .chart").addClass("hidden"); - $("#middle #result #resultWordsHistory").addClass("hidden"); - $("#middle #result #resultReplay").addClass("hidden"); - $("#middle #result .loginTip").addClass("hidden"); - $("#middle #result #showWordHistoryButton").addClass("hidden"); - $("#middle #result #watchReplayButton").addClass("hidden"); - $("#middle #result #saveScreenshotButton").addClass("hidden"); - - console.log( - `Test Completed: ${result.wpm} wpm ${result.acc}% acc ${result.rawWpm} raw ${result.consistency}% consistency` - ); - } else { - $("#middle #result .stats").removeClass("hidden"); - $("#middle #result .chart").removeClass("hidden"); - // $("#middle #result #resultWordsHistory").removeClass("hidden"); - if (firebase.auth().currentUser == null) { - $("#middle #result .loginTip").removeClass("hidden"); - } - $("#middle #result #showWordHistoryButton").removeClass("hidden"); - $("#middle #result #watchReplayButton").removeClass("hidden"); - $("#middle #result #saveScreenshotButton").removeClass("hidden"); - } - - if (window.scrollY > 0) - $([document.documentElement, document.body]) - .stop() - .animate({ scrollTop: 0 }, 250); - - UI.swapElements( - $("#typingTest"), - $("#result"), - 250, - () => { - TestUI.setResultCalculating(false); - $("#words").empty(); - ChartController.result.resize(); - - if (Config.alwaysShowWordsHistory && Config.burstHeatmap) { - TestUI.applyBurstHeatmap(); - } - $("#result").focus(); - window.scrollTo({ top: 0 }); - $("#testModesNotice").addClass("hidden"); - }, - () => { - $("#resultExtraButtons").removeClass("hidden").css("opacity", 0).animate( - { - opacity: 1, - }, - 125 - ); - if (Config.alwaysShowWordsHistory && !TestLogic.glarsesMode) { - TestUI.toggleResultWords(); - } - Keymap.hide(); - } - ); -} - -==> ./monkeytype/src/js/test/test-config.js <== -import * as CustomWordAmountPopup from "./custom-word-amount-popup"; -import * as CustomTestDurationPopup from "./custom-test-duration-popup"; -import * as UpdateConfig from "./config"; -import * as ManualRestart from "./manual-restart-tracker"; -import * as TestLogic from "./test-logic"; -import * as QuoteSearchPopup from "./quote-search-popup"; -import * as CustomTextPopup from "./custom-text-popup"; -import * as UI from "./ui"; - -// export function show() { -// $("#top .config").removeClass("hidden").css("opacity", 1); -// } - -// export function hide() { -// $("#top .config").css("opacity", 0).addClass("hidden"); -// } - -export function show() { - $("#top .config") - .stop(true, true) - .removeClass("hidden") - .css("opacity", 0) - .animate( - { - opacity: 1, - }, - 125 - ); -} - -export function hide() { - $("#top .config") - .stop(true, true) - .css("opacity", 1) - .animate( - { - opacity: 0, - }, - 125, - () => { - $("#top .config").addClass("hidden"); - } - ); -} - -export function update(previous, current) { - if (previous == current) return; - $("#top .config .mode .text-button").removeClass("active"); - $("#top .config .mode .text-button[mode='" + current + "']").addClass( - "active" - ); - if (current == "time") { - // $("#top .config .wordCount").addClass("hidden"); - // $("#top .config .time").removeClass("hidden"); - // $("#top .config .customText").addClass("hidden"); - $("#top .config .punctuationMode").removeClass("disabled"); - $("#top .config .numbersMode").removeClass("disabled"); - // $("#top .config .puncAndNum").removeClass("disabled"); - // $("#top .config .punctuationMode").removeClass("hidden"); - // $("#top .config .numbersMode").removeClass("hidden"); - // $("#top .config .quoteLength").addClass("hidden"); - } else if (current == "words") { - // $("#top .config .wordCount").removeClass("hidden"); - // $("#top .config .time").addClass("hidden"); - // $("#top .config .customText").addClass("hidden"); - $("#top .config .punctuationMode").removeClass("disabled"); - $("#top .config .numbersMode").removeClass("disabled"); - // $("#top .config .puncAndNum").removeClass("disabled"); - // $("#top .config .punctuationMode").removeClass("hidden"); - // $("#top .config .numbersMode").removeClass("hidden"); - // $("#top .config .quoteLength").addClass("hidden"); - } else if (current == "custom") { - // $("#top .config .wordCount").addClass("hidden"); - // $("#top .config .time").addClass("hidden"); - // $("#top .config .customText").removeClass("hidden"); - $("#top .config .punctuationMode").removeClass("disabled"); - $("#top .config .numbersMode").removeClass("disabled"); - // $("#top .config .puncAndNum").removeClass("disabled"); - // $("#top .config .punctuationMode").removeClass("hidden"); - // $("#top .config .numbersMode").removeClass("hidden"); - // $("#top .config .quoteLength").addClass("hidden"); - } else if (current == "quote") { - // $("#top .config .wordCount").addClass("hidden"); - // $("#top .config .time").addClass("hidden"); - // $("#top .config .customText").addClass("hidden"); - $("#top .config .punctuationMode").addClass("disabled"); - $("#top .config .numbersMode").addClass("disabled"); - // $("#top .config .puncAndNum").addClass("disabled"); - // $("#top .config .punctuationMode").removeClass("hidden"); - // $("#top .config .numbersMode").removeClass("hidden"); - // $("#result .stats .source").removeClass("hidden"); - // $("#top .config .quoteLength").removeClass("hidden"); - } else if (current == "zen") { - // $("#top .config .wordCount").addClass("hidden"); - // $("#top .config .time").addClass("hidden"); - // $("#top .config .customText").addClass("hidden"); - // $("#top .config .punctuationMode").addClass("hidden"); - // $("#top .config .numbersMode").addClass("hidden"); - // $("#top .config .quoteLength").addClass("hidden"); - } - - let submenu = { - time: "time", - words: "wordCount", - custom: "customText", - quote: "quoteLength", - zen: "", - }; - - let animTime = 250; - - if (current == "zen") { - $(`#top .config .${submenu[previous]}`).animate( - { - opacity: 0, - }, - animTime / 2, - () => { - $(`#top .config .${submenu[previous]}`).addClass("hidden"); - } - ); - $(`#top .config .puncAndNum`).animate( - { - opacity: 0, - }, - animTime / 2, - () => { - $(`#top .config .puncAndNum`).addClass("invisible"); - } - ); - return; - } - - if (previous == "zen") { - setTimeout(() => { - $(`#top .config .${submenu[current]}`).removeClass("hidden"); - $(`#top .config .${submenu[current]}`) - .css({ opacity: 0 }) - .animate( - { - opacity: 1, - }, - animTime / 2 - ); - $(`#top .config .puncAndNum`).removeClass("invisible"); - $(`#top .config .puncAndNum`) - .css({ opacity: 0 }) - .animate( - { - opacity: 1, - }, - animTime / 2 - ); - }, animTime / 2); - return; - } - - UI.swapElements( - $("#top .config ." + submenu[previous]), - $("#top .config ." + submenu[current]), - animTime - ); -} - -$(document).on("click", "#top .config .wordCount .text-button", (e) => { - const wrd = $(e.currentTarget).attr("wordCount"); - if (wrd == "custom") { - CustomWordAmountPopup.show(); - } else { - UpdateConfig.setWordCount(wrd); - ManualRestart.set(); - TestLogic.restart(); - } -}); - -$(document).on("click", "#top .config .time .text-button", (e) => { - let mode = $(e.currentTarget).attr("timeConfig"); - if (mode == "custom") { - CustomTestDurationPopup.show(); - } else { - UpdateConfig.setTimeConfig(mode); - ManualRestart.set(); - TestLogic.restart(); - } -}); - -$(document).on("click", "#top .config .quoteLength .text-button", (e) => { - let len = $(e.currentTarget).attr("quoteLength"); - if (len == -2) { - // UpdateConfig.setQuoteLength(-2, false, e.shiftKey); - QuoteSearchPopup.show(); - } else { - if (len == -1) { - len = [0, 1, 2, 3]; - } - UpdateConfig.setQuoteLength(len, false, e.shiftKey); - ManualRestart.set(); - TestLogic.restart(); - } -}); - -$(document).on("click", "#top .config .customText .text-button", () => { - CustomTextPopup.show(); -}); - -$(document).on("click", "#top .config .punctuationMode .text-button", () => { - UpdateConfig.togglePunctuation(); - ManualRestart.set(); - TestLogic.restart(); -}); - -$(document).on("click", "#top .config .numbersMode .text-button", () => { - UpdateConfig.toggleNumbers(); - ManualRestart.set(); - TestLogic.restart(); -}); - -$(document).on("click", "#top .config .mode .text-button", (e) => { - if ($(e.currentTarget).hasClass("active")) return; - const mode = $(e.currentTarget).attr("mode"); - UpdateConfig.setMode(mode); - ManualRestart.set(); - TestLogic.restart(); -}); - -==> ./monkeytype/src/js/test/practise-words.js <== -import * as TestStats from "./test-stats"; -import * as Notifications from "./notifications"; -import Config, * as UpdateConfig from "./config"; -import * as CustomText from "./custom-text"; -import * as TestLogic from "./test-logic"; - -export let before = { - mode: null, - punctuation: null, - numbers: null, -}; - -export function init(missed, slow) { - if (Config.mode === "zen") return; - let limit; - if ((missed && !slow) || (!missed && slow)) { - limit = 20; - } else if (missed && slow) { - limit = 10; - } - - let sortableMissedWords = []; - if (missed) { - Object.keys(TestStats.missedWords).forEach((missedWord) => { - sortableMissedWords.push([missedWord, TestStats.missedWords[missedWord]]); - }); - sortableMissedWords.sort((a, b) => { - return b[1] - a[1]; - }); - sortableMissedWords = sortableMissedWords.slice(0, limit); - } - - if (missed && !slow && sortableMissedWords.length == 0) { - Notifications.add("You haven't missed any words", 0); - return; - } - - let sortableSlowWords = []; - if (slow) { - sortableSlowWords = TestLogic.words.get().map(function (e, i) { - return [e, TestStats.burstHistory[i]]; - }); - sortableSlowWords.sort((a, b) => { - return a[1] - b[1]; - }); - sortableSlowWords = sortableSlowWords.slice( - 0, - Math.min(limit, Math.round(TestLogic.words.length * 0.2)) - ); - } - - // console.log(sortableMissedWords); - // console.log(sortableSlowWords); - - if (sortableMissedWords.length == 0 && sortableSlowWords.length == 0) { - Notifications.add("Could not start a new custom test", 0); - return; - } - - let newCustomText = []; - sortableMissedWords.forEach((missed, index) => { - for (let i = 0; i < missed[1]; i++) { - newCustomText.push(missed[0]); - } - }); - - sortableSlowWords.forEach((slow, index) => { - for (let i = 0; i < sortableSlowWords.length - index; i++) { - newCustomText.push(slow[0]); - } - }); - - // console.log(newCustomText); - - let mode = before.mode === null ? Config.mode : before.mode; - let punctuation = - before.punctuation === null ? Config.punctuation : before.punctuation; - let numbers = before.numbers === null ? Config.numbers : before.numbers; - UpdateConfig.setMode("custom"); - - CustomText.setText(newCustomText); - CustomText.setIsWordRandom(true); - CustomText.setWord( - (sortableSlowWords.length + sortableMissedWords.length) * 5 - ); - - TestLogic.restart(false, false, false, true); - before.mode = mode; - before.punctuation = punctuation; - before.numbers = numbers; -} - -export function resetBefore() { - before.mode = null; - before.punctuation = null; - before.numbers = null; -} - -export function showPopup(focus = false) { - if ($("#practiseWordsPopupWrapper").hasClass("hidden")) { - if (Config.mode === "zen") { - Notifications.add("Practice words is unsupported in zen mode", 0); - return; - } - $("#practiseWordsPopupWrapper") - .stop(true, true) - .css("opacity", 0) - .removeClass("hidden") - .animate({ opacity: 1 }, 100, () => { - if (focus) { - console.log("focusing"); - $("#practiseWordsPopup .missed").focus(); - } - }); - } -} - -function hidePopup() { - if (!$("#practiseWordsPopupWrapper").hasClass("hidden")) { - $("#practiseWordsPopupWrapper") - .stop(true, true) - .css("opacity", 1) - .animate( - { - opacity: 0, - }, - 100, - (e) => { - $("#practiseWordsPopupWrapper").addClass("hidden"); - } - ); - } -} - -$("#practiseWordsPopupWrapper").click((e) => { - if ($(e.target).attr("id") === "practiseWordsPopupWrapper") { - hidePopup(); - } -}); - -$("#practiseWordsPopup .button.missed").click(() => { - hidePopup(); - init(true, false); -}); - -$("#practiseWordsPopup .button.slow").click(() => { - hidePopup(); - init(false, true); -}); - -$("#practiseWordsPopup .button.both").click(() => { - hidePopup(); - init(true, true); -}); - -$("#practiseWordsPopup .button").keypress((e) => { - if (e.key == "Enter") { - $(e.currentTarget).click(); - } -}); - -$("#practiseWordsPopup .button.both").on("focusout", (e) => { - e.preventDefault(); - $("#practiseWordsPopup .missed").focus(); -}); - -==> ./monkeytype/src/js/test/test-stats.js <== -import * as TestLogic from "./test-logic"; -import Config from "./config"; -import * as Misc from "./misc"; -import * as TestStats from "./test-stats"; - -export let invalid = false; -export let start, end; -export let start2, end2; -export let wpmHistory = []; -export let rawHistory = []; -export let burstHistory = []; - -export let keypressPerSecond = []; -export let currentKeypress = { - count: 0, - errors: 0, - words: [], - afk: true, -}; -export let lastKeypress; -export let currentBurstStart = 0; - -// export let errorsPerSecond = []; -// export let currentError = { -// count: 0, -// words: [], -// }; -export let lastSecondNotRound = false; -export let missedWords = {}; -export let accuracy = { - correct: 0, - incorrect: 0, -}; -export let keypressTimings = { - spacing: { - current: -1, - array: [], - }, - duration: { - current: -1, - array: [], - }, -}; - -export function getStats() { - let ret = { - start, - end, - wpmHistory, - rawHistory, - burstHistory, - keypressPerSecond, - currentKeypress, - lastKeypress, - currentBurstStart, - lastSecondNotRound, - missedWords, - accuracy, - keypressTimings, - }; - - try { - ret.keySpacingStats = { - average: - keypressTimings.spacing.array.reduce( - (previous, current) => (current += previous) - ) / keypressTimings.spacing.array.length, - sd: Misc.stdDev(keypressTimings.spacing.array), - }; - } catch (e) { - // - } - try { - ret.keyDurationStats = { - average: - keypressTimings.duration.array.reduce( - (previous, current) => (current += previous) - ) / keypressTimings.duration.array.length, - sd: Misc.stdDev(keypressTimings.duration.array), - }; - } catch (e) { - // - } - - return ret; -} - -export function restart() { - start = 0; - end = 0; - invalid = false; - wpmHistory = []; - rawHistory = []; - burstHistory = []; - keypressPerSecond = []; - currentKeypress = { - count: 0, - errors: 0, - words: [], - afk: true, - }; - currentBurstStart = 0; - // errorsPerSecond = []; - // currentError = { - // count: 0, - // words: [], - // }; - lastSecondNotRound = false; - missedWords = {}; - accuracy = { - correct: 0, - incorrect: 0, - }; - keypressTimings = { - spacing: { - current: -1, - array: [], - }, - duration: { - current: -1, - array: [], - }, - }; -} - -export let restartCount = 0; -export let incompleteSeconds = 0; - -export function incrementRestartCount() { - restartCount++; -} - -export function incrementIncompleteSeconds(val) { - incompleteSeconds += val; -} - -export function resetIncomplete() { - restartCount = 0; - incompleteSeconds = 0; -} - -export function setInvalid() { - invalid = true; -} - -export function calculateTestSeconds(now) { - if (now === undefined) { - let endAfkSeconds = (end - lastKeypress) / 1000; - if ((Config.mode == "zen" || TestLogic.bailout) && endAfkSeconds < 7) { - return (lastKeypress - start) / 1000; - } else { - return (end - start) / 1000; - } - } else { - return (now - start) / 1000; - } -} - -export function setEnd(e) { - end = e; - end2 = Date.now(); -} - -export function setStart(s) { - start = s; - start2 = Date.now(); -} - -export function updateLastKeypress() { - lastKeypress = performance.now(); -} - -export function pushToWpmHistory(word) { - wpmHistory.push(word); -} - -export function pushToRawHistory(word) { - rawHistory.push(word); -} - -export function incrementKeypressCount() { - currentKeypress.count++; -} - -export function setKeypressNotAfk() { - currentKeypress.afk = false; -} - -export function incrementKeypressErrors() { - currentKeypress.errors++; -} - -export function pushKeypressWord(word) { - currentKeypress.words.push(word); -} - -export function pushKeypressesToHistory() { - keypressPerSecond.push(currentKeypress); - currentKeypress = { - count: 0, - errors: 0, - words: [], - afk: true, - }; -} - -export function calculateAfkSeconds(testSeconds) { - let extraAfk = 0; - if (testSeconds !== undefined) { - if (Config.mode === "time") { - extraAfk = Math.round(testSeconds) - keypressPerSecond.length; - } else { - extraAfk = Math.ceil(testSeconds) - keypressPerSecond.length; - } - if (extraAfk < 0) extraAfk = 0; - // console.log("-- extra afk debug"); - // console.log("should be " + Math.ceil(testSeconds)); - // console.log(keypressPerSecond.length); - // console.log( - // `gonna add extra ${extraAfk} seconds of afk because of no keypress data` - // ); - } - let ret = keypressPerSecond.filter((x) => x.afk).length; - return ret + extraAfk; -} - -export function setLastSecondNotRound() { - lastSecondNotRound = true; -} - -export function setBurstStart(time) { - currentBurstStart = time; -} - -export function calculateBurst() { - let timeToWrite = (performance.now() - currentBurstStart) / 1000; - let wordLength; - if (Config.mode === "zen") { - wordLength = TestLogic.input.current.length; - if (wordLength == 0) { - wordLength = TestLogic.input.getHistoryLast().length; - } - } else { - wordLength = TestLogic.words.getCurrent().length; - } - let speed = Misc.roundTo2((wordLength * (60 / timeToWrite)) / 5); - return Math.round(speed); -} - -export function pushBurstToHistory(speed) { - if (burstHistory[TestLogic.words.currentIndex] === undefined) { - burstHistory.push(speed); - } else { - //repeated word - override - burstHistory[TestLogic.words.currentIndex] = speed; - } -} - -export function calculateAccuracy() { - let acc = (accuracy.correct / (accuracy.correct + accuracy.incorrect)) * 100; - return isNaN(acc) ? 100 : acc; -} - -export function incrementAccuracy(correctincorrect) { - if (correctincorrect) { - accuracy.correct++; - } else { - accuracy.incorrect++; - } -} - -export function setKeypressTimingsTooLong() { - keypressTimings.spacing.array = "toolong"; - keypressTimings.duration.array = "toolong"; -} - -export function pushKeypressDuration(val) { - keypressTimings.duration.array.push(val); -} - -export function setKeypressDuration(val) { - keypressTimings.duration.current = val; -} - -export function pushKeypressSpacing(val) { - keypressTimings.spacing.array.push(val); -} - -export function setKeypressSpacing(val) { - keypressTimings.spacing.current = val; -} - -export function recordKeypressSpacing() { - let now = performance.now(); - let diff = Math.abs(keypressTimings.spacing.current - now); - if (keypressTimings.spacing.current !== -1) { - pushKeypressSpacing(diff); - } - setKeypressSpacing(now); -} - -export function resetKeypressTimings() { - keypressTimings = { - spacing: { - current: performance.now(), - array: [], - }, - duration: { - current: performance.now(), - array: [], - }, - }; -} - -export function pushMissedWord(word) { - if (!Object.keys(missedWords).includes(word)) { - missedWords[word] = 1; - } else { - missedWords[word]++; - } -} - -export function removeAfkData() { - let testSeconds = calculateTestSeconds(); - keypressPerSecond.splice(testSeconds); - keypressTimings.duration.array.splice(testSeconds); - keypressTimings.spacing.array.splice(testSeconds); - wpmHistory.splice(testSeconds); -} - -function countChars() { - let correctWordChars = 0; - let correctChars = 0; - let incorrectChars = 0; - let extraChars = 0; - let missedChars = 0; - let spaces = 0; - let correctspaces = 0; - for (let i = 0; i < TestLogic.input.history.length; i++) { - let word = - Config.mode == "zen" - ? TestLogic.input.getHistory(i) - : TestLogic.words.get(i); - if (TestLogic.input.getHistory(i) === "") { - //last word that was not started - continue; - } - if (TestLogic.input.getHistory(i) == word) { - //the word is correct - correctWordChars += word.length; - correctChars += word.length; - if ( - i < TestLogic.input.history.length - 1 && - Misc.getLastChar(TestLogic.input.getHistory(i)) !== "\n" - ) { - correctspaces++; - } - } else if (TestLogic.input.getHistory(i).length >= word.length) { - //too many chars - for (let c = 0; c < TestLogic.input.getHistory(i).length; c++) { - if (c < word.length) { - //on char that still has a word list pair - if (TestLogic.input.getHistory(i)[c] == word[c]) { - correctChars++; - } else { - incorrectChars++; - } - } else { - //on char that is extra - extraChars++; - } - } - } else { - //not enough chars - let toAdd = { - correct: 0, - incorrect: 0, - missed: 0, - }; - for (let c = 0; c < word.length; c++) { - if (c < TestLogic.input.getHistory(i).length) { - //on char that still has a word list pair - if (TestLogic.input.getHistory(i)[c] == word[c]) { - toAdd.correct++; - } else { - toAdd.incorrect++; - } - } else { - //on char that is extra - toAdd.missed++; - } - } - correctChars += toAdd.correct; - incorrectChars += toAdd.incorrect; - if (i === TestLogic.input.history.length - 1 && Config.mode == "time") { - //last word - check if it was all correct - add to correct word chars - if (toAdd.incorrect === 0) correctWordChars += toAdd.correct; - } else { - missedChars += toAdd.missed; - } - } - if (i < TestLogic.input.history.length - 1) { - spaces++; - } - } - if (Config.funbox === "nospace" || Config.funbox === "arrows") { - spaces = 0; - correctspaces = 0; - } - return { - spaces: spaces, - correctWordChars: correctWordChars, - allCorrectChars: correctChars, - incorrectChars: - Config.mode == "zen" ? TestStats.accuracy.incorrect : incorrectChars, - extraChars: extraChars, - missedChars: missedChars, - correctSpaces: correctspaces, - }; -} - -export function calculateStats() { - let testSeconds = TestStats.calculateTestSeconds(); - console.log((TestStats.end2 - TestStats.start2) / 1000); - console.log(testSeconds); - if (Config.mode != "custom") { - testSeconds = Misc.roundTo2(testSeconds); - } - let chars = countChars(); - let wpm = Misc.roundTo2( - ((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5 - ); - let wpmraw = Misc.roundTo2( - ((chars.allCorrectChars + - chars.spaces + - chars.incorrectChars + - chars.extraChars) * - (60 / testSeconds)) / - 5 - ); - let acc = Misc.roundTo2(TestStats.calculateAccuracy()); - return { - wpm: isNaN(wpm) ? 0 : wpm, - wpmRaw: isNaN(wpmraw) ? 0 : wpmraw, - acc: acc, - correctChars: chars.correctWordChars, - incorrectChars: chars.incorrectChars, - missedChars: chars.missedChars, - extraChars: chars.extraChars, - allChars: - chars.allCorrectChars + - chars.spaces + - chars.incorrectChars + - chars.extraChars, - time: testSeconds, - spaces: chars.spaces, - correctSpaces: chars.correctSpaces, - }; -} + +==> ./monkeytype/README.md <== +[![](https://github.com/Miodec/monkeytype/blob/master/static/images/githubbanner2.png?raw=true)](https://monkeytype.com/) +
+ +JavaScript +HTML5 +CSS3 +CSS3 +
+ +# About + +Monkeytype is a minimalistic and customizable typing test. It features many test modes, an account system to save your typing speed history, and user-configurable features like themes, sounds, a smooth caret, and more. + +# Features + +- minimalistic design with no ads +- look at what you are typing +- focus mode +- different test modes +- punctuation mode +- themes +- quotes +- live wpm +- smooth caret +- account system +- command line +- and much more + +# Discord bot + +On the [Monkeytype Discord server](https://www.discord.gg/monkeytype), we added a Discord bot to auto-assign roles on our server. You can find its code over at https://github.com/Miodec/monkey-bot + +# Bug report or Feature request + +If you encounter a bug or have a feature request, [send me a message on Reddit](https://reddit.com/user/miodec), [create an issue](https://github.com/Miodec/monkeytype/issues), [create a discussion thread](https://github.com/Miodec/monkeytype/discussions), or [join the Discord server](https://www.discord.gg/monkeytype). + +# Want to Contribute? + +Refer to [CONTRIBUTING.md.](https://github.com/Miodec/monkeytype/blob/master/CONTRIBUTING.md) + +# Code of Conduct + +Before contributing to this repository, please read the [code of conduct.](https://github.com/Miodec/monkeytype/blob/master/CODE_OF_CONDUCT.md) + +# Credits + +[Montydrei](https://www.reddit.com/user/montydrei) for the name suggestion. + +Everyone who provided valuable feedback on the [original Reddit post](https://www.reddit.com/r/MechanicalKeyboards/comments/gc6wx3/experimenting_with_a_completely_new_type_of/) for the prototype of this website. + +All of the [contributors](https://github.com/Miodec/monkeytype/graphs/contributors) that have helped with implementing various features, adding themes, fixing bugs, and more. + +# Support + +If you wish to support further development and feel extra awesome, you can [donate](https://ko-fi.com/monkeytype), [become a Patron](https://www.patreon.com/monkeytype) or [buy a t-shirt](https://www.monkeytype.store/). + +==> ./monkeytype/.npmrc <== +engine-strict=true + +==> ./monkeytype/backend/example.env <== +DB_NAME=monkeytype +DB_URI=mongodb://localhost:27017 +MODE=dev +# You can also use the format mongodb://username:password@host:port or +# uncomment the following lines if you want to define them separately +# DB_USERNAME= +# DB_PASSWORD= +# DB_AUTH_MECHANISM="SCRAM-SHA-256" +# DB_AUTH_SOURCE=admin + +==> ./monkeytype/backend/init/mongodb.js <== +const { MongoClient } = require("mongodb"); + +let mongoClient; + +module.exports = { + async connectDB() { + let options = { + useNewUrlParser: true, + useUnifiedTopology: true, + connectTimeoutMS: 2000, + serverSelectionTimeoutMS: 2000, + }; + + if (process.env.DB_USERNAME && process.env.DB_PASSWORD) { + options.auth = { + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + }; + } + + if (process.env.DB_AUTH_MECHANISM) { + options.authMechanism = process.env.DB_AUTH_MECHANISM; + } + + if (process.env.DB_AUTH_SOURCE) { + options.authSource = process.env.DB_AUTH_SOURCE; + } + + return MongoClient.connect(process.env.DB_URI, options) + .then((client) => { + mongoClient = client; + }) + .catch((e) => { + console.error(e.message); + console.error("FAILED TO CONNECT TO DATABASE. EXITING..."); + process.exit(1); + }); + }, + mongoDB() { + return mongoClient.db(process.env.DB_NAME); + }, +}; + +==> ./monkeytype/backend/server.js <== +const express = require("express"); +const { config } = require("dotenv"); +const path = require("path"); +const MonkeyError = require("./handlers/error"); +config({ path: path.join(__dirname, ".env") }); +const cors = require("cors"); +const admin = require("firebase-admin"); +const Logger = require("./handlers/logger.js"); +const serviceAccount = require("./credentials/serviceAccountKey.json"); +const { connectDB, mongoDB } = require("./init/mongodb"); +const jobs = require("./jobs"); +const addApiRoutes = require("./api/routes"); + +const PORT = process.env.PORT || 5005; + +// MIDDLEWARE & SETUP +const app = express(); +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); +app.use(cors()); + +app.set("trust proxy", 1); + +app.use((req, res, next) => { + if (process.env.MAINTENANCE === "true") { + res.status(503).json({ message: "Server is down for maintenance" }); + } else { + next(); + } +}); + +addApiRoutes(app); + +//DO NOT REMOVE NEXT, EVERYTHING WILL EXPLODE +app.use(function (e, req, res, next) { + if (/ECONNREFUSED.*27017/i.test(e.message)) { + e.message = "Could not connect to the database. It may have crashed."; + delete e.stack; + } + + let monkeyError; + if (e.errorID) { + //its a monkey error + monkeyError = e; + } else { + //its a server error + monkeyError = new MonkeyError(e.status, e.message, e.stack); + } + if (!monkeyError.uid && req.decodedToken) { + monkeyError.uid = req.decodedToken.uid; + } + if (process.env.MODE !== "dev" && monkeyError.status > 400) { + Logger.log( + "system_error", + `${monkeyError.status} ${monkeyError.message}`, + monkeyError.uid + ); + mongoDB().collection("errors").insertOne({ + _id: monkeyError.errorID, + timestamp: Date.now(), + status: monkeyError.status, + uid: monkeyError.uid, + message: monkeyError.message, + stack: monkeyError.stack, + }); + monkeyError.stack = undefined; + } else { + console.error(monkeyError.message); + } + return res.status(monkeyError.status || 500).json(monkeyError); +}); + +console.log("Starting server..."); +app.listen(PORT, async () => { + console.log(`Listening on port ${PORT}`); + console.log("Connecting to database..."); + await connectDB(); + console.log("Database connected"); + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); + + console.log("Starting cron jobs..."); + jobs.forEach((job) => job.start()); +}); + +==> ./monkeytype/backend/constants/quoteLanguages.js <== +const SUPPORTED_QUOTE_LANGUAGES = [ + "albanian", + "arabic", + "code_c++", + "code_c", + "code_java", + "code_javascript", + "code_python", + "code_rust", + "czech", + "danish", + "dutch", + "english", + "filipino", + "french", + "german", + "hindi", + "icelandic", + "indonesian", + "irish", + "italian", + "lithuanian", + "malagasy", + "polish", + "portuguese", + "russian", + "serbian", + "slovak", + "spanish", + "swedish", + "thai", + "toki_pona", + "turkish", + "vietnamese", +]; + +module.exports = SUPPORTED_QUOTE_LANGUAGES; + +==> ./monkeytype/backend/dao/leaderboards.js <== +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); +const { ObjectID } = require("mongodb"); +const Logger = require("../handlers/logger"); +const { performance } = require("perf_hooks"); + +class LeaderboardsDAO { + static async get(mode, mode2, language, skip, limit = 50) { + if (limit > 50 || limit <= 0) limit = 50; + if (skip < 0) skip = 0; + const preset = await mongoDB() + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .find() + .sort({ rank: 1 }) + .skip(parseInt(skip)) + .limit(parseInt(limit)) + .toArray(); + return preset; + } + + static async getRank(mode, mode2, language, uid) { + const res = await mongoDB() + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .findOne({ uid }); + if (res) + res.count = await mongoDB() + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .estimatedDocumentCount(); + return res; + } + + static async update(mode, mode2, language, uid = undefined) { + let str = `lbPersonalBests.${mode}.${mode2}.${language}`; + let start1 = performance.now(); + let lb = await mongoDB() + .collection("users") + .aggregate( + [ + { + $match: { + [str + ".wpm"]: { + $exists: true, + }, + [str + ".acc"]: { + $exists: true, + }, + [str + ".timestamp"]: { + $exists: true, + }, + banned: { $exists: false }, + }, + }, + { + $set: { + [str + ".uid"]: "$uid", + [str + ".name"]: "$name", + [str + ".discordId"]: "$discordId", + }, + }, + { + $replaceRoot: { + newRoot: "$" + str, + }, + }, + { + $sort: { + wpm: -1, + acc: -1, + timestamp: -1, + }, + }, + ], + { allowDiskUse: true } + ) + .toArray(); + let end1 = performance.now(); + + let start2 = performance.now(); + let retval = undefined; + lb.forEach((lbEntry, index) => { + lbEntry.rank = index + 1; + if (uid && lbEntry.uid === uid) { + retval = index + 1; + } + }); + let end2 = performance.now(); + let start3 = performance.now(); + try { + await mongoDB() + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .drop(); + } catch (e) {} + if (lb && lb.length !== 0) + await mongoDB() + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .insertMany(lb); + let end3 = performance.now(); + + let start4 = performance.now(); + await mongoDB() + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .createIndex({ + uid: -1, + }); + await mongoDB() + .collection(`leaderboards.${language}.${mode}.${mode2}`) + .createIndex({ + rank: 1, + }); + let end4 = performance.now(); + + let timeToRunAggregate = (end1 - start1) / 1000; + let timeToRunLoop = (end2 - start2) / 1000; + let timeToRunInsert = (end3 - start3) / 1000; + let timeToRunIndex = (end4 - start4) / 1000; + + Logger.log( + `system_lb_update_${language}_${mode}_${mode2}`, + `Aggregate ${timeToRunAggregate}s, loop ${timeToRunLoop}s, insert ${timeToRunInsert}s, index ${timeToRunIndex}s`, + uid + ); + + if (retval) { + return { + message: "Successfully updated leaderboard", + rank: retval, + }; + } else { + return { + message: "Successfully updated leaderboard", + }; + } + } +} + +module.exports = LeaderboardsDAO; + +==> ./monkeytype/backend/dao/preset.js <== +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); +const { ObjectID } = require("mongodb"); + +class PresetDAO { + static async getPresets(uid) { + const preset = await mongoDB() + .collection("presets") + .find({ uid }) + .sort({ timestamp: -1 }) + .toArray(); // this needs to be changed to later take patreon into consideration + return preset; + } + + static async addPreset(uid, name, config) { + const count = await mongoDB().collection("presets").find({ uid }).count(); + if (count >= 10) throw new MonkeyError(409, "Too many presets"); + let preset = await mongoDB() + .collection("presets") + .insertOne({ uid, name, config }); + return { + insertedId: preset.insertedId, + }; + } + + static async editPreset(uid, _id, name, config) { + console.log(_id); + const preset = await mongoDB() + .collection("presets") + .findOne({ uid, _id: ObjectID(_id) }); + if (!preset) throw new MonkeyError(404, "Preset not found"); + if (config) { + return await mongoDB() + .collection("presets") + .updateOne({ uid, _id: ObjectID(_id) }, { $set: { name, config } }); + } else { + return await mongoDB() + .collection("presets") + .updateOne({ uid, _id: ObjectID(_id) }, { $set: { name } }); + } + } + + static async removePreset(uid, _id) { + const preset = await mongoDB() + .collection("presets") + .findOne({ uid, _id: ObjectID(_id) }); + if (!preset) throw new MonkeyError(404, "Preset not found"); + return await mongoDB() + .collection("presets") + .deleteOne({ uid, _id: ObjectID(_id) }); + } +} + +module.exports = PresetDAO; + +==> ./monkeytype/backend/dao/quote-ratings.js <== +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); + +class QuoteRatingsDAO { + static async submit(quoteId, language, rating, update) { + if (update) { + await mongoDB() + .collection("quote-rating") + .updateOne( + { quoteId, language }, + { $inc: { totalRating: rating } }, + { upsert: true } + ); + } else { + await mongoDB() + .collection("quote-rating") + .updateOne( + { quoteId, language }, + { $inc: { ratings: 1, totalRating: rating } }, + { upsert: true } + ); + } + let quoteRating = await this.get(quoteId, language); + + let average = parseFloat( + ( + Math.round((quoteRating.totalRating / quoteRating.ratings) * 10) / 10 + ).toFixed(1) + ); + + return await mongoDB() + .collection("quote-rating") + .updateOne({ quoteId, language }, { $set: { average } }); + } + + static async get(quoteId, language) { + return await mongoDB() + .collection("quote-rating") + .findOne({ quoteId, language }); + } +} + +module.exports = QuoteRatingsDAO; + +==> ./monkeytype/backend/dao/user.js <== +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); +const { ObjectID } = require("mongodb"); +const { checkAndUpdatePb } = require("../handlers/pb"); +const { updateAuthEmail } = require("../handlers/auth"); +const { isUsernameValid } = require("../handlers/validation"); + +class UsersDAO { + static async addUser(name, email, uid) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (user) + throw new MonkeyError(400, "User document already exists", "addUser"); + return await mongoDB() + .collection("users") + .insertOne({ name, email, uid, addedAt: Date.now() }); + } + + static async deleteUser(uid) { + return await mongoDB().collection("users").deleteOne({ uid }); + } + + static async updateName(uid, name) { + const nameDoc = await mongoDB() + .collection("users") + .findOne({ name: { $regex: new RegExp(`^${name}$`, "i") } }); + if (nameDoc) throw new MonkeyError(409, "Username already taken"); + let user = await mongoDB().collection("users").findOne({ uid }); + if ( + Date.now() - user.lastNameChange < 2592000000 && + isUsernameValid(user.name) + ) { + throw new MonkeyError(409, "You can change your name once every 30 days"); + } + return await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { name, lastNameChange: Date.now() } }); + } + + static async clearPb(uid) { + return await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { personalBests: {}, lbPersonalBests: {} } }); + } + + static async isNameAvailable(name) { + const nameDoc = await mongoDB().collection("users").findOne({ name }); + if (nameDoc) { + return false; + } else { + return true; + } + } + + static async updateQuoteRatings(uid, quoteRatings) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) + throw new MonkeyError(404, "User not found", "updateQuoteRatings"); + await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { quoteRatings } }); + return true; + } + + static async updateEmail(uid, email) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "update email"); + await updateAuthEmail(uid, email); + await mongoDB().collection("users").updateOne({ uid }, { $set: { email } }); + return true; + } + + static async getUser(uid) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "get user"); + return user; + } + + static async getUserByDiscordId(discordId) { + const user = await mongoDB().collection("users").findOne({ discordId }); + if (!user) + throw new MonkeyError(404, "User not found", "get user by discord id"); + return user; + } + + static async addTag(uid, name) { + let _id = ObjectID(); + await mongoDB() + .collection("users") + .updateOne({ uid }, { $push: { tags: { _id, name } } }); + return { + _id, + name, + }; + } + + static async getTags(uid) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "get tags"); + return user.tags; + } + + static async editTag(uid, _id, name) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "edit tag"); + if ( + user.tags === undefined || + user.tags.filter((t) => t._id == _id).length === 0 + ) + throw new MonkeyError(404, "Tag not found"); + return await mongoDB() + .collection("users") + .updateOne( + { + uid: uid, + "tags._id": ObjectID(_id), + }, + { $set: { "tags.$.name": name } } + ); + } + + static async removeTag(uid, _id) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "remove tag"); + if ( + user.tags === undefined || + user.tags.filter((t) => t._id == _id).length === 0 + ) + throw new MonkeyError(404, "Tag not found"); + return await mongoDB() + .collection("users") + .updateOne( + { + uid: uid, + "tags._id": ObjectID(_id), + }, + { $pull: { tags: { _id: ObjectID(_id) } } } + ); + } + + static async removeTagPb(uid, _id) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "remove tag pb"); + if ( + user.tags === undefined || + user.tags.filter((t) => t._id == _id).length === 0 + ) + throw new MonkeyError(404, "Tag not found"); + return await mongoDB() + .collection("users") + .updateOne( + { + uid: uid, + "tags._id": ObjectID(_id), + }, + { $set: { "tags.$.personalBests": {} } } + ); + } + + static async updateLbMemory(uid, mode, mode2, language, rank) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "update lb memory"); + if (user.lbMemory === undefined) user.lbMemory = {}; + if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {}; + if (user.lbMemory[mode][mode2] === undefined) + user.lbMemory[mode][mode2] = {}; + user.lbMemory[mode][mode2][language] = rank; + return await mongoDB() + .collection("users") + .updateOne( + { uid }, + { + $set: { lbMemory: user.lbMemory }, + } + ); + } + + static async checkIfPb(uid, result) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "check if pb"); + + const { + mode, + mode2, + acc, + consistency, + difficulty, + lazyMode, + language, + punctuation, + rawWpm, + wpm, + funbox, + } = result; + + if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { + return false; + } + + if (mode === "quote") { + return false; + } + + let lbpb = user.lbPersonalBests; + if (!lbpb) lbpb = {}; + + let pb = checkAndUpdatePb( + user.personalBests, + lbpb, + mode, + mode2, + acc, + consistency, + difficulty, + lazyMode, + language, + punctuation, + rawWpm, + wpm + ); + + if (pb.isPb) { + await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { personalBests: pb.obj } }); + if (pb.lbObj) { + await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { lbPersonalBests: pb.lbObj } }); + } + return true; + } else { + return false; + } + } + + static async checkIfTagPb(uid, result) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "check if tag pb"); + + if (user.tags === undefined || user.tags.length === 0) { + return []; + } + + const { + mode, + mode2, + acc, + consistency, + difficulty, + lazyMode, + language, + punctuation, + rawWpm, + wpm, + tags, + funbox, + } = result; + + if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { + return []; + } + + if (mode === "quote") { + return []; + } + + let tagsToCheck = []; + user.tags.forEach((tag) => { + tags.forEach((resultTag) => { + if (resultTag == tag._id) { + tagsToCheck.push(tag); + } + }); + }); + + let ret = []; + + tagsToCheck.forEach(async (tag) => { + let tagpb = checkAndUpdatePb( + tag.personalBests, + undefined, + mode, + mode2, + acc, + consistency, + difficulty, + lazyMode, + language, + punctuation, + rawWpm, + wpm + ); + if (tagpb.isPb) { + ret.push(tag._id); + await mongoDB() + .collection("users") + .updateOne( + { uid, "tags._id": ObjectID(tag._id) }, + { $set: { "tags.$.personalBests": tagpb.obj } } + ); + } + }); + + return ret; + } + + static async resetPb(uid) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "reset pb"); + return await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { personalBests: {} } }); + } + + static async updateTypingStats(uid, restartCount, timeTyping) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) + throw new MonkeyError(404, "User not found", "update typing stats"); + + return await mongoDB() + .collection("users") + .updateOne( + { uid }, + { + $inc: { + startedTests: restartCount + 1, + completedTests: 1, + timeTyping, + }, + } + ); + } + + static async linkDiscord(uid, discordId) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "link discord"); + return await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { discordId } }); + } + + static async unlinkDiscord(uid) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "unlink discord"); + return await mongoDB() + .collection("users") + .updateOne({ uid }, { $set: { discordId: null } }); + } + + static async incrementBananas(uid, wpm) { + const user = await mongoDB().collection("users").findOne({ uid }); + if (!user) + throw new MonkeyError(404, "User not found", "increment bananas"); + + let best60; + try { + best60 = Math.max(...user.personalBests.time[60].map((best) => best.wpm)); + } catch (e) { + best60 = undefined; + } + + if (best60 === undefined || wpm >= best60 - best60 * 0.25) { + //increment when no record found or wpm is within 25% of the record + return await mongoDB() + .collection("users") + .updateOne({ uid }, { $inc: { bananas: 1 } }); + } else { + return null; + } + } +} + +module.exports = UsersDAO; + +==> ./monkeytype/backend/dao/config.js <== +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); + +class ConfigDAO { + static async saveConfig(uid, config) { + return await mongoDB() + .collection("configs") + .updateOne({ uid }, { $set: { config } }, { upsert: true }); + } + + static async getConfig(uid) { + let config = await mongoDB().collection("configs").findOne({ uid }); + // if (!config) throw new MonkeyError(404, "Config not found"); + return config; + } +} + +module.exports = ConfigDAO; + +==> ./monkeytype/backend/dao/new-quotes.js <== +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); +const fs = require("fs"); +const simpleGit = require("simple-git"); +const path = require("path"); +let git; +try { + git = simpleGit(path.join(__dirname, "../../../monkeytype-new-quotes")); +} catch (e) { + git = undefined; +} +const stringSimilarity = require("string-similarity"); +const { ObjectID } = require("mongodb"); + +class NewQuotesDAO { + static async add(text, source, language, uid) { + if (!git) throw new MonkeyError(500, "Git not available."); + let quote = { + text: text, + source: source, + language: language.toLowerCase(), + submittedBy: uid, + timestamp: Date.now(), + approved: false, + }; + //check for duplicate first + const fileDir = path.join( + __dirname, + `../../../monkeytype-new-quotes/static/quotes/${language}.json` + ); + let duplicateId = -1; + let similarityScore = -1; + if (fs.existsSync(fileDir)) { + // let quoteFile = fs.readFileSync(fileDir); + // quoteFile = JSON.parse(quoteFile.toString()); + // quoteFile.quotes.every((old) => { + // if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.9) { + // duplicateId = old.id; + // similarityScore = stringSimilarity.compareTwoStrings( + // old.text, + // quote.text + // ); + // return false; + // } + // return true; + // }); + } else { + return { languageError: 1 }; + } + if (duplicateId != -1) { + return { duplicateId, similarityScore }; + } + return await mongoDB().collection("new-quotes").insertOne(quote); + } + + static async get() { + if (!git) throw new MonkeyError(500, "Git not available."); + return await mongoDB() + .collection("new-quotes") + .find({ approved: false }) + .sort({ timestamp: 1 }) + .limit(10) + .toArray(); + } + + static async approve(quoteId, editQuote, editSource) { + if (!git) throw new MonkeyError(500, "Git not available."); + //check mod status + let quote = await mongoDB() + .collection("new-quotes") + .findOne({ _id: ObjectID(quoteId) }); + if (!quote) { + throw new MonkeyError(404, "Quote not found"); + } + let language = quote.language; + quote = { + text: editQuote ? editQuote : quote.text, + source: editSource ? editSource : quote.source, + length: quote.text.length, + }; + let message = ""; + const fileDir = path.join( + __dirname, + `../../../monkeytype-new-quotes/static/quotes/${language}.json` + ); + await git.pull("upstream", "master"); + if (fs.existsSync(fileDir)) { + let quoteFile = fs.readFileSync(fileDir); + quoteFile = JSON.parse(quoteFile.toString()); + quoteFile.quotes.every((old) => { + if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.8) { + throw new MonkeyError(409, "Duplicate quote"); + } + }); + let maxid = 0; + quoteFile.quotes.map(function (q) { + if (q.id > maxid) { + maxid = q.id; + } + }); + quote.id = maxid + 1; + quoteFile.quotes.push(quote); + fs.writeFileSync(fileDir, JSON.stringify(quoteFile, null, 2)); + message = `Added quote to ${language}.json.`; + } else { + //file doesnt exist, create it + quote.id = 1; + fs.writeFileSync( + fileDir, + JSON.stringify({ + language: language, + groups: [ + [0, 100], + [101, 300], + [301, 600], + [601, 9999], + ], + quotes: [quote], + }) + ); + message = `Created file ${language}.json and added quote.`; + } + await git.add([`static/quotes/${language}.json`]); + await git.commit(`Added quote to ${language}.json`); + await git.push("origin", "master"); + await mongoDB() + .collection("new-quotes") + .deleteOne({ _id: ObjectID(quoteId) }); + return { quote, message }; + } + + static async refuse(quoteId) { + if (!git) throw new MonkeyError(500, "Git not available."); + return await mongoDB() + .collection("new-quotes") + .deleteOne({ _id: ObjectID(quoteId) }); + } +} + +module.exports = NewQuotesDAO; + +==> ./monkeytype/backend/dao/public-stats.js <== +// const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); +const { roundTo2 } = require("../handlers/misc"); + +class PublicStatsDAO { + //needs to be rewritten, this is public stats not user stats + static async updateStats(restartCount, time) { + time = roundTo2(time); + await mongoDB() + .collection("public") + .updateOne( + { type: "stats" }, + { + $inc: { + testsCompleted: 1, + testsStarted: restartCount + 1, + timeTyping: time, + }, + }, + { upsert: true } + ); + return true; + } +} + +module.exports = PublicStatsDAO; + +==> ./monkeytype/backend/dao/bot.js <== +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); + +async function addCommand(command, arguments) { + return await mongoDB().collection("bot-commands").insertOne({ + command, + arguments, + executed: false, + requestTimestamp: Date.now(), + }); +} + +async function addCommands(commands, arguments) { + if (commands.length === 0 || commands.length !== arguments.length) { + return []; + } + + const normalizedCommands = commands.map((command, index) => { + return { + command, + arguments: arguments[index], + executed: false, + requestTimestamp: Date.now(), + }; + }); + + return await mongoDB() + .collection("bot-commands") + .insertMany(normalizedCommands); +} + +class BotDAO { + static async updateDiscordRole(discordId, wpm) { + return await addCommand("updateRole", [discordId, wpm]); + } + + static async linkDiscord(uid, discordId) { + return await addCommand("linkDiscord", [discordId, uid]); + } + + static async unlinkDiscord(uid, discordId) { + return await addCommand("unlinkDiscord", [discordId, uid]); + } + + static async awardChallenge(discordId, challengeName) { + return await addCommand("awardChallenge", [discordId, challengeName]); + } + + static async announceLbUpdate(newRecords, leaderboardId) { + if (newRecords.length === 0) { + return []; + } + + const leaderboardCommands = Array(newRecords.length).fill("sayLbUpdate"); + const leaderboardCommandsArguments = newRecords.map((newRecord) => { + return [ + newRecord.discordId ?? newRecord.name, + newRecord.rank, + leaderboardId, + newRecord.wpm, + newRecord.raw, + newRecord.acc, + newRecord.consistency, + ]; + }); + + return await addCommands(leaderboardCommands, leaderboardCommandsArguments); + } +} + +module.exports = BotDAO; + +==> ./monkeytype/backend/dao/psa.js <== +const { mongoDB } = require("../init/mongodb"); + +class PsaDAO { + static async get(uid, config) { + return await mongoDB().collection("psa").find().toArray(); + } +} + +module.exports = PsaDAO; + +==> ./monkeytype/backend/dao/result.js <== +const { ObjectID } = require("mongodb"); +const MonkeyError = require("../handlers/error"); +const { mongoDB } = require("../init/mongodb"); +const UserDAO = require("./user"); + +class ResultDAO { + static async addResult(uid, result) { + let user; + try { + user = await UserDAO.getUser(uid); + } catch (e) { + user = null; + } + if (!user) throw new MonkeyError(404, "User not found", "add result"); + if (result.uid === undefined) result.uid = uid; + // result.ir = true; + let res = await mongoDB().collection("results").insertOne(result); + return { + insertedId: res.insertedId, + }; + } + + static async deleteAll(uid) { + return await mongoDB().collection("results").deleteMany({ uid }); + } + + static async updateTags(uid, resultid, tags) { + const result = await mongoDB() + .collection("results") + .findOne({ _id: ObjectID(resultid), uid }); + if (!result) throw new MonkeyError(404, "Result not found"); + const userTags = await UserDAO.getTags(uid); + const userTagIds = userTags.map((tag) => tag._id.toString()); + let validTags = true; + tags.forEach((tagId) => { + if (!userTagIds.includes(tagId)) validTags = false; + }); + if (!validTags) + throw new MonkeyError(400, "One of the tag id's is not vaild"); + return await mongoDB() + .collection("results") + .updateOne({ _id: ObjectID(resultid), uid }, { $set: { tags } }); + } + + static async getResult(uid, id) { + const result = await mongoDB() + .collection("results") + .findOne({ _id: ObjectID(id), uid }); + if (!result) throw new MonkeyError(404, "Result not found"); + return result; + } + + static async getLastResult(uid) { + let result = await mongoDB() + .collection("results") + .find({ uid }) + .sort({ timestamp: -1 }) + .limit(1) + .toArray(); + result = result[0]; + if (!result) throw new MonkeyError(404, "No results found"); + return result; + } + + static async getResultByTimestamp(uid, timestamp) { + return await mongoDB().collection("results").findOne({ uid, timestamp }); + } + + static async getResults(uid, start, end) { + start = start ?? 0; + end = end ?? 1000; + const result = await mongoDB() + .collection("results") + .find({ uid }) + .sort({ timestamp: -1 }) + .skip(start) + .limit(end) + .toArray(); // this needs to be changed to later take patreon into consideration + if (!result) throw new MonkeyError(404, "Result not found"); + return result; + } +} + +module.exports = ResultDAO; + +==> ./monkeytype/backend/middlewares/auth.js <== +const MonkeyError = require("../handlers/error"); +const { verifyIdToken } = require("../handlers/auth"); + +module.exports = { + async authenticateRequest(req, res, next) { + try { + if (process.env.MODE === "dev" && !req.headers.authorization) { + if (req.body.uid) { + req.decodedToken = { + uid: req.body.uid, + }; + console.log("Running authorization in dev mode"); + return next(); + } else { + throw new MonkeyError( + 400, + "Running authorization in dev mode but still no uid was provided" + ); + } + } + const { authorization } = req.headers; + if (!authorization) + throw new MonkeyError( + 401, + "Unauthorized", + `endpoint: ${req.baseUrl} no authorization header found` + ); + const token = authorization.split(" "); + if (token[0].trim() !== "Bearer") + return next( + new MonkeyError(400, "Invalid Token", "Incorrect token type") + ); + req.decodedToken = await verifyIdToken(token[1]); + return next(); + } catch (e) { + return next(e); + } + }, +}; + +==> ./monkeytype/backend/middlewares/rate-limit.js <== +const rateLimit = require("express-rate-limit"); + +const getAddress = (req) => + req.headers["cf-connecting-ip"] || + req.headers["x-forwarded-for"] || + req.ip || + "255.255.255.255"; +const message = "Too many requests, please try again later"; +const multiplier = process.env.MODE === "dev" ? 100 : 1; + +// Config Routing +exports.configUpdate = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 500 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.configGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 120 * multiplier, + message, + keyGenerator: getAddress, +}); + +// Leaderboards Routing +exports.leaderboardsGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +// New Quotes Routing +exports.newQuotesGet = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 500 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.newQuotesAdd = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.newQuotesAction = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 500 * multiplier, + message, + keyGenerator: getAddress, +}); + +// Quote Ratings Routing +exports.quoteRatingsGet = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 500 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.quoteRatingsSubmit = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 500 * multiplier, + message, + keyGenerator: getAddress, +}); + +// Quote reporting +exports.quoteReportSubmit = rateLimit({ + windowMs: 30 * 60 * 1000, // 30 min + max: 50 * multiplier, + message, + keyGenerator: getAddress, +}); + +// Presets Routing +exports.presetsGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.presetsAdd = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.presetsRemove = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.presetsEdit = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +// PSA (Public Service Announcement) Routing +exports.psaGet = rateLimit({ + windowMs: 60 * 1000, + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +// Results Routing +exports.resultsGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.resultsAdd = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 500 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.resultsTagsUpdate = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 30 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.resultsDeleteAll = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 10 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.resultsLeaderboardGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.resultsLeaderboardQualificationGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +// Users Routing +exports.userGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userSignup = rateLimit({ + windowMs: 24 * 60 * 60 * 1000, // 1 day + max: 3 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userDelete = rateLimit({ + windowMs: 24 * 60 * 60 * 1000, // 1 day + max: 3 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userCheckName = rateLimit({ + windowMs: 60 * 1000, + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userUpdateName = rateLimit({ + windowMs: 24 * 60 * 60 * 1000, // 1 day + max: 3 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userUpdateLBMemory = rateLimit({ + windowMs: 60 * 1000, + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userUpdateEmail = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userClearPB = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userTagsGet = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userTagsRemove = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 30 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userTagsClearPB = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 60 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userTagsEdit = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 30 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userTagsAdd = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 30 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userDiscordLink = exports.usersTagsEdit = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 15 * multiplier, + message, + keyGenerator: getAddress, +}); + +exports.userDiscordUnlink = exports.usersTagsEdit = rateLimit({ + windowMs: 60 * 60 * 1000, // 60 min + max: 15 * multiplier, + message, + keyGenerator: getAddress, +}); + +==> ./monkeytype/backend/middlewares/apiUtils.js <== +const joi = require("joi"); +const MonkeyError = require("../handlers/error"); + +function requestValidation(validationSchema) { + return (req, res, next) => { + // In dev environments, as an alternative to token authentication, + // you can pass the authentication middleware by having a user id in the body. + // Inject the user id into the schema so that validation will not fail. + if (process.env.MODE === "dev") { + validationSchema.body = { + uid: joi.any(), + ...(validationSchema.body ?? {}), + }; + } + + Object.keys(validationSchema).forEach((key) => { + const schema = validationSchema[key]; + const joiSchema = joi.object().keys(schema); + const { error } = joiSchema.validate(req[key] ?? {}); + if (error) { + const errorMessage = error.details[0].message; + throw new MonkeyError(400, `Invalid request: ${errorMessage}`); + } + }); + + next(); + }; +} + +module.exports = { + requestValidation, +}; + +==> ./monkeytype/backend/.gitignore <== +lastId.txt +log_success.txt +log_failed.txt + +==> ./monkeytype/backend/api/controllers/leaderboards.js <== +const LeaderboardsDAO = require("../../dao/leaderboards"); +const ResultDAO = require("../../dao/result"); +const UserDAO = require("../../dao/user"); +const admin = require("firebase-admin"); +const { verifyIdToken } = require("../../handlers/auth"); + +class LeaderboardsController { + static async get(req, res, next) { + try { + const { language, mode, mode2, skip, limit } = req.query; + + let uid; + + const { authorization } = req.headers; + if (authorization) { + const token = authorization.split(" "); + if (token[0].trim() == "Bearer") + req.decodedToken = await verifyIdToken(token[1]); + uid = req.decodedToken.uid; + } + + if (!language || !mode || !mode2 || !skip) { + return res.status(400).json({ + message: "Missing parameters", + }); + } + let retval = await LeaderboardsDAO.get( + mode, + mode2, + language, + skip, + limit + ); + retval.forEach((item) => { + if (uid && item.uid == uid) { + // + } else { + delete item.discordId; + delete item.uid; + delete item.difficulty; + delete item.language; + } + }); + return res.status(200).json(retval); + } catch (e) { + return next(e); + } + } + + static async getRank(req, res, next) { + try { + const { language, mode, mode2 } = req.query; + const { uid } = req.decodedToken; + if (!language || !mode || !mode2 || !uid) { + return res.status(400).json({ + message: "Missing parameters", + }); + } + let retval = await LeaderboardsDAO.getRank(mode, mode2, language, uid); + return res.status(200).json(retval); + } catch (e) { + return next(e); + } + } + + static async update(req, res, next) { + try { + return res.status(200).json({ + message: "Leaderboards disabled", + lbdisabled: true, + }); + if (process.env.LBDISABLED === true) { + return res.status(200).json({ + message: "Leaderboards disabled", + lbdisabled: true, + }); + } + const { rid } = req.body; + const { uid } = req.decodedToken; + if (!rid) { + return res.status(400).json({ + message: "Missing parameters", + }); + } + //verify user first + let user = await UserDAO.getUser(uid); + if (!user) { + return res.status(400).json({ + message: "User not found", + }); + } + if (user.banned === true) { + return res.status(200).json({ + message: "User banned", + banned: true, + }); + } + let userauth = await admin.auth().getUser(uid); + if (!userauth.emailVerified) { + return res.status(200).json({ + message: "User needs to verify email address", + needsToVerifyEmail: true, + }); + } + + let result = await ResultDAO.getResult(uid, rid); + if (!result.language) result.language = "english"; + if ( + result.mode == "time" && + result.isPb && + (result.mode2 == 15 || result.mode2 == 60) && + ["english"].includes(result.language) + ) { + //check if its better than their current lb pb + let lbpb = + user?.lbPersonalBests?.[result.mode]?.[result.mode2]?.[ + result.language + ]?.wpm; + if (!lbpb) lbpb = 0; + if (result.wpm >= lbpb) { + //run update + let retval = await LeaderboardsDAO.update( + result.mode, + result.mode2, + result.language, + uid + ); + if (retval.rank) { + await UserDAO.updateLbMemory( + uid, + result.mode, + result.mode2, + result.language, + retval.rank + ); + } + return res.status(200).json(retval); + } else { + let rank = await LeaderboardsDAO.getRank( + result.mode, + result.mode2, + result.language, + uid + ); + rank = rank?.rank; + if (!rank) { + return res.status(400).json({ + message: "User has a lbPb but was not found on the leaderboard", + }); + } + await UserDAO.updateLbMemory( + uid, + result.mode, + result.mode2, + result.language, + rank + ); + return res.status(200).json({ + message: "Not a new leaderboard personal best", + rank, + }); + } + } else { + return res.status(400).json({ + message: "This result is not eligible for any leaderboard", + }); + } + } catch (e) { + return next(e); + } + } + + static async debugUpdate(req, res, next) { + try { + const { language, mode, mode2 } = req.body; + if (!language || !mode || !mode2) { + return res.status(400).json({ + message: "Missing parameters", + }); + } + let retval = await LeaderboardsDAO.update(mode, mode2, language); + return res.status(200).json(retval); + } catch (e) { + return next(e); + } + } +} + +module.exports = LeaderboardsController; + +==> ./monkeytype/backend/api/controllers/preset.js <== +const PresetDAO = require("../../dao/preset"); +const { + isTagPresetNameValid, + validateConfig, +} = require("../../handlers/validation"); +const MonkeyError = require("../../handlers/error"); + +class PresetController { + static async getPresets(req, res, next) { + try { + const { uid } = req.decodedToken; + let presets = await PresetDAO.getPresets(uid); + return res.status(200).json(presets); + } catch (e) { + return next(e); + } + } + + static async addPreset(req, res, next) { + try { + const { name, config } = req.body; + const { uid } = req.decodedToken; + if (!isTagPresetNameValid(name)) + throw new MonkeyError(400, "Invalid preset name."); + validateConfig(config); + let preset = await PresetDAO.addPreset(uid, name, config); + return res.status(200).json(preset); + } catch (e) { + return next(e); + } + } + + static async editPreset(req, res, next) { + try { + const { _id, name, config } = req.body; + const { uid } = req.decodedToken; + if (!isTagPresetNameValid(name)) + throw new MonkeyError(400, "Invalid preset name."); + if (config) validateConfig(config); + await PresetDAO.editPreset(uid, _id, name, config); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async removePreset(req, res, next) { + try { + const { _id } = req.body; + const { uid } = req.decodedToken; + await PresetDAO.removePreset(uid, _id); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } +} + +module.exports = PresetController; + +==> ./monkeytype/backend/api/controllers/core.js <== +class CoreController { + static async handleTestResult() {} +} + +==> ./monkeytype/backend/api/controllers/quote-ratings.js <== +const QuoteRatingsDAO = require("../../dao/quote-ratings"); +const UserDAO = require("../../dao/user"); +const MonkeyError = require("../../handlers/error"); + +class QuoteRatingsController { + static async getRating(req, res, next) { + try { + const { quoteId, language } = req.query; + let data = await QuoteRatingsDAO.get(parseInt(quoteId), language); + return res.status(200).json(data); + } catch (e) { + return next(e); + } + } + static async submitRating(req, res, next) { + try { + let { uid } = req.decodedToken; + let { quoteId, rating, language } = req.body; + quoteId = parseInt(quoteId); + rating = parseInt(rating); + if (isNaN(quoteId) || isNaN(rating)) { + throw new MonkeyError( + 400, + "Bad request. Quote id or rating is not a number." + ); + } + if (typeof language !== "string") { + throw new MonkeyError(400, "Bad request. Language is not a string."); + } + + if (rating < 1 || rating > 5) { + throw new MonkeyError( + 400, + "Bad request. Rating must be between 1 and 5." + ); + } + + rating = Math.round(rating); + + //check if user already submitted a rating + let user = await UserDAO.getUser(uid); + + if (!user) { + throw new MonkeyError(401, "User not found."); + } + let quoteRatings = user.quoteRatings; + + if (quoteRatings === undefined) quoteRatings = {}; + if (quoteRatings[language] === undefined) quoteRatings[language] = {}; + if (quoteRatings[language][quoteId] == undefined) + quoteRatings[language][quoteId] = undefined; + + let quoteRating = quoteRatings[language][quoteId]; + + let newRating; + let update; + if (quoteRating) { + //user already voted for this + newRating = rating - quoteRating; + update = true; + } else { + //user has not voted for this + newRating = rating; + update = false; + } + + await QuoteRatingsDAO.submit(quoteId, language, newRating, update); + quoteRatings[language][quoteId] = rating; + await UserDAO.updateQuoteRatings(uid, quoteRatings); + + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } +} + +module.exports = QuoteRatingsController; + +==> ./monkeytype/backend/api/controllers/user.js <== +const UsersDAO = require("../../dao/user"); +const BotDAO = require("../../dao/bot"); +const { + isUsernameValid, + isTagPresetNameValid, +} = require("../../handlers/validation"); +const MonkeyError = require("../../handlers/error"); +const fetch = require("node-fetch"); +const Logger = require("./../../handlers/logger.js"); +const uaparser = require("ua-parser-js"); + +// import UsersDAO from "../../dao/user"; +// import BotDAO from "../../dao/bot"; +// import { isUsernameValid } from "../../handlers/validation"; + +class UserController { + static async createNewUser(req, res, next) { + try { + const { name } = req.body; + const { email, uid } = req.decodedToken; + await UsersDAO.addUser(name, email, uid); + Logger.log("user_created", `${name} ${email}`, uid); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async deleteUser(req, res, next) { + try { + const { uid } = req.decodedToken; + const userInfo = await UsersDAO.getUser(uid); + await UsersDAO.deleteUser(uid); + Logger.log("user_deleted", `${userInfo.email} ${userInfo.name}`, uid); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async updateName(req, res, next) { + try { + const { uid } = req.decodedToken; + const { name } = req.body; + if (!isUsernameValid(name)) + return res.status(400).json({ + message: + "Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -", + }); + let olduser = await UsersDAO.getUser(uid); + await UsersDAO.updateName(uid, name); + Logger.log( + "user_name_updated", + `changed name from ${olduser.name} to ${name}`, + uid + ); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async clearPb(req, res, next) { + try { + const { uid } = req.decodedToken; + await UsersDAO.clearPb(uid); + Logger.log("user_cleared_pbs", "", uid); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async checkName(req, res, next) { + try { + const { name } = req.body; + if (!isUsernameValid(name)) + return next({ + status: 400, + message: + "Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -", + }); + const available = await UsersDAO.isNameAvailable(name); + if (!available) + return res.status(400).json({ message: "Username unavailable" }); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async updateEmail(req, res, next) { + try { + const { uid } = req.decodedToken; + const { newEmail } = req.body; + try { + await UsersDAO.updateEmail(uid, newEmail); + } catch (e) { + throw new MonkeyError(400, e.message, "update email", uid); + } + Logger.log("user_email_updated", `changed email to ${newEmail}`, uid); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async getUser(req, res, next) { + try { + const { email, uid } = req.decodedToken; + let userInfo; + try { + userInfo = await UsersDAO.getUser(uid); + } catch (e) { + if (email && uid) { + userInfo = await UsersDAO.addUser(undefined, email, uid); + } else { + throw new MonkeyError( + 400, + "User not found. Could not recreate user document.", + "Tried to recreate user document but either email or uid is nullish", + uid + ); + } + } + let agent = uaparser(req.headers["user-agent"]); + let logobj = { + ip: + req.headers["cf-connecting-ip"] || + req.headers["x-forwarded-for"] || + req.ip || + "255.255.255.255", + agent: + agent.os.name + + " " + + agent.os.version + + " " + + agent.browser.name + + " " + + agent.browser.version, + }; + if (agent.device.vendor) { + logobj.device = + agent.device.vendor + + " " + + agent.device.model + + " " + + agent.device.type; + } + Logger.log("user_data_requested", logobj, uid); + return res.status(200).json(userInfo); + } catch (e) { + return next(e); + } + } + + static async linkDiscord(req, res, next) { + try { + const { uid } = req.decodedToken; + + let requser; + try { + requser = await UsersDAO.getUser(uid); + } catch (e) { + requser = null; + } + if (requser?.banned === true) { + throw new MonkeyError(403, "Banned accounts cannot link with Discord"); + } + + let discordFetch = await fetch("https://discord.com/api/users/@me", { + headers: { + authorization: `${req.body.data.tokenType} ${req.body.data.accessToken}`, + }, + }); + discordFetch = await discordFetch.json(); + const did = discordFetch.id; + if (!did) { + throw new MonkeyError( + 500, + "Could not get Discord account info", + "did is undefined" + ); + } + let user; + try { + user = await UsersDAO.getUserByDiscordId(did); + } catch (e) { + user = null; + } + if (user !== null) { + throw new MonkeyError( + 400, + "This Discord account is already linked to a different account" + ); + } + await UsersDAO.linkDiscord(uid, did); + await BotDAO.linkDiscord(uid, did); + Logger.log("user_discord_link", `linked to ${did}`, uid); + return res.status(200).json({ + message: "Discord account linked", + did, + }); + } catch (e) { + return next(e); + } + } + + static async unlinkDiscord(req, res, next) { + try { + const { uid } = req.decodedToken; + let userInfo; + try { + userInfo = await UsersDAO.getUser(uid); + } catch (e) { + throw new MonkeyError(400, "User not found."); + } + if (!userInfo.discordId) { + throw new MonkeyError( + 400, + "User does not have a linked Discord account" + ); + } + await BotDAO.unlinkDiscord(uid, userInfo.discordId); + await UsersDAO.unlinkDiscord(uid); + Logger.log("user_discord_unlinked", userInfo.discordId, uid); + return res.status(200).send(); + } catch (e) { + return next(e); + } + } + + static async addTag(req, res, next) { + try { + const { uid } = req.decodedToken; + const { tagName } = req.body; + if (!isTagPresetNameValid(tagName)) + return res.status(400).json({ + message: + "Tag name invalid. Name cannot contain special characters or more than 16 characters. Can include _ . and -", + }); + let tag = await UsersDAO.addTag(uid, tagName); + return res.status(200).json(tag); + } catch (e) { + return next(e); + } + } + + static async clearTagPb(req, res, next) { + try { + const { uid } = req.decodedToken; + const { tagid } = req.body; + await UsersDAO.removeTagPb(uid, tagid); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async editTag(req, res, next) { + try { + const { uid } = req.decodedToken; + const { tagid, newname } = req.body; + if (!isTagPresetNameValid(newname)) + return res.status(400).json({ + message: + "Tag name invalid. Name cannot contain special characters or more than 16 characters. Can include _ . and -", + }); + await UsersDAO.editTag(uid, tagid, newname); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async removeTag(req, res, next) { + try { + const { uid } = req.decodedToken; + const { tagid } = req.body; + await UsersDAO.removeTag(uid, tagid); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } + + static async getTags(req, res, next) { + try { + const { uid } = req.decodedToken; + let tags = await UsersDAO.getTags(uid); + if (tags == undefined) tags = []; + return res.status(200).json(tags); + } catch (e) { + return next(e); + } + } + + static async updateLbMemory(req, res, next) { + try { + const { uid } = req.decodedToken; + const { mode, mode2, language, rank } = req.body; + await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } +} + +module.exports = UserController; + +==> ./monkeytype/backend/api/controllers/config.js <== +const ConfigDAO = require("../../dao/config"); +const { validateConfig } = require("../../handlers/validation"); + +class ConfigController { + static async getConfig(req, res, next) { + try { + const { uid } = req.decodedToken; + let config = await ConfigDAO.getConfig(uid); + return res.status(200).json(config); + } catch (e) { + return next(e); + } + } + static async saveConfig(req, res, next) { + try { + const { config } = req.body; + const { uid } = req.decodedToken; + validateConfig(config); + await ConfigDAO.saveConfig(uid, config); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } +} + +module.exports = ConfigController; + +==> ./monkeytype/backend/api/controllers/new-quotes.js <== +const NewQuotesDAO = require("../../dao/new-quotes"); +const MonkeyError = require("../../handlers/error"); +const UserDAO = require("../../dao/user"); +const Logger = require("../../handlers/logger.js"); +// const Captcha = require("../../handlers/captcha"); + +class NewQuotesController { + static async getQuotes(req, res, next) { + try { + const { uid } = req.decodedToken; + const userInfo = await UserDAO.getUser(uid); + if (!userInfo.quoteMod) { + throw new MonkeyError(403, "You don't have permission to do this"); + } + let data = await NewQuotesDAO.get(); + return res.status(200).json(data); + } catch (e) { + return next(e); + } + } + + static async addQuote(req, res, next) { + try { + throw new MonkeyError( + 500, + "Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up." + ); + // let { uid } = req.decodedToken; + // let { text, source, language, captcha } = req.body; + // if (!text || !source || !language) { + // throw new MonkeyError(400, "Please fill all the fields"); + // } + // if (!(await Captcha.verify(captcha))) { + // throw new MonkeyError(400, "Captcha check failed"); + // } + // let data = await NewQuotesDAO.add(text, source, language, uid); + // return res.status(200).json(data); + } catch (e) { + return next(e); + } + } + + static async approve(req, res, next) { + try { + let { uid } = req.decodedToken; + let { quoteId, editText, editSource } = req.body; + const userInfo = await UserDAO.getUser(uid); + if (!userInfo.quoteMod) { + throw new MonkeyError(403, "You don't have permission to do this"); + } + if (editText === "" || editSource === "") { + throw new MonkeyError(400, "Please fill all the fields"); + } + let data = await NewQuotesDAO.approve(quoteId, editText, editSource); + Logger.log("system_quote_approved", data, uid); + return res.status(200).json(data); + } catch (e) { + return next(e); + } + } + + static async refuse(req, res, next) { + try { + let { uid } = req.decodedToken; + let { quoteId } = req.body; + await NewQuotesDAO.refuse(quoteId, uid); + return res.sendStatus(200); + } catch (e) { + return next(e); + } + } +} + +module.exports = NewQuotesController; + +==> ./monkeytype/backend/api/controllers/psa.js <== +const PsaDAO = require("../../dao/psa"); + +class PsaController { + static async get(req, res, next) { + try { + let data = await PsaDAO.get(); + return res.status(200).json(data); + } catch (e) { + return next(e); + } + } +} + +module.exports = PsaController; + +==> ./monkeytype/backend/api/controllers/result.js <== +const ResultDAO = require("../../dao/result"); +const UserDAO = require("../../dao/user"); +const PublicStatsDAO = require("../../dao/public-stats"); +const BotDAO = require("../../dao/bot"); +const { validateObjectValues } = require("../../handlers/validation"); +const { stdDev, roundTo2 } = require("../../handlers/misc"); +const objecthash = require("object-hash"); +const Logger = require("../../handlers/logger"); +const path = require("path"); +const { config } = require("dotenv"); +config({ path: path.join(__dirname, ".env") }); + +let validateResult; +let validateKeys; +try { + let module = require("../../anticheat/anticheat"); + validateResult = module.validateResult; + validateKeys = module.validateKeys; + if (!validateResult || !validateKeys) throw new Error("undefined"); +} catch (e) { + if (process.env.MODE === "dev") { + console.error( + "No anticheat module found. Continuing in dev mode, results will not be validated." + ); + } else { + console.error("No anticheat module found."); + console.error( + "To continue in dev mode, add 'MODE=dev' to the .env file in the backend directory." + ); + process.exit(1); + } +} + +class ResultController { + static async getResults(req, res, next) { + try { + const { uid } = req.decodedToken; + const results = await ResultDAO.getResults(uid); + return res.status(200).json(results); + } catch (e) { + next(e); + } + } + + static async deleteAll(req, res, next) { + try { + const { uid } = req.decodedToken; + await ResultDAO.deleteAll(uid); + Logger.log("user_results_deleted", "", uid); + return res.sendStatus(200); + } catch (e) { + next(e); + } + } + + static async updateTags(req, res, next) { + try { + const { uid } = req.decodedToken; + const { tags, resultid } = req.body; + await ResultDAO.updateTags(uid, resultid, tags); + return res.sendStatus(200); + } catch (e) { + next(e); + } + } + + static async addResult(req, res, next) { + try { + const { uid } = req.decodedToken; + const { result } = req.body; + result.uid = uid; + if (validateObjectValues(result) > 0) + return res.status(400).json({ message: "Bad input" }); + if ( + result.wpm <= 0 || + result.wpm > 350 || + result.acc < 75 || + result.acc > 100 || + result.consistency > 100 + ) { + return res.status(400).json({ message: "Bad input" }); + } + if (result.wpm == result.raw && result.acc != 100) { + return res.status(400).json({ message: "Bad input" }); + } + if ( + (result.mode === "time" && result.mode2 < 15 && result.mode2 > 0) || + (result.mode === "time" && + result.mode2 == 0 && + result.testDuration < 15) || + (result.mode === "words" && result.mode2 < 10 && result.mode2 > 0) || + (result.mode === "words" && + result.mode2 == 0 && + result.testDuration < 15) || + (result.mode === "custom" && + result.customText !== undefined && + !result.customText.isWordRandom && + !result.customText.isTimeRandom && + result.customText.textLen < 10) || + (result.mode === "custom" && + result.customText !== undefined && + result.customText.isWordRandom && + !result.customText.isTimeRandom && + result.customText.word < 10) || + (result.mode === "custom" && + result.customText !== undefined && + !result.customText.isWordRandom && + result.customText.isTimeRandom && + result.customText.time < 15) + ) { + return res.status(400).json({ message: "Test too short" }); + } + + let resulthash = result.hash; + delete result.hash; + const serverhash = objecthash(result); + if (serverhash !== resulthash) { + Logger.log( + "incorrect_result_hash", + { + serverhash, + resulthash, + result, + }, + uid + ); + return res.status(400).json({ message: "Incorrect result hash" }); + } + + if (validateResult) { + if (!validateResult(result)) { + return res + .status(400) + .json({ message: "Result data doesn't make sense" }); + } + } else { + if (process.env.MODE === "dev") { + console.error( + "No anticheat module found. Continuing in dev mode, results will not be validated." + ); + } else { + throw new Error("No anticheat module found"); + } + } + + result.timestamp = Math.round(result.timestamp / 1000) * 1000; + + //dont use - result timestamp is unreliable, can be changed by system time and stuff + // if (result.timestamp > Math.round(Date.now() / 1000) * 1000 + 10) { + // Logger.log( + // "time_traveler", + // { + // resultTimestamp: result.timestamp, + // serverTimestamp: Math.round(Date.now() / 1000) * 1000 + 10, + // }, + // uid + // ); + // return res.status(400).json({ message: "Time traveler detected" }); + + // this probably wont work if we replace the timestamp with the server time later + // let timestampres = await ResultDAO.getResultByTimestamp( + // uid, + // result.timestamp + // ); + // if (timestampres) { + // return res.status(400).json({ message: "Duplicate result" }); + // } + + //convert result test duration to miliseconds + const testDurationMilis = result.testDuration * 1000; + //get latest result ordered by timestamp + let lastResultTimestamp; + try { + lastResultTimestamp = + (await ResultDAO.getLastResult(uid)).timestamp - 1000; + } catch (e) { + lastResultTimestamp = null; + } + + result.timestamp = Math.round(Date.now() / 1000) * 1000; + + //check if its greater than server time - milis or result time - milis + if ( + lastResultTimestamp && + (lastResultTimestamp + testDurationMilis > result.timestamp || + lastResultTimestamp + testDurationMilis > + Math.round(Date.now() / 1000) * 1000) + ) { + Logger.log( + "invalid_result_spacing", + { + lastTimestamp: lastResultTimestamp, + resultTime: result.timestamp, + difference: + lastResultTimestamp + testDurationMilis - result.timestamp, + }, + uid + ); + return res.status(400).json({ message: "Invalid result spacing" }); + } + + try { + result.keySpacingStats = { + average: + result.keySpacing.reduce( + (previous, current) => (current += previous) + ) / result.keySpacing.length, + sd: stdDev(result.keySpacing), + }; + } catch (e) { + // + } + try { + result.keyDurationStats = { + average: + result.keyDuration.reduce( + (previous, current) => (current += previous) + ) / result.keyDuration.length, + sd: stdDev(result.keyDuration), + }; + } catch (e) { + // + } + + const user = await UserDAO.getUser(uid); + // result.name = user.name; + + //check keyspacing and duration here for bots + if ( + result.mode === "time" && + result.wpm > 130 && + result.testDuration < 122 + ) { + if (user.verified === false || user.verified === undefined) { + if ( + result.keySpacingStats !== null && + result.keyDurationStats !== null + ) { + if (validateKeys) { + if (!validateKeys(result, uid)) { + return res + .status(400) + .json({ message: "Possible bot detected" }); + } + } else { + if (process.env.MODE === "dev") { + console.error( + "No anticheat module found. Continuing in dev mode, results will not be validated." + ); + } else { + throw new Error("No anticheat module found"); + } + } + } else { + return res.status(400).json({ message: "Missing key data" }); + } + } + } + + delete result.keySpacing; + delete result.keyDuration; + delete result.smoothConsistency; + delete result.wpmConsistency; + + try { + result.keyDurationStats.average = roundTo2( + result.keyDurationStats.average + ); + result.keyDurationStats.sd = roundTo2(result.keyDurationStats.sd); + result.keySpacingStats.average = roundTo2( + result.keySpacingStats.average + ); + result.keySpacingStats.sd = roundTo2(result.keySpacingStats.sd); + } catch (e) { + // + } + + let isPb = false; + let tagPbs = []; + + if (!result.bailedOut) { + isPb = await UserDAO.checkIfPb(uid, result); + tagPbs = await UserDAO.checkIfTagPb(uid, result); + } + + if (isPb) { + result.isPb = true; + } + + if (result.mode === "time" && String(result.mode2) === "60") { + UserDAO.incrementBananas(uid, result.wpm); + if (isPb && user.discordId) { + BotDAO.updateDiscordRole(user.discordId, result.wpm); + } + } + + if (result.challenge && user.discordId) { + BotDAO.awardChallenge(user.discordId, result.challenge); + } else { + delete result.challenge; + } + + let tt = 0; + let afk = result.afkDuration; + if (afk == undefined) { + afk = 0; + } + tt = result.testDuration + result.incompleteTestSeconds - afk; + + await UserDAO.updateTypingStats(uid, result.restartCount, tt); + + await PublicStatsDAO.updateStats(result.restartCount, tt); + + if (result.bailedOut === false) delete result.bailedOut; + if (result.blindMode === false) delete result.blindMode; + if (result.lazyMode === false) delete result.lazyMode; + if (result.difficulty === "normal") delete result.difficulty; + if (result.funbox === "none") delete result.funbox; + if (result.language === "english") delete result.language; + if (result.numbers === false) delete result.numbers; + if (result.punctuation === false) delete result.punctuation; + + if (result.mode !== "custom") delete result.customText; + + let addedResult = await ResultDAO.addResult(uid, result); + + if (isPb) { + Logger.log( + "user_new_pb", + `${result.mode + " " + result.mode2} ${result.wpm} ${result.acc}% ${ + result.rawWpm + } ${result.consistency}% (${addedResult.insertedId})`, + uid + ); + } + + return res.status(200).json({ + message: "Result saved", + isPb, + name: result.name, + tagPbs, + insertedId: addedResult.insertedId, + }); + } catch (e) { + next(e); + } + } + + static async getLeaderboard(req, res, next) { + try { + // const { type, mode, mode2 } = req.params; + // const results = await ResultDAO.getLeaderboard(type, mode, mode2); + // return res.status(200).json(results); + return res + .status(503) + .json({ message: "Leaderboard temporarily disabled" }); + } catch (e) { + next(e); + } + } + + static async checkLeaderboardQualification(req, res, next) { + try { + // const { uid } = req.decodedToken; + // const { result } = req.body; + // const data = await ResultDAO.checkLeaderboardQualification(uid, result); + // return res.status(200).json(data); + return res + .status(503) + .json({ message: "Leaderboard temporarily disabled" }); + } catch (e) { + next(e); + } + } +} + +module.exports = ResultController; + +==> ./monkeytype/backend/api/routes/leaderboards.js <== +const { authenticateRequest } = require("../../middlewares/auth"); +const LeaderboardsController = require("../controllers/leaderboards"); +const RateLimit = require("../../middlewares/rate-limit"); + +const { Router } = require("express"); + +const router = Router(); + +router.get("/", RateLimit.leaderboardsGet, LeaderboardsController.get); + +router.get( + "/rank", + RateLimit.leaderboardsGet, + authenticateRequest, + LeaderboardsController.getRank +); + +module.exports = router; + +==> ./monkeytype/backend/api/routes/preset.js <== +const { authenticateRequest } = require("../../middlewares/auth"); +const PresetController = require("../controllers/preset"); +const RateLimit = require("../../middlewares/rate-limit"); + +const { Router } = require("express"); + +const router = Router(); + +router.get( + "/", + RateLimit.presetsGet, + authenticateRequest, + PresetController.getPresets +); + +router.post( + "/add", + RateLimit.presetsAdd, + authenticateRequest, + PresetController.addPreset +); + +router.post( + "/edit", + RateLimit.presetsEdit, + authenticateRequest, + PresetController.editPreset +); + +router.post( + "/remove", + RateLimit.presetsRemove, + authenticateRequest, + PresetController.removePreset +); + +module.exports = router; + +==> ./monkeytype/backend/api/routes/core.js <== +const { authenticateRequest } = require("../../middlewares/auth"); +const { Router } = require("express"); + +const router = Router(); + +router.post("/test", authenticateRequest); + +==> ./monkeytype/backend/api/routes/user.js <== +const { authenticateRequest } = require("../../middlewares/auth"); +const { Router } = require("express"); +const UserController = require("../controllers/user"); +const RateLimit = require("../../middlewares/rate-limit"); + +const router = Router(); + +router.get( + "/", + RateLimit.userGet, + authenticateRequest, + UserController.getUser +); + +router.post( + "/signup", + RateLimit.userSignup, + authenticateRequest, + UserController.createNewUser +); + +router.post("/checkName", RateLimit.userCheckName, UserController.checkName); + +router.post( + "/delete", + RateLimit.userDelete, + authenticateRequest, + UserController.deleteUser +); + +router.post( + "/updateName", + RateLimit.userUpdateName, + authenticateRequest, + UserController.updateName +); + +router.post( + "/updateLbMemory", + RateLimit.userUpdateLBMemory, + authenticateRequest, + UserController.updateLbMemory +); + +router.post( + "/updateEmail", + RateLimit.userUpdateEmail, + authenticateRequest, + UserController.updateEmail +); + +router.post( + "/clearPb", + RateLimit.userClearPB, + authenticateRequest, + UserController.clearPb +); + +router.post( + "/tags/add", + RateLimit.userTagsAdd, + authenticateRequest, + UserController.addTag +); + +router.get( + "/tags", + RateLimit.userTagsGet, + authenticateRequest, + UserController.getTags +); + +router.post( + "/tags/clearPb", + RateLimit.userTagsClearPB, + authenticateRequest, + UserController.clearTagPb +); + +router.post( + "/tags/remove", + RateLimit.userTagsRemove, + authenticateRequest, + UserController.removeTag +); + +router.post( + "/tags/edit", + RateLimit.userTagsEdit, + authenticateRequest, + UserController.editTag +); + +router.post( + "/discord/link", + RateLimit.userDiscordLink, + authenticateRequest, + UserController.linkDiscord +); + +router.post( + "/discord/unlink", + RateLimit.userDiscordUnlink, + authenticateRequest, + UserController.unlinkDiscord +); + +module.exports = router; + +==> ./monkeytype/backend/api/routes/index.js <== +const pathOverride = process.env.API_PATH_OVERRIDE; +const BASE_ROUTE = pathOverride ? `/${pathOverride}` : ""; + +const API_ROUTE_MAP = { + "/user": require("./user"), + "/config": require("./config"), + "/results": require("./result"), + "/presets": require("./preset"), + "/psa": require("./psa"), + "/leaderboard": require("./leaderboards"), + "/quotes": require("./quotes"), +}; + +function addApiRoutes(app) { + app.get("/", (req, res) => { + res.status(200).json({ message: "OK" }); + }); + + Object.keys(API_ROUTE_MAP).forEach((route) => { + const apiRoute = `${BASE_ROUTE}${route}`; + const router = API_ROUTE_MAP[route]; + app.use(apiRoute, router); + }); +} + +module.exports = addApiRoutes; + +==> ./monkeytype/backend/api/routes/config.js <== +const { authenticateRequest } = require("../../middlewares/auth"); +const { Router } = require("express"); +const ConfigController = require("../controllers/config"); +const RateLimit = require("../../middlewares/rate-limit"); + +const router = Router(); + +router.get( + "/", + RateLimit.configGet, + authenticateRequest, + ConfigController.getConfig +); + +router.post( + "/save", + RateLimit.configUpdate, + authenticateRequest, + ConfigController.saveConfig +); + +module.exports = router; + +==> ./monkeytype/backend/api/routes/quotes.js <== +const joi = require("joi"); +const { authenticateRequest } = require("../../middlewares/auth"); +const { Router } = require("express"); +const NewQuotesController = require("../controllers/new-quotes"); +const QuoteRatingsController = require("../controllers/quote-ratings"); +const RateLimit = require("../../middlewares/rate-limit"); +const { requestValidation } = require("../../middlewares/apiUtils"); +const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quoteLanguages"); + +const quotesRouter = Router(); + +quotesRouter.get( + "/", + RateLimit.newQuotesGet, + authenticateRequest, + NewQuotesController.getQuotes +); + +quotesRouter.post( + "/", + RateLimit.newQuotesAdd, + authenticateRequest, + NewQuotesController.addQuote +); + +quotesRouter.post( + "/approve", + RateLimit.newQuotesAction, + authenticateRequest, + NewQuotesController.approve +); + +quotesRouter.post( + "/reject", + RateLimit.newQuotesAction, + authenticateRequest, + NewQuotesController.refuse +); + +quotesRouter.get( + "/rating", + RateLimit.quoteRatingsGet, + authenticateRequest, + QuoteRatingsController.getRating +); + +quotesRouter.post( + "/rating", + RateLimit.quoteRatingsSubmit, + authenticateRequest, + QuoteRatingsController.submitRating +); + +quotesRouter.post( + "/report", + RateLimit.quoteReportSubmit, + authenticateRequest, + requestValidation({ + body: { + quoteId: joi.string().required(), + quoteLanguage: joi + .string() + .valid(...SUPPORTED_QUOTE_LANGUAGES) + .required(), + reason: joi + .string() + .valid( + "Grammatical error", + "Inappropriate content", + "Low quality content" + ) + .required(), + comment: joi.string().allow("").max(250).required(), + }, + }), + (req, res) => { + res.sendStatus(200); + } +); + +module.exports = quotesRouter; + +==> ./monkeytype/backend/api/routes/psa.js <== +const { authenticateRequest } = require("../../middlewares/auth"); +const PsaController = require("../controllers/psa"); +const RateLimit = require("../../middlewares/rate-limit"); + +const { Router } = require("express"); + +const router = Router(); + +router.get("/", RateLimit.psaGet, PsaController.get); + +module.exports = router; + +==> ./monkeytype/backend/api/routes/result.js <== +const { authenticateRequest } = require("../../middlewares/auth"); +const { Router } = require("express"); +const ResultController = require("../controllers/result"); +const RateLimit = require("../../middlewares/rate-limit"); + +const router = Router(); + +router.get( + "/", + RateLimit.resultsGet, + authenticateRequest, + ResultController.getResults +); + +router.post( + "/add", + RateLimit.resultsAdd, + authenticateRequest, + ResultController.addResult +); + +router.post( + "/updateTags", + RateLimit.resultsTagsUpdate, + authenticateRequest, + ResultController.updateTags +); + +router.post( + "/deleteAll", + RateLimit.resultsDeleteAll, + authenticateRequest, + ResultController.deleteAll +); + +router.get( + "/getLeaderboard/:type/:mode/:mode2", + RateLimit.resultsLeaderboardGet, + ResultController.getLeaderboard +); + +router.post( + "/checkLeaderboardQualification", + RateLimit.resultsLeaderboardQualificationGet, + authenticateRequest, + ResultController.checkLeaderboardQualification +); + +module.exports = router; + +==> ./monkeytype/backend/jobs/deleteOldLogs.js <== +const { CronJob } = require("cron"); +const { mongoDB } = require("../init/mongodb"); + +const CRON_SCHEDULE = "0 0 0 * * *"; +const LOG_MAX_AGE_DAYS = 7; +const LOG_MAX_AGE_MILLISECONDS = LOG_MAX_AGE_DAYS * 24 * 60 * 60 * 1000; + +async function deleteOldLogs() { + const data = await mongoDB() + .collection("logs") + .deleteMany({ timestamp: { $lt: Date.now() - LOG_MAX_AGE_MILLISECONDS } }); + + Logger.log( + "system_logs_deleted", + `${data.deletedCount} logs deleted older than ${LOG_MAX_AGE_DAYS} day(s)`, + undefined + ); +} + +module.exports = new CronJob(CRON_SCHEDULE, deleteOldLogs); + +==> ./monkeytype/backend/jobs/index.js <== +const updateLeaderboards = require("./updateLeaderboards"); +const deleteOldLogs = require("./deleteOldLogs"); + +module.exports = [updateLeaderboards, deleteOldLogs]; + +==> ./monkeytype/backend/jobs/updateLeaderboards.js <== +const { CronJob } = require("cron"); +const { mongoDB } = require("../init/mongodb"); +const BotDAO = require("../dao/bot"); +const LeaderboardsDAO = require("../dao/leaderboards"); + +const CRON_SCHEDULE = "30 4/5 * * * *"; +const RECENT_AGE_MINUTES = 10; +const RECENT_AGE_MILLISECONDS = RECENT_AGE_MINUTES * 60 * 1000; + +async function getTop10(leaderboardTime) { + return await LeaderboardsDAO.get("time", leaderboardTime, "english", 0, 10); +} + +async function updateLeaderboardAndNotifyChanges(leaderboardTime) { + const top10BeforeUpdate = await getTop10(leaderboardTime); + + const previousRecordsMap = Object.fromEntries( + top10BeforeUpdate.map((record) => { + return [record.uid, record]; + }) + ); + + await LeaderboardsDAO.update("time", leaderboardTime, "english"); + + const top10AfterUpdate = await getTop10(leaderboardTime); + + const newRecords = top10AfterUpdate.filter((record) => { + const userId = record.uid; + + const userImprovedRank = + userId in previousRecordsMap && + previousRecordsMap[userId].rank > record.rank; + + const newUserInTop10 = !(userId in previousRecordsMap); + + const isRecentRecord = + record.timestamp > Date.now() - RECENT_AGE_MILLISECONDS; + + return (userImprovedRank || newUserInTop10) && isRecentRecord; + }); + + if (newRecords.length > 0) { + await BotDAO.announceLbUpdate( + newRecords, + `time ${leaderboardTime} english` + ); + } +} + +async function updateLeaderboards() { + await updateLeaderboardAndNotifyChanges("15"); + await updateLeaderboardAndNotifyChanges("60"); +} + +module.exports = new CronJob(CRON_SCHEDULE, updateLeaderboards); + +==> ./monkeytype/backend/handlers/logger.js <== +const { mongoDB } = require("../init/mongodb"); + +async function log(event, message, uid) { + console.log(new Date(), "t", event, "t", uid, "t", message); + await mongoDB().collection("logs").insertOne({ + timestamp: Date.now(), + uid, + event, + message, + }); +} + +module.exports = { + log, +}; + +==> ./monkeytype/backend/handlers/misc.js <== +module.exports = { + roundTo2(num) { + return Math.round((num + Number.EPSILON) * 100) / 100; + }, + stdDev(array) { + const n = array.length; + const mean = array.reduce((a, b) => a + b) / n; + return Math.sqrt( + array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n + ); + }, + mean(array) { + try { + return ( + array.reduce((previous, current) => (current += previous)) / + array.length + ); + } catch (e) { + return 0; + } + }, +}; + +==> ./monkeytype/backend/handlers/auth.js <== +const admin = require("firebase-admin"); + +module.exports = { + async verifyIdToken(idToken) { + return await admin.auth().verifyIdToken(idToken); + }, + async updateAuthEmail(uid, email) { + return await admin.auth().updateUser(uid, { + email, + emailVerified: false, + }); + }, +}; + +==> ./monkeytype/backend/handlers/pb.js <== +/* + + +obj structure + +time: { + 10: [ - this is a list because there can be + different personal bests for different difficulties, languages and punctuation + { + acc, + consistency, + difficulty, + language, + punctuation, + raw, + timestamp, + wpm + } + ] +}, +words: { + 10: [ + {} + ] +}, +zen: { + zen: [ + {} + ] +}, +custom: { + custom: { + [] + } +} + + + + + +*/ + +module.exports = { + checkAndUpdatePb( + obj, + lbObj, + mode, + mode2, + acc, + consistency, + difficulty, + lazyMode = false, + language, + punctuation, + raw, + wpm + ) { + //verify structure first + if (obj === undefined) obj = {}; + if (obj[mode] === undefined) obj[mode] = {}; + if (obj[mode][mode2] === undefined) obj[mode][mode2] = []; + + let isPb = false; + let found = false; + //find a pb + obj[mode][mode2].forEach((pb) => { + //check if we should compare first + if ( + (pb.lazyMode === lazyMode || + (pb.lazyMode === undefined && lazyMode === false)) && + pb.difficulty === difficulty && + pb.language === language && + pb.punctuation === punctuation + ) { + found = true; + //compare + if (pb.wpm < wpm) { + //update + isPb = true; + pb.acc = acc; + pb.consistency = consistency; + pb.difficulty = difficulty; + pb.language = language; + pb.punctuation = punctuation; + pb.lazyMode = lazyMode; + pb.raw = raw; + pb.wpm = wpm; + pb.timestamp = Date.now(); + } + } + }); + //if not found push a new one + if (!found) { + isPb = true; + obj[mode][mode2].push({ + acc, + consistency, + difficulty, + lazyMode, + language, + punctuation, + raw, + wpm, + timestamp: Date.now(), + }); + } + + if ( + lbObj && + mode === "time" && + (mode2 == "15" || mode2 == "60") && + !lazyMode + ) { + //updating lbpersonalbests object + //verify structure first + if (lbObj[mode] === undefined) lbObj[mode] = {}; + if (lbObj[mode][mode2] === undefined || Array.isArray(lbObj[mode][mode2])) + lbObj[mode][mode2] = {}; + + let bestForEveryLanguage = {}; + if (obj?.[mode]?.[mode2]) { + obj[mode][mode2].forEach((pb) => { + if (!bestForEveryLanguage[pb.language]) { + bestForEveryLanguage[pb.language] = pb; + } else { + if (bestForEveryLanguage[pb.language].wpm < pb.wpm) { + bestForEveryLanguage[pb.language] = pb; + } + } + }); + Object.keys(bestForEveryLanguage).forEach((key) => { + if (lbObj[mode][mode2][key] === undefined) { + lbObj[mode][mode2][key] = bestForEveryLanguage[key]; + } else { + if (lbObj[mode][mode2][key].wpm < bestForEveryLanguage[key].wpm) { + lbObj[mode][mode2][key] = bestForEveryLanguage[key]; + } + } + }); + bestForEveryLanguage = {}; + } + } + + return { + isPb, + obj, + lbObj, + }; + }, +}; + +==> ./monkeytype/backend/handlers/error.js <== +const uuid = require("uuid"); + +class MonkeyError { + constructor(status, message, stack = null, uid) { + this.status = status ?? 500; + this.errorID = uuid.v4(); + this.stack = stack; + // this.message = + // process.env.MODE === "dev" + // ? stack + // ? String(stack) + // : this.status === 500 + // ? String(message) + // : message + // : "Internal Server Error " + this.errorID; + + if (process.env.MODE === "dev") { + this.message = stack + ? String(message) + "\nStack: " + String(stack) + : String(message); + } else { + if (this.stack && this.status >= 500) { + this.message = "Internal Server Error " + this.errorID; + } else { + this.message = String(message); + } + } + } +} + +module.exports = MonkeyError; + +==> ./monkeytype/backend/handlers/validation.js <== +const MonkeyError = require("./error"); + +function isUsernameValid(name) { + if (name === null || name === undefined || name === "") return false; + if (/.*miodec.*/.test(name.toLowerCase())) return false; + //sorry for the bad words + if ( + /.*(bitly|fuck|bitch|shit|pussy|nigga|niqqa|niqqer|nigger|ni99a|ni99er|niggas|niga|niger|cunt|faggot|retard).*/.test( + name.toLowerCase() + ) + ) + return false; + if (name.length > 14) return false; + if (/^\..*/.test(name.toLowerCase())) return false; + return /^[0-9a-zA-Z_.-]+$/.test(name); +} + +function isTagPresetNameValid(name) { + if (name === null || name === undefined || name === "") return false; + if (name.length > 16) return false; + return /^[0-9a-zA-Z_.-]+$/.test(name); +} + +function isConfigKeyValid(name) { + if (name === null || name === undefined || name === "") return false; + if (name.length > 40) return false; + return /^[0-9a-zA-Z_.\-#+]+$/.test(name); +} + +function validateConfig(config) { + Object.keys(config).forEach((key) => { + if (!isConfigKeyValid(key)) { + throw new MonkeyError(500, `Invalid config: ${key} failed regex check`); + } + // if (key === "resultFilters") return; + // if (key === "customBackground") return; + if (key === "customBackground" || key === "customLayoutfluid") { + let val = config[key]; + if (/[<>]/.test(val)) { + throw new MonkeyError( + 500, + `Invalid config: ${key}:${val} failed regex check` + ); + } + } else { + let val = config[key]; + if (Array.isArray(val)) { + val.forEach((valarr) => { + if (!isConfigKeyValid(valarr)) { + throw new MonkeyError( + 500, + `Invalid config: ${key}:${valarr} failed regex check` + ); + } + }); + } else { + if (!isConfigKeyValid(val)) { + throw new MonkeyError( + 500, + `Invalid config: ${key}:${val} failed regex check` + ); + } + } + } + }); + return true; +} + +function validateObjectValues(val) { + let errCount = 0; + if (val === null || val === undefined) { + // + } else if (Array.isArray(val)) { + //array + val.forEach((val2) => { + errCount += validateObjectValues(val2); + }); + } else if (typeof val === "object" && !Array.isArray(val)) { + //object + Object.keys(val).forEach((valkey) => { + errCount += validateObjectValues(val[valkey]); + }); + } else { + if (!/^[0-9a-zA-Z._\-+]+$/.test(val)) { + errCount++; + } + } + return errCount; +} + +module.exports = { + isUsernameValid, + isTagPresetNameValid, + validateConfig, + validateObjectValues, +}; + +==> ./monkeytype/backend/handlers/captcha.js <== +const fetch = require("node-fetch"); +const path = require("path"); +const { config } = require("dotenv"); +config({ path: path.join(__dirname, ".env") }); + +module.exports = { + async verify(captcha) { + if (process.env.MODE === "dev") return true; + let response = await fetch( + `https://www.google.com/recaptcha/api/siteverify`, + { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: `secret=${process.env.RECAPTCHA_SECRET}&response=${captcha}`, + } + ); + response = await response.json(); + return response?.success; + }, +}; + +==> ./monkeytype/backend/handlers/pb_old.js <== +// module.exports = { +// check(result, userdata) { +// let pbs = null; +// if (result.mode == "quote") { +// return false; +// } +// if (result.funbox !== "none") { +// return false; +// } + +// pbs = userdata?.personalBests; +// if(pbs === undefined){ +// //userdao set personal best +// return true; +// } + +// // try { +// // pbs = userdata.personalBests; +// // if (pbs === undefined) { +// // throw new Error("pb is undefined"); +// // } +// // } catch (e) { +// // User.findOne({ uid: userdata.uid }, (err, user) => { +// // user.personalBests = { +// // [result.mode]: { +// // [result.mode2]: [ +// // { +// // language: result.language, +// // difficulty: result.difficulty, +// // punctuation: result.punctuation, +// // wpm: result.wpm, +// // acc: result.acc, +// // raw: result.rawWpm, +// // timestamp: Date.now(), +// // consistency: result.consistency, +// // }, +// // ], +// // }, +// // }; +// // }).then(() => { +// // return true; +// // }); +// // } + +// let toUpdate = false; +// let found = false; +// try { +// if (pbs[result.mode][result.mode2] === undefined) { +// pbs[result.mode][result.mode2] = []; +// } +// pbs[result.mode][result.mode2].forEach((pb) => { +// if ( +// pb.punctuation === result.punctuation && +// pb.difficulty === result.difficulty && +// pb.language === result.language +// ) { +// //entry like this already exists, compare wpm +// found = true; +// if (pb.wpm < result.wpm) { +// //new pb +// pb.wpm = result.wpm; +// pb.acc = result.acc; +// pb.raw = result.rawWpm; +// pb.timestamp = Date.now(); +// pb.consistency = result.consistency; +// toUpdate = true; +// } else { +// //no pb +// return false; +// } +// } +// }); +// //checked all pbs, nothing found - meaning this is a new pb +// if (!found) { +// pbs[result.mode][result.mode2] = [ +// { +// language: result.language, +// difficulty: result.difficulty, +// punctuation: result.punctuation, +// wpm: result.wpm, +// acc: result.acc, +// raw: result.rawWpm, +// timestamp: Date.now(), +// consistency: result.consistency, +// }, +// ]; +// toUpdate = true; +// } +// } catch (e) { +// // console.log(e); +// pbs[result.mode] = {}; +// pbs[result.mode][result.mode2] = [ +// { +// language: result.language, +// difficulty: result.difficulty, +// punctuation: result.punctuation, +// wpm: result.wpm, +// acc: result.acc, +// raw: result.rawWpm, +// timestamp: Date.now(), +// consistency: result.consistency, +// }, +// ]; +// toUpdate = true; +// } + +// if (toUpdate) { +// // User.findOne({ uid: userdata.uid }, (err, user) => { +// // user.personalBests = pbs; +// // user.save(); +// // }); + +// //userdao update the whole personalBests parameter with pbs object +// return true; +// } else { +// return false; +// } +// } +// } + +==> ./monkeytype/backend/credentials/.gitkeep <== + +==> ./monkeytype/.prettierignore <== +*.min.js +*.min.css +layouts.js +quotes/* +chartjs-plugin-*.js +sound/* +node_modules +css/balloon.css +_list.json + +==> ./monkeytype/.editorconfig <== +root = true + +[*.{html,js,css,scss,json,yml,yaml}] +indent_size = 2 +indent_style = space + +==> ./monkeytype/.gitignore <== +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +firebase-debug.log* + +# Firebase cache +.firebase/ + +# Firebase config + +# Uncomment this if you'd like others to create their own Firebase project. +# For a team working on the same Firebase project(s), it is recommended to leave +# it commented so all members can deploy to the same project(s) in .firebaserc. +# .firebaserc + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +#Mac files +.DS_Store + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +#vs code +.vscode +*.code-workspace + +.idea + +#firebase +.firebaserc +.firebaserc_copy +serviceAccountKey*.json + +#generated files +dist/ + +#cloudflare y +.cloudflareKey.txt +.cloudflareKey_copy.txt +purgeCfCache.sh + +static/adtest.html +backend/lastId.txt +backend/log_success.txt +backend/credentials/*.json +backend/.env + +static/adtest.html +backend/migrationStats.txt + +backend/anticheat +==> ./monkeytype/static/index.html <== + + + + + + + Monkeytype + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ :( + + It seems like the CSS failed to load. Please clear your cache to + redownload the styles. If that doesn't help contact support. +
+ (jack@monkeytype.com or discord.gg/monkeytype) +
+ (ctrl/cmd + shift + r on Chromium browsers) + + If the website works for a bit but then this screen comes back, clear + your cache again and then on Monkeytype open the command line (esc) + and search for "Clear SW cache". + +
+
+ + +
+
+
+
+
+ Important information about your account. Please click this message. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +==> ./monkeytype/static/privacy-policy.html <== + + + + + + Privacy Policy | Monkeytype + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+

+ +

+ +

Effective date: September 8, 2021

+

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

+

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

+

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

+

Table of Contents

+ + + +

What data do we collect?

+

Monkeytype collects the following data:

+
    +
  • Email
  • +
  • Username
  • +
  • Information about each typing test
  • +
  • Your currently active settings
  • +
  • How many typing tests you've started and completed
  • +
  • How long you've been typing on the website
  • +
+ +

How do we collect your data?

+ +

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

+
    +
  • Create an account
  • +
  • Complete a typing test
  • +
  • Change settings on the website
  • +
+ +

How will we use your data?

+

Monkeytype collects your data so that we can:

+ +
    +
  • + Allow you to view result history of previous tests you completed +
  • +
  • + Save results from tests you take and show you statistics based on + them +
  • +
  • Remember your settings
  • +
  • Display leaderboards
  • +
+ +

How do we store your data?

+

Monkeytype securely stores your data using Firebase Firestore.

+ +

+ What are your data protection rights? +

+

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

+
    +
  • + 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. +
  • +
  • + 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. +
  • +
  • + The right to erasure – You have the right to request that Monkeytype + erase your personal data, under certain conditions. +
  • +
  • + The right to restrict processing – You have the right to request + that Monkeytype restrict the processing of your personal data, under + certain conditions. +
  • +
  • + The right to object to processing – You have the right to object to + Monkeytype processing of your personal data, under certain + conditions. +
  • +
  • + 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. +
  • +
+ + +

What log data do we collect?

+

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

+

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

+ +

What are cookies?

+

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

+

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

+ +

How do we use cookies?

+

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

+
    +
  • Keeping you signed in
  • +
  • Remembering your active settings
  • +
  • Remembering your active tags
  • +
+

What types of cookies do we use?

+

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

+

How to manage your cookies

+

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

+

Privacy policies of other websites

+

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

+ +

Changes to our privacy policy

+

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

+ + +

How to contact us

+

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

+

+ Email: + + jack@monkeytype.com + +

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

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

+

Agreement

+

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

+ +

Limitations

+

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

+

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

+

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

+
    +
  1. is unlawful or promotes unlawful activities;
  2. +
  3. is or contains sexually obscene content;
  4. +
  5. is libelous, defamatory, or fraudulent;
  6. +
  7. is discriminatory or abusive toward any individual or group;
  8. +
  9. + 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; +
  10. +
  11. + 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 +
  12. +
  13. + 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. +
  14. +
+ +

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

+
    +
  1. + harass, abuse, threaten, or incite violence towards any individual + or group, including other Users and Monkeytype contributors; +
  2. +
  3. + 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; +
  4. +
  5. + 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; +
  6. +
  7. access the Services in ways that exceed your authorization;
  8. +
  9. + 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; +
  10. +
  11. + violate the privacy of any third party, such as by posting another + person's personal information without their consent; +
  12. +
  13. + 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; +
  14. +
  15. + facilitate or encourage any violations of this Agreement or + interfere with the operation, appearance, security, or functionality + of the Services; or +
  16. +
  17. use the Services in any manner that is harmful to minors.
  18. +
+

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

+

Privacy Policy

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

Limitations on Automated Use

+ You shouldn't use bots or access our Services in malicious or + un-permitted ways. While accessing or using the Services, you may not: +
    +
  1. use bots, hacks, or cheats while using our site;
  2. +
  3. create manual requests to Monkeytype servers;
  4. +
  5. + tamper with or use non-public areas of the Services, or the computer + or delivery systems of Monkeytype and/or its service providers; +
  6. +
  7. + 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; +
  8. +
  9. + 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; +
  10. +
  11. + 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; +
  12. +
  13. + 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 +
  14. +
  15. + 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. +
  16. +
+

Links

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

Changes

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

Disclaimer

+

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

+

Contact

+

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

+

+ Email: + + jack@monkeytype.com + +

+ Discord: + + Miodec#1512 + +
+

+ Terms based on + Glitch terms +

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

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

+

Table of Contents

+ + + +

How to Disclose a Vulnerability

+

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

+
    +
  • Vulnerability Description
  • +
  • Proof of Concept
  • +
  • Impact
  • +
  • Screenshots or Proof
  • +
+ +

Submission Guidelines

+

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

+
+
+ + + + + +==> ./monkeytype/static/themes/terminal.css <== +:root { + --bg-color: #191a1b; + --caret-color: #79a617; + --main-color: #79a617; + --sub-color: #48494b; + --text-color: #e7eae0; + --error-color: #a61717; + --error-extra-color: #731010; + --colorful-error-color: #a61717; + --colorful-error-extra-color: #731010; +} + +==> ./monkeytype/static/themes/ms_cupcakes.css <== +:root { + --bg-color: #ffffff; + --main-color: #5ed5f3; + --caret-color: #303030; + --sub-color: #d64090; + --text-color: #0a282f; + --error-color: #000000; + --error-extra-color: #c9c9c9; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/fledgling.css <== +:root { + --bg-color: #3b363f; + --main-color: #fc6e83; + --caret-color: #474747; + --sub-color: #ead8d6; + --text-color: #fc6e83; + --error-color: #f52443; + --error-extra-color: #bd001c; + --colorful-error-color: #ff0a2f; + --colorful-error-extra-color: #000000; +} + +==> ./monkeytype/static/themes/horizon.css <== +:root { + --bg-color: #1C1E26; + --main-color:#c4a88a; + --caret-color: #BBBBBB; + --sub-color: #db886f; + --text-color: #bbbbbb; + --error-color: #D55170; + --error-extra-color: #ff3d3d; + --colorful-error-color: #D55170; + --colorful-error-extra-color:#D55170; +} + +#menu .icon-button:nth-child(1) { + color: #D55170; +} + +#menu .icon-button:nth-child(2) { + color: #E4A88A; +} + +#menu .icon-button:nth-child(3) { + color: #DB886F; +} + +#menu .icon-button:nth-child(4) { + color: #DB887A; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #FFC819; +} + +==> ./monkeytype/static/themes/vaporwave.css <== +:root { + --bg-color: #a4a7ea; + --main-color: #e368da; + --caret-color: #28cafe; + --sub-color: #7c7faf; + --text-color: #f1ebf1; + --error-color: #573ca9; + --error-extra-color: #3d2b77; + --colorful-error-color: #28cafe; + --colorful-error-extra-color: #25a9ce; +} + +==> ./monkeytype/static/themes/witch_girl.css <== +:root { + --bg-color: #f3dbda; + --main-color: #56786a; + --caret-color: #afc5bd; + --sub-color: #ddb4a7; + --text-color: #56786a; + --error-color: #b29a91; + --error-extra-color: #b29a91; + --colorful-error-color: #b29a91; + --colorful-error-extra-color: #b29a91; +} + +==> ./monkeytype/static/themes/gruvbox_light.css <== +:root { + --bg-color: #fbf1c7; + --main-color: #689d6a; + --caret-color: #689d6a; + --sub-color: #a89984; + --text-color: #3c3836; + --error-color: #cc241d; + --error-extra-color: #9d0006; + --colorful-error-color: #cc241d; + --colorful-error-extra-color: #9d0006; +} + +==> ./monkeytype/static/themes/soaring_skies.css <== +:root { + --bg-color: #fff9f2; + --main-color: #55c6f0; + --caret-color: #1e107a; + --sub-color: #1e107a; + --text-color: #1d1e1e; + --error-color: #fb5745; + --error-extra-color: #b03c30; + --colorful-error-color: #fb5745; + --colorful-error-extra-color: #b03c30; +} + +==> ./monkeytype/static/themes/terra.css <== +:root { + --bg-color: #0c100e; + --main-color: #89c559; + --caret-color: #89c559; + --sub-color: #436029; + --text-color: #f0edd1; + --error-color: #d3ca78; + --error-extra-color: #89844d; + --colorful-error-color: #d3ca78; + --colorful-error-extra-color: #89844d; +} + +==> ./monkeytype/static/themes/camping.css <== +:root { + --bg-color: #faf1e4; + --main-color: #618c56; + --caret-color: #618c56; + --sub-color: #c2b8aa; + --text-color: #3c403b; + --error-color: #ad4f4e; + --error-extra-color: #7e3a39; + --colorful-error-color: #ad4f4e; + --colorful-error-extra-color: #7e3a39; +} + +#top .logo .bottom { + color: #ad4f4e; +} + +==> ./monkeytype/static/themes/lil_dragon.css <== +:root { + --bg-color: #ebe1ef; + --main-color: #8a5bd6; + --caret-color: #212b43; + --sub-color: #ac76e5; + --text-color: #212b43; + --error-color: #f794ca; + --error-extra-color: #f279c2; + --colorful-error-color: #f794ca; + --colorful-error-extra-color: #f279c2; +} + +#menu .icon-button { + color: #ba96db; +} + +#menu .icon-button:hover { + color: #212b43; +} + +==> ./monkeytype/static/themes/laser.css <== +:root { + --bg-color: #221b44; + --main-color: #009eaf; + --caret-color: #009eaf; + --sub-color: #b82356; + --text-color: #dbe7e8; + --error-color: #a8d400; + --error-extra-color: #668000; + --colorful-error-color: #a8d400; + --colorful-error-extra-color: #668000; +} + +==> ./monkeytype/static/themes/desert_oasis.css <== +:root { + --bg-color: #fff2d5; /*Background*/ + --main-color: #d19d01; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ + --caret-color: #3a87fe; /*Cursor Color*/ + --sub-color: #0061fe; /*WPM text color of scrollbar and general color, before typed color*/ + --text-color: #332800; /*Color of text after hovering over it*/ + --error-color: #76bb40; + --error-extra-color: #4e7a27; + --colorful-error-color: #76bb40; + --colorful-error-extra-color: #4e7a27; +} + +#menu .icon-button:nth-child(1) { + color: #76bb40; +} + +#menu .icon-button:nth-child(2) { + color: #76bb40; +} + +#menu .icon-button:nth-child(3) { + color: #76bb40; +} + +#menu .icon-button:nth-child(4) { + color: #76bb40; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #76bb40; +} + +==> ./monkeytype/static/themes/honey.css <== +:root { + --bg-color: #f2aa00; + --main-color: #fff546; + --caret-color: #795200; + --sub-color: #a66b00; + --text-color: #f3eecb; + --error-color: #df3333; + --error-extra-color: #6d1f1f; + --colorful-error-color: #df3333; + --colorful-error-extra-color: #6d1f1f; +} + +==> ./monkeytype/static/themes/9009.css <== +:root { + --bg-color: #eeebe2; + --main-color: #080909; + --caret-color: #7fa480; + --sub-color: #99947f; + --text-color: #080909; + --error-color: #c87e74; + --colorful-error-color: #a56961; + --colorful-error-color: #c87e74; + --colorful-error-extra-color: #a56961; +} + +.word letter.incorrect { + color: var(--error-color); +} + +.word letter.incorrect.extra { + color: var(--colorful-error-color); +} + +.word.error { + border-bottom: solid 2px var(--error-color); +} + +key { + color: var(--sub-color); + background-color: var(--main-color); +} + +#menu .icon-button { + color: var(--main-color); +} + +#menu .icon-button:nth-child(1) { + color: var(--error-color); +} + +#menu .icon-button:nth-child(4) { + color: var(--caret-color); +} + +==> ./monkeytype/static/themes/sweden.css <== +:root { + --bg-color: #0058a3; + --main-color: #ffcc02; + --caret-color: #b5b5b5; + --sub-color: #57abdb; + --text-color: #ffffff; + --error-color: #e74040; + --error-extra-color: #a22f2f; + --colorful-error-color: #f56674; + --colorful-error-extra-color: #e33546; +} + +==> ./monkeytype/static/themes/solarized_dark.css <== +:root { + --bg-color: #002b36; + --main-color: #859900; + --caret-color: #dc322f; + --sub-color: #2aa198; + --text-color: #268bd2; + --error-color: #d33682; + --error-extra-color: #9b225c; + --colorful-error-color: #d33682; + --colorful-error-extra-color: #9b225c; +} + +==> ./monkeytype/static/themes/mizu.css <== +:root { + --bg-color: #afcbdd; + --main-color: #fcfbf6; + --caret-color: #fcfbf6; + --sub-color: #85a5bb; + --text-color: #1a2633; + --error-color: #bf616a; + --error-extra-color: #793e44; + --colorful-error-color: #bf616a; + --colorful-error-extra-color: #793e44; +} + +==> ./monkeytype/static/themes/terror_below.css <== +:root { + --bg-color: #0b1e1a; + --caret-color: #66ac92; + --main-color: #66ac92; + --sub-color: #015c53; + --text-color: #dceae5; + --error-color: #bf616a; + --error-extra-color: #793e44; + --colorful-error-color: #bf616a; + --colorful-error-extra-color: #793e44; +} + +==> ./monkeytype/static/themes/bingsu.css <== +:root { + /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */ + --bg-color: #b8a7aa; + --main-color: #83616e; + --caret-color: #ebe6ea; + --sub-color: #48373d; + --text-color: #ebe6ea; + --error-color: #921341; + --error-extra-color: #640b2c; + --colorful-error-color: #921341; + --colorful-error-extra-color: #640b2c; +} + +/* .word.error{ + border-bottom: double 4px var(--error-color); +} */ + +#menu .icon-button:nth-child(1) { + color: var(--caret-color); +} + +==> ./monkeytype/static/themes/botanical.css <== +:root { + --bg-color: #7b9c98; + --main-color: #eaf1f3; + --caret-color: #abc6c4; + --sub-color: #495755; + --text-color: #eaf1f3; + --error-color: #f6c9b4; + --error-extra-color: #f59a71; + --colorful-error-color: #f6c9b4; + --colorful-error-extra-color: #f59a71; +} + +==> ./monkeytype/static/themes/iceberg_dark.css <== +:root { + --bg-color: #161821; + --caret-color: #d2d4de; + --main-color: #84a0c6; + --sub-color: #595e76; + --text-color: #c6c8d1; + --error-color: #e27878; + --error-extra-color: #e2a478; + --colorful-error-color: #e27878; + --colorful-error-extra-color: #e2a478; +} + +==> ./monkeytype/static/themes/aether.css <== +:root { + --bg-color: #101820; + --main-color: #eedaea; + --caret-color: #eedaea; + --sub-color: #cf6bdd; + --text-color: #eedaea; + --error-color: #ff5253; + --error-extra-color: #e3002b; + --colorful-error-color: #ff5253; + --colorful-error-extra-color: #e3002b; +} + +#menu .icon-button:nth-child(1) { + color: #e4002b; +} + +#menu .icon-button:nth-child(2) { + color: #c53562; +} + +#menu .icon-button:nth-child(3) { + color: #95549e; +} + +#menu .icon-button:nth-child(4) { + color: #6744a1; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #393c73; +} + +==> ./monkeytype/static/themes/magic_girl.css <== +:root { + --bg-color: #ffffff; + --main-color: #f5b1cc; + --caret-color: #e45c96; + --sub-color: #93e8d3; + --text-color: #00ac8c; + --error-color: #ffe495; + --error-extra-color: #e45c96; + --colorful-error-color: #ffe485; + --colorful-error-extra-color: #e45c96; +} + +==> ./monkeytype/static/themes/mashu.css <== +:root { + --bg-color: #2b2b2c; + --main-color: #76689a; + --caret-color: #76689a; + --sub-color: #d8a0a6; + --text-color: #f1e2e4; + --error-color: #d44729; + --error-extra-color: #8f2f19; + --colorful-error-color: #d44729; + --colorful-error-extra-color: #8f2f19; +} + +==> ./monkeytype/static/themes/dark_magic_girl.css <== +:root { + --bg-color: #091f2c; + --main-color: #f5b1cc; + --caret-color: #93e8d3; + --sub-color: #93e8d3; + --text-color: #a288d9; + --error-color: #e45c96; + --error-extra-color: #e45c96; + --colorful-error-color: #00b398; + --colorful-error-extra-color: #e45c96; +} + +==> ./monkeytype/static/themes/mint.css <== +:root { + --bg-color: #05385b; + --main-color: #5cdb95; + --caret-color: #5cdb95; + --sub-color: #20688a; + --text-color: #edf5e1; + --error-color: #f35588; + --error-extra-color: #a3385a; + --colorful-error-color: #f35588; + --colorful-error-extra-color: #a3385a; +} + +==> ./monkeytype/static/themes/rudy.css <== +:root { + --bg-color: #1a2b3e; + --caret-color: #af8f5c; + --main-color: #af8f5c; + --sub-color: #3a506c; + --text-color: #c9c8bf; + --error-color: #bf616a; + --error-extra-color: #793e44; + --colorful-error-color: #bf616a; + --colorful-error-extra-color: #793e44; +} + +==> ./monkeytype/static/themes/dev.css <== +/*this theme is based on "Dev theme by KDr3w" color pallet: https://www.deviantart.com/kdr3w/art/Dev-825722799 */ +:root { + --bg-color: #1b2028; + --main-color: #23a9d5; + --caret-color: #4b5975; + --sub-color: #4b5975; + --text-color: #ccccb5; + --error-color: #b81b2c; + --error-extra-color: #84131f; + --colorful-error-color: #b81b2c; + --colorful-error-extra-color: #84131f; +} + +==> ./monkeytype/static/themes/dollar.css <== +:root { + --bg-color: #e4e4d4; + --main-color: #6b886b; + --caret-color: #424643; + --sub-color: #8a9b69; + --text-color: #555a56; + --error-color: #d60000; + --error-extra-color: #f68484; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/onedark.css <== +:root { + --bg-color: #2f343f; + --caret-color: #61afef; + --main-color: #61afef; + --sub-color: #eceff4; + --text-color: #98c379; + --error-color: #e06c75; + --error-extra-color: #d62436; + --colorful-error-color: #d62436; + --colorful-error-extra-color: #ff0019; +} + +==> ./monkeytype/static/themes/red_dragon.css <== +:root { + --bg-color: #1a0b0c; + --main-color: #ff3a32; + --caret-color: #ff3a32; + --sub-color: #e2a528; + --text-color: #4a4d4e; + --error-color: #771b1f; + --error-extra-color: #591317; + --colorful-error-color: #771b1f; + --colorful-error-extra-color: #591317; +} + +==> ./monkeytype/static/themes/tiramisu.css <== +:root { + --bg-color: #cfc6b9; + --main-color: #c0976f; + --caret-color: #7d5448; + --sub-color: #c0976f; + --text-color: #7d5448; + --error-color: #e9632d; + --error-extra-color: #e9632d; + --colorful-error-color: #e9632d; + --colorful-error-extra-color: #e9632d; +} + +==> ./monkeytype/static/themes/midnight.css <== +:root { + --bg-color: #0b0e13; + --main-color: #60759f; + --caret-color: #60759f; + --sub-color: #394760; + --text-color: #9fadc6; + --error-color: #c27070; + --error-extra-color: #c28b70; + --colorful-error-color: #c27070; + --colorful-error-extra-color: #c28b70; +} + +==> ./monkeytype/static/themes/dots.css <== +:root { + --bg-color: #121520; + --caret-color: #fff; + --main-color: #fff; + --sub-color: #676e8a; + --text-color: #fff; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #da3333; + --colorful-error-extra-color: #791717; +} + +#menu { + gap: 0.5rem; +} + +#top.focus #menu .icon-button, +#top.focus #menu:before, +#top.focus #menu:after { + background: var(--sub-color); +} + +#menu .icon-button { + border-radius: 10rem !important; + color: #121520; +} + +/* #menu:before{ + content: ""; + background: #f94348; + width: 1.25rem; + height: 1.25rem; + padding: .5rem; + border-radius: 10rem; +} */ + +#menu .icon-button:nth-child(1) { + background: #f94348; +} + +#menu .icon-button:nth-child(2) { + background: #9261ff; +} + +#menu .icon-button:nth-child(3) { + background: #3cc5f8; +} + +#menu .icon-button:nth-child(4) { + background: #4acb8a; +} + +#menu .icon-button:nth-child(5) { + background: #ffd543; +} + +#menu .icon-button:nth-child(6), +#menu .icon-button:nth-child(7) { + background: #ff9349; +} + +/* #menu:after{ + content: ""; + background: #ff9349; + width: 1.25rem; + height: 1.25rem; + padding: .5rem; + border-radius: 10rem; +} */ + +#top.focus #menu .icon-button.discord::after { + border-color: transparent; +} + +==> ./monkeytype/static/themes/ez_mode.css <== +:root { + --bg-color: #0068c6; + --main-color: #fa62d5; + --caret-color: #4ddb47; + --sub-color: #f5f5f5; + --text-color: #fa62d5; + --error-color: #4ddb47; + --error-extra-color: #42ba3b; + --colorful-error-color: #4ddb47; + --colorful-error-extra-color: #42ba3b; +} + +.pageSettings .section h1 { + color: var(--text-color); +} + +.pageSettings .section > .text { + color: var(--sub-color); +} + +.pageAbout .section .title { + color: var(--text-color); +} + +.pageAbout .section p { + color: var(--sub-color); +} + +#leaderboardsWrapper #leaderboards .title { + color: var(--sub-color); +} + +#leaderboardsWrapper #leaderboards .tables table thead { + color: var(--sub-color); +} + +#leaderboardsWrapper #leaderboards .tables table tbody { + color: var(--sub-color); +} + +==> ./monkeytype/static/themes/matrix.css <== +:root { + --bg-color: #000000; + --main-color: #15ff00; + --caret-color: #15ff00; + --sub-color: #003B00; + --text-color: #adffa7; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #da3333; + --colorful-error-extra-color: #791717; +} + +#liveWpm, +#timerNumber { + color: white; +} + +==> ./monkeytype/static/themes/aurora.css <== +:root { + --bg-color: #011926; + --main-color: #00e980; + --caret-color: #00e980; + --sub-color: #245c69; + --text-color: #fff; + --error-color: #b94da1; + --error-extra-color: #9b3a76; + --colorful-error-color: #b94da1; + --colorful-error-extra-color: #9b3a76; +} + +@keyframes rgb { + 0% { + color: #009fb4; + } + 25% { + color: #00e975; + } + 50% { + color: #00ffea; + } + 75% { + color: #00e975; + } + 100% { + color: #009fb4; + } +} + +@keyframes rgb-bg { + 0% { + background: #009fb4; + } + 25% { + background: #00e975; + } + 50% { + background: #00ffea; + } + 75% { + background: #00e975; + } + 100% { + background: #009fb4; + } +} + +.button.discord::after, +#caret, +.pageSettings .section .buttons .button.active, +.pageSettings .section.languages .buttons .language.active, +.pageAccount .group.filterButtons .buttons .button.active { + animation: rgb-bg 5s linear infinite; +} + +#top.focus .button.discord::after, +#top .button.discord.dotHidden::after { + animation-name: none !important; +} + +.logo .bottom, +#top .config .group .buttons .text-button.active, +#result .stats .group .bottom, +#menu .icon-button:hover, +#top .config .group .buttons .text-button:hover, +a:hover, +#words.flipped .word { + animation: rgb 5s linear infinite; +} + +#words.flipped .word letter.correct { + color: var(--sub-color); +} + +#words:not(.flipped) .word letter.correct { + animation: rgb 5s linear infinite; +} + +==> ./monkeytype/static/themes/night_runner.css <== +:root { + --bg-color: #212121; + --main-color: #feff04; + --caret-color: #feff04; + --sub-color: #5c4a9c; + --text-color: #e8e8e8; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #da3333; + --colorful-error-extra-color: #791717; +} + +==> ./monkeytype/static/themes/taro.css <== +:root { + /* --bg-color: linear-gradient(215deg, #cbb8ba, #706768); */ + --bg-color: #b3baff; + --main-color: #130f1a; + --caret-color: #00e9e5; + --sub-color: #6f6c91; + --text-color: #130f1a; + --error-color: #ffe23e; + --error-extra-color: #fff1c3; + --colorful-error-color: #ffe23e; + --colorful-error-extra-color: #fff1c3; +} + +.word.error { + border-bottom: dotted 2px var(--text-color); +} + +#menu .icon-button:nth-child(1) { + background: var(--caret-color); + border-radius: 50%; +} + +#menu .icon-button:nth-child(2) { + background: var(--error-color); + border-radius: 50%; +} + +==> ./monkeytype/static/themes/vscode.css <== +:root { + --bg-color: #1e1e1e; + --main-color: #007acc; + --caret-color: #569cd6; + --sub-color: #4d4d4d; + --text-color: #d4d4d4; + --error-color: #f44747; + --error-extra-color: #f44747; + --colorful-error-color: #f44747; + --colorful-error-extra-color: #f44747; +} + +==> ./monkeytype/static/themes/nord.css <== +:root { + --bg-color: #242933; + --caret-color: #d8dee9; + --main-color: #d8dee9; + --sub-color: #617b94; + --text-color: #d8dee9; + --error-color: #bf616a; + --error-extra-color: #793e44; + --colorful-error-color: #bf616a; + --colorful-error-extra-color: #793e44; +} + +==> ./monkeytype/static/themes/iceberg_light.css <== +:root { + --bg-color: #e8e9ec; + --caret-color: #262a3f; + --main-color: #2d539e; + --sub-color: #adb1c4; + --text-color: #33374c; + --error-color: #cc517a; + --error-extra-color: #cc3768; + --colorful-error-color: #cc517a; + --colorful-error-extra-color: #cc3768; +} + +==> ./monkeytype/static/themes/wavez.css <== +:root { + --bg-color: #1c292f; + --main-color: #6bde3b; + --caret-color: #6bde3b; + --sub-color: #1a454e; + --text-color: #e9efe6; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/pink_lemonade.css <== +:root { + --bg-color: #f6d992; + --main-color: #f6a192; + --caret-color: #fcfcf8; + --sub-color: #f6b092; + --text-color: #fcfcf8; + --error-color: #ff6f69; + --error-extra-color: #ff6f69; + --colorful-error-color: #ff6f69; + --colorful-error-extra-color: #ff6f69; +} + +==> ./monkeytype/static/themes/dualshot.css <== +:root { + --bg-color: #737373; + --main-color: #212222; + --caret-color: #212222; + --sub-color: #aaaaaa; + --text-color: #212222; + --error-color: #c82931; + --error-extra-color: #ac1823; + --colorful-error-color: #c82931; + --colorful-error-extra-color: #ac1823; +} + +#menu .icon-button:nth-child(1) { + color: #2884bb; +} + +#menu .icon-button:nth-child(2) { + color: #25a5a9; +} + +#menu .icon-button:nth-child(3) { + color: #de9c24; +} + +#menu .icon-button:nth-child(4) { + color: #d82231; +} + +#menu .icon-button:nth-child(5) { + color: #212222; +} + +#menu .icon-button:nth-child(6) { + color: #212222; +} + +==> ./monkeytype/static/themes/material.css <== +:root { + --bg-color: #263238; + --main-color: #80cbc4; + --caret-color: #80cbc4; + --sub-color: #4c6772; + --text-color: #e6edf3; + --error-color: #fb4934; + --error-extra-color: #cc241d; + --colorful-error-color: #fb4934; + --colorful-error-extra-color: #cc241d; +} + +==> ./monkeytype/static/themes/paper.css <== +:root { + --bg-color: #eeeeee; + --main-color: #444444; + --caret-color: #444444; + --sub-color: #b2b2b2; + --text-color: #444444; + --error-color: #d70000; + --error-extra-color: #d70000; + --colorful-error-color: #d70000; + --colorful-error-extra-color: #d70000; +} + +==> ./monkeytype/static/themes/metropolis.css <== +:root { + --bg-color: #0f1f2c; + --main-color: #56c3b7; + --caret-color: #56c3b7; + --sub-color: #326984; + --text-color: #e4edf1; + --error-color: #d44729; + --error-extra-color: #8f2f19; + --colorful-error-color: #d44729; + --colorful-error-extra-color: #8f2f19; +} + +#top .logo .bottom { + color: #f4bc46; +} + +#menu .icon-button:nth-child(1) { + color: #d44729; +} + +#menu .icon-button:nth-child(2) { + color: #d44729; +} + +#menu .icon-button:nth-child(3) { + color: #d44729; +} + +#menu .icon-button:nth-child(4) { + color: #d44729; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6), +#menu .icon-button:nth-child(7) { + color: #d44729; +} + +==> ./monkeytype/static/themes/nebula.css <== +:root { + --bg-color: #212135; + --main-color: #be3c88; + --caret-color: #78c729; + --sub-color: #19b3b8; + --text-color: #838686; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/alpine.css <== +:root { + --bg-color: #6c687f; /*Background*/ + --main-color: #ffffff; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ + --caret-color: #585568; /*Cursor Color*/ + --sub-color: #9994b8; /*WPM text color of scrollbar and general color, before typed color*/ + --text-color: #ffffff; /*Color of text after hovering over it*/ + --error-color: #e32b2b; + --error-extra-color: #a62626; + --colorful-error-color: #e32b2b; + --colorful-error-extra-color: #a62626; +} + +==> ./monkeytype/static/themes/rgb.css <== +:root { + --bg-color: #111; + --main-color: #eee; + --caret-color: #eee; + --sub-color: #444; + --text-color: #eee; + --error-color: #eee; + --error-extra-color: #b3b3b3; + --colorful-error-color: #eee; + --colorful-error-extra-color: #b3b3b3; +} + +@keyframes rgb { + 0% { + color: #f44336; + } + 25% { + color: #ffc107; + } + 50% { + color: #4caf50; + } + 75% { + color: #3f51b5; + } + 100% { + color: #f44336; + } +} + +@keyframes rgb-bg { + 0% { + background: #f44336; + } + 25% { + background: #ffc107; + } + 50% { + background: #4caf50; + } + 75% { + background: #3f51b5; + } + 100% { + background: #f44336; + } +} + +.button.discord::after, +#caret, +.pageSettings .section .buttons .button.active, +.pageSettings .section.languages .buttons .language.active, +.pageAccount .group.filterButtons .buttons .button.active { + animation: rgb-bg 5s linear infinite; +} + +#top.focus .button.discord::after, +#top .button.discord.dotHidden::after { + animation-name: none !important; +} + +.logo .bottom, +#top .config .group .buttons .text-button.active, +#result .stats .group .bottom, +#menu .icon-button:hover, +#top .config .group .buttons .text-button:hover, +a:hover, +#words.flipped .word { + animation: rgb 5s linear infinite; +} + +/* .word letter.correct{ + animation: rgb 5s linear infinite; + +} */ + +#words.flipped .word letter.correct { + color: var(--sub-color); +} + +#words:not(.flipped) .word letter.correct { + animation: rgb 5s linear infinite; +} + +==> ./monkeytype/static/themes/hanok.css <== +:root { + --bg-color: #d8d2c3; + --main-color: #513a2a; + --caret-color: #513a2a; + --sub-color: #513a2a; + --text-color: #393b3b; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/serika_dark.css <== +:root { + --bg-color: #323437; + --main-color: #e2b714; + --caret-color: #e2b714; + --sub-color: #646669; + --text-color: #d1d0c5; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/modern_ink.css <== +:root { + --bg-color: #ffffff; + --main-color: #ff360d; + --caret-color: #ff0000; + --sub-color: #b7b7b7; + --text-color: #000000; + --error-color: #d70000; + --error-extra-color: #b00000; + --colorful-error-color: #ff1c1c; + --colorful-error-extra-color: #b00000; +} + +#menu .icon-button:nth-child(1) { + color: #ff0000; +} + +#menu .icon-button:nth-child(5) { + color: #ff0000; +} + +/* kinda confusing to type with this */ +/* .word letter.incorrect { + -webkit-transform: scale(0.5) translate(-100%, -100%); +} + +.word letter.incorrect.extra { + -webkit-transform: scale(0.5); +} */ + +.word.error { + border-bottom: solid 2px #ff0000; +} + +==> ./monkeytype/static/themes/muted.css <== +:root { + --bg-color: #525252; + --main-color: #C5B4E3; + --caret-color: #B1E4E3; + --sub-color: #939eae; + --text-color: #B1E4E3; + --error-color: #EDC1CD; + } + +==> ./monkeytype/static/themes/nautilus.css <== +:root { + --bg-color: #132237; + --main-color: #ebb723; + --caret-color: #ebb723; + --sub-color: #0b4c6c; + --text-color: #1cbaac; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #da3333; + --colorful-error-extra-color: #791717; +} + +==> ./monkeytype/static/themes/miami_nights.css <== +:root { + --bg-color: #18181a; + --main-color: #e4609b; + --caret-color: #e4609b; + --sub-color: #47bac0; + --text-color: #fff; + --error-color: #fff591; + --error-extra-color: #b6af68; + --colorful-error-color: #fff591; + --colorful-error-extra-color: #b6af68; +} + +==> ./monkeytype/static/themes/moonlight.css <== +/*inspired by GMK MOONLIGHT*/ +:root { + --bg-color: #1f2730; + --main-color: #c69f68; + --caret-color: #8f744b; + --sub-color: #4b5975; + --text-color: #ccccb5; + --error-color: #b81b2c; + --error-extra-color: #84131f; + --colorful-error-color: #b81b2c; + --colorful-error-extra-color: #84131f; +} +#menu { + gap: 0.5rem; +} +#top.focus #menu .icon-button, +#top.focus #menu:before, +#top.focus #menu:after { + background: var(--bg-color); +} +#menu .icon-button { + border-radius: rem !important; + color: #1f2730 !important; +} +#menu .icon-button :hover { + border-radius: rem !important; + color: #4b5975 !important; + transition: 0.25s; +} +#menu .icon-button:nth-child(1) { + background: #c69f68; +} +#menu .icon-button:nth-child(2) { + background: #c69f68; +} +#menu .icon-button:nth-child(3) { + background: #c69f68; +} +#menu .icon-button:nth-child(4) { + background: #c69f68; +} +#menu .icon-button:nth-child(5) { + background: #c69f68; +} +#menu .icon-button:nth-child(6), +#menu .icon-button:nth-child(7) { + background: #c69f68; +} +#top.focus #menu .icon-button.discord::after { + border-color: transparent; +} + +==> ./monkeytype/static/themes/darling.css <== +:root { + --bg-color: #fec8cd; + --main-color: #ffffff; + --caret-color: #ffffff; + --sub-color: #a30000; + --text-color: #ffffff; + --error-color: #2e7dde; + --error-extra-color: #2e7dde; + --colorful-error-color: #2e7dde; + --colorful-error-extra-color: #2e7dde; + --font: Roboto Mono; +} + +==> ./monkeytype/static/themes/pastel.css <== +:root { + --bg-color: #e0b2bd; + --main-color: #fbf4b6; + --caret-color: #fbf4b6; + --sub-color: #b4e9ff; + --text-color: #6d5c6f; + --error-color: #ff6961; + --error-extra-color: #c23b22; + --colorful-error-color: #ff6961; + --colorful-error-extra-color: #c23b22; +} + +==> ./monkeytype/static/themes/dark.css <== +:root { + --bg-color: #111; + --main-color: #eee; + --caret-color: #eee; + --sub-color: #444; + --text-color: #eee; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #da3333; + --colorful-error-extra-color: #791717; +} + +==> ./monkeytype/static/themes/ishtar.css <== +:root { + --bg-color: #202020; + --main-color: #91170c; + --caret-color: #c58940; + --sub-color: #847869; + --text-color: #fae1c3; + --error-color: #bb1e10; + --error-extra-color: #791717; + --colorful-error-color: #c5da33; + --colorful-error-extra-color: #849224; +} + +#top .logo .bottom { + color: #fae1c3; +} + +==> ./monkeytype/static/themes/retro.css <== +:root { + --bg-color: #dad3c1; + --main-color: #1d1b17; + --caret-color: #1d1b17; + --sub-color: #918b7d; + --text-color: #1d1b17; + --error-color: #bf616a; + --error-extra-color: #793e44; + --colorful-error-color: #bf616a; + --colorful-error-extra-color: #793e44; +} + +==> ./monkeytype/static/themes/stealth.css <== +:root { + --bg-color: #010203; + --main-color: #383e42; + --caret-color: #e25303; + --sub-color: #5e676e; + --text-color: #383e42; + --error-color: #e25303; + --error-extra-color: #73280c; + --colorful-error-color: #e25303; + --colorful-error-extra-color: #73280c; +} +#menu .icon-button:nth-child(4) { + color: #e25303; +} +#timerNumber { + color: #5e676e; +} + +==> ./monkeytype/static/themes/repose_dark.css <== +:root { + --bg-color: #2F3338; + --main-color: #D6D2BC; + --caret-color: #D6D2BC; + --sub-color: #8F8E84; + --text-color: #D6D2BC; + --error-color: #FF4A59; + --error-extra-color: #C43C53; + --colorful-error-color: #FF4A59; + --colorful-error-extra-color: #C43C53; +} + +==> ./monkeytype/static/themes/lime.css <== +:root { + --bg-color: #7c878e; + --main-color: #93c247; + --caret-color: #93c247; + --sub-color: #4b5257; + --text-color: #bfcfdc; + --error-color: #ea4221; + --error-extra-color: #7e2a33; + --colorful-error-color: #ea4221; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/blueberry_light.css <== +:root { + --bg-color: #dae0f5; + --main-color: #506477; + --caret-color: #df4576; + --sub-color: #92a4be; + --text-color: #678198; + --error-color: #df4576; + --error-extra-color: #d996ac; + --colorful-error-color: #df4576; + --colorful-error-extra-color: #d996ac; +} + +#top .logo .bottom { + color: #df4576; +} + +==> ./monkeytype/static/themes/fruit_chew.css <== +:root { + --bg-color: #d6d3d6; + --main-color: #5c1e5f; + --caret-color: #b92221; + --sub-color: #b49cb5; + --text-color: #282528; + --error-color: #bd2621; + --error-extra-color: #a62626; + --colorful-error-color: #bd2621; + --colorful-error-extra-color: #a62626; +} + +#menu .icon-button:nth-child(1) { + color: #a6bf50; +} + +#menu .icon-button:nth-child(2) { + color: #c3921a; +} + +#menu .icon-button:nth-child(3) { + color: #b92221; +} + +#menu .icon-button:nth-child(4) { + color: #88b6ce; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #661968; +} + +==> ./monkeytype/static/themes/olivia.css <== +:root { + --bg-color: #1c1b1d; + --main-color: #deaf9d; + --caret-color: #deaf9d; + --sub-color: #4e3e3e; + --text-color: #f2efed; + --error-color: #bf616a; + --error-extra-color: #793e44; + --colorful-error-color: #e03d4e; + --colorful-error-extra-color: #aa2f3b; +} + +==> ./monkeytype/static/themes/diner.css <== +:root { + --bg-color: #537997; + --main-color: #c3af5b; + --caret-color: #ad5145; + --sub-color: #445c7f; + --text-color: #dfdbc8; + --error-color: #ad5145; + --error-extra-color: #7e2a33; + --colorful-error-color: #ad5145; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/sewing_tin_light.css <== +:root { + --bg-color: #ffffff; + --main-color: #2d2076; + --caret-color: #fbdb8c; + --sub-color: #385eca; + --text-color: #2d2076; + --error-color: #f2ce83; + --error-extra-color: #f2ce83; + --colorful-error-color: #f2ce83; + --colorful-error-extra-color: #f2ce83; +} + +#menu .icon-button { + color: #f2ce83; +} + +#menu .icon-button:hover { + color: #c6915e; +} + +#top .logo .text { + background-color: #ffffff; /* fallback */ + background: -webkit-linear-gradient( + #2d2076, + #2d2076 25%, + #2e3395 25%, + #2e3395 50%, + #3049ba 50%, + #3049ba 75%, + #385eca 75%, + #385eca + ); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +#top .logo .text .top { + /* prevent it from being transparent */ + -webkit-text-fill-color: #385eca; +} + +==> ./monkeytype/static/themes/carbon.css <== +:root { + --bg-color: #313131; + --main-color: #f66e0d; + --caret-color: #f66e0d; + --sub-color: #616161; + --text-color: #f5e6c8; + --error-color: #e72d2d; + --error-extra-color: #7e2a33; + --colorful-error-color: #e72d2d; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/cafe.css <== +:root { + --bg-color: #ceb18d; + --main-color: #14120f; + --caret-color: #14120f; + --sub-color: #d4d2d1; + --text-color: #14120f; + --error-color: #c82931; + --error-extra-color: #ac1823; + --colorful-error-color: #c82931; + --colorful-error-extra-color: #ac1823; +} + +==> ./monkeytype/static/themes/future_funk.css <== +:root { + --bg-color: #2e1a47; + --main-color: #f7f2ea; + --caret-color: #f7f2ea; + --sub-color: #c18fff; + --text-color: #f7f2ea; + --error-color: #f04e98; + --error-extra-color: #bd1c66; + --colorful-error-color: #f04e98; + --colorful-error-extra-color: #bd1c66; +} + +#menu .icon-button:nth-child(1) { + color: #f04e98; +} + +#menu .icon-button:nth-child(2) { + color: #f8bed6; +} + +#menu .icon-button:nth-child(3) { + color: #f6eb61; +} + +#menu .icon-button:nth-child(4) { + color: #a4dbe8; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #a266ed; +} + +==> ./monkeytype/static/themes/fire.css <== +:root { + --bg-color: #0f0000; + --main-color: #b31313; + --caret-color: #b31313; + --sub-color: #683434; + --text-color: #ffffff; + --error-color: #2f3cb6; + --error-extra-color: #434a8f; + --colorful-error-color: #2f3cb6; + --colorful-error-extra-color: #434a8f; +} + +@keyframes rgb { + 0% { + color: #b31313; + } + 25% { + color: #ff9000; + } + 50% { + color: #fdda16; + } + 75% { + color: #ff9000; + } + 100% { + color: #b31313; + } +} + +@keyframes rgb-bg { + 0% { + background: #b31313; + } + 25% { + background: #ff9000; + } + 50% { + background: #fdda16; + } + 75% { + background: #ff9000; + } + 100% { + background: #b31313; + } +} + +.button.discord::after, +#caret, +.pageSettings .section .buttons .button.active, +.pageSettings .section.languages .buttons .language.active, +.pageAccount .group.filterButtons .buttons .button.active { + animation: rgb-bg 5s linear infinite; +} + +#top.focus .button.discord::after, +#top .button.discord.dotHidden::after { + animation-name: none !important; +} + +.logo .bottom, +#top .config .group .buttons .text-button.active, +#result .stats .group .bottom, +#menu .icon-button:hover, +#top .config .group .buttons .text-button:hover, +a:hover, +#words.flipped .word { + animation: rgb 5s linear infinite; +} + +#words.flipped .word letter.correct { + color: var(--sub-color); +} + +#words:not(.flipped) .word letter.correct { + animation: rgb 5s linear infinite; +} + +==> ./monkeytype/static/themes/striker.css <== +:root { + --bg-color: #124883; + --main-color: #d7dcda; + --caret-color: #d7dcda; + --sub-color: #0f2d4e; + --text-color: #d6dbd9; + --error-color: #fb4934; + --error-extra-color: #cc241d; + --colorful-error-color: #fb4934; + --colorful-error-extra-color: #cc241d; +} + +==> ./monkeytype/static/themes/monokai.css <== +:root { + --bg-color: #272822; + --main-color: #a6e22e; + --caret-color: #66d9ef; + --sub-color: #e6db74; + --text-color: #e2e2dc; + --error-color: #f92672; + --error-extra-color: #fd971f; + --colorful-error-color: #f92672; + --colorful-error-extra-color: #fd971f; +} + +==> ./monkeytype/static/themes/8008.css <== +:root { + --bg-color: #333a45; + --main-color: #f44c7f; + --caret-color: #f44c7f; + --sub-color: #939eae; + --text-color: #e9ecf0; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #c5da33; + --colorful-error-extra-color: #849224; +} + +==> ./monkeytype/static/themes/froyo.css <== +:root { + --bg-color: #e1dacb; + --main-color: #7b7d7d; + --caret-color: #7b7d7d; + --sub-color: #b29c5e; + --text-color: #7b7d7d; + --error-color: #f28578; + --error-extra-color: #d56558; + --colorful-error-color: #f28578; + --colorful-error-extra-color: #d56558; +} + +#menu .icon-button:nth-child(1) { + color: #ff7e73; +} + +#menu .icon-button:nth-child(2) { + color: #f5c370; +} + +#menu .icon-button:nth-child(3) { + color: #08d9a3; +} + +#menu .icon-button:nth-child(4) { + color: #0ca5e2; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #875ac6; +} + +==> ./monkeytype/static/themes/evil_eye.css <== +:root { + --bg-color: #0084c2; + --main-color: #f7f2ea; + --caret-color: #f7f2ea; + --sub-color: #01589f; + --text-color: #171718; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/deku.css <== +:root { + --bg-color: #058b8c; + --main-color: #b63530; + --caret-color: #b63530; + --sub-color: #255458; + --text-color: #f7f2ea; + --error-color: #b63530; + --error-extra-color: #530e0e; + --colorful-error-color: #ddca1f; + --colorful-error-extra-color: #8f8610; +} + +==> ./monkeytype/static/themes/alduin.css <== +:root { + --bg-color: #1c1c1c; + --main-color: #dfd7af; + --caret-color: #e3e3e3; + --sub-color: #444444; + --text-color: #f5f3ed; + --error-color: #af5f5f; + --error-extra-color: #4d2113; + --colorful-error-color: #af5f5f; + --colorful-error-extra-color: #4d2113; +} + +==> ./monkeytype/static/themes/dracula.css <== +:root { + --bg-color: #282a36; + --main-color: #f2f2f2; + --caret-color: #f2f2f2; + --sub-color: #bd93f9; + --text-color: #f2f2f2; + --error-color: #f758a0; + --error-extra-color: #732e51; + --colorful-error-color: #f758a0; + --colorful-error-extra-color: #732e51; +} + +#menu .icon-button:nth-child(1) { + color: #ec75c4; +} + +#menu .icon-button:nth-child(2) { + color: #8be9fd; +} + +#menu .icon-button:nth-child(3) { + color: #50fa7b; +} + +#menu .icon-button:nth-child(4) { + color: #f1fa8c; +} + +#menu .icon-button:nth-child(5) { + color: #ffb86c; +} + +#menu .icon-button:nth-child(6) { + color: #ffb86c; +} + +==> ./monkeytype/static/themes/metaverse.css <== +:root { + --bg-color: #232323; + --main-color: #d82934; + --caret-color: #d82934; + --sub-color: #5e5e5e; + --text-color: #e8e8e8; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #d7da33; + --colorful-error-extra-color: #737917; +} + +==> ./monkeytype/static/themes/hammerhead.css <== +:root { + --bg-color: #030613; + --main-color: #4fcdb9; + --caret-color: #4fcdb9; + --sub-color: #1e283a; + --text-color: #e2f1f5; + --error-color: #e32b2b; + --error-extra-color: #a62626; + --colorful-error-color: #e32b2b; + --colorful-error-extra-color: #a62626; +} + +==> ./monkeytype/static/themes/bushido.css <== +:root { + --bg-color: #242933; + --main-color: #ec4c56; + --caret-color: #ec4c56; + --sub-color: #596172; + --text-color: #f6f0e9; + --error-color: #ec4c56; + --error-extra-color: #9b333a; + --colorful-error-color: #ecdc4c; + --colorful-error-extra-color: #bdb03d; +} + +==> ./monkeytype/static/themes/matcha_moccha.css <== +:root { + --bg-color: #523525; + --main-color: #7ec160; + --caret-color: #7ec160; + --sub-color: #9e6749; + --text-color: #ecddcc; + --error-color: #fb4934; + --error-extra-color: #cc241d; + --colorful-error-color: #fb4934; + --colorful-error-extra-color: #cc241d; +} + +==> ./monkeytype/static/themes/modern_dolch.css <== +:root { + --bg-color: #2d2e30; + --main-color: #7eddd3; + --caret-color: #7eddd3; + --sub-color: #54585c; + --text-color: #e3e6eb; + --error-color: #d36a7b; + --error-extra-color: #994154; + --colorful-error-color: #d36a7b; + --colorful-error-extra-color: #994154; +} + +==> ./monkeytype/static/themes/creamsicle.css <== +:root { + --bg-color: #ff9869; + --main-color: #fcfcf8; + --caret-color: #fcfcf8; + --sub-color: #ff661f; + --text-color: #fcfcf8; + --error-color: #6a0dad; + --error-extra-color: #6a0dad; + --colorful-error-color: #6a0dad; + --colorful-error-extra-color: #6a0dad; +} + +==> ./monkeytype/static/themes/strawberry.css <== +:root { + --bg-color: #f37f83; + --main-color: #fcfcf8; + --caret-color: #fcfcf8; + --sub-color: #e53c58; + --text-color: #fcfcf8; + --error-color: #fcd23f; + --error-extra-color: #d7ae1e; + --colorful-error-color: #fcd23f; + --colorful-error-extra-color: #d7ae1e; +} + +==> ./monkeytype/static/themes/shadow.css <== +:root { + --bg-color: #000; + --main-color: #eee; + --caret-color: #eee; + --sub-color: #444; + --text-color: #eee; + --error-color: #fff; + --error-extra-color: #d8d8d8; + --colorful-error-color: #fff; + --colorful-error-extra-color: #d8d8d8; +} + +#top .logo .icon{ + color: #8C3230; +} + +#top .logo .text{ + color: #557D8D; +} + +@keyframes shadow { + to { + color: #000; + } +} + +@keyframes shadow-repeat { + 50% { + color: #000; + } + 100% { + color: #eee; + } +} + +#liveWpm, +#timerNumber { + color: white; +} + +#top .config .group .buttons .text-button.active, +#result .stats .group, +#menu .icon-button:hover, +#top .config .group .buttons .text-button:hover, +a:hover { + animation: shadow-repeat 3s linear infinite forwards; +} + +#logo, +#typingTest .word letter.correct { + animation: shadow 5s linear 1 forwards; +} + +==> ./monkeytype/static/themes/bento.css <== +:root { + --bg-color: #2d394d; + --main-color: #ff7a90; + --caret-color: #ff7a90; + --sub-color: #4a768d; + --text-color: #fffaf8; + --error-color: #ee2a3a; + --error-extra-color: #f04040; + --colorful-error-color: #fc2032; + --colorful-error-extra-color: #f04040; +} + +==> ./monkeytype/static/themes/menthol.css <== +:root { + --bg-color: #00c18c; + --main-color: #ffffff; + --caret-color: #99fdd8; + --sub-color: #186544; + --text-color: #ffffff; + --error-color: #e03c3c; + --error-extra-color: #b12525; + --colorful-error-color: #e03c3c; + --colorful-error-extra-color: #b12525; +} + +==> ./monkeytype/static/themes/our_theme.css <== +:root { + --bg-color: #ce1226; + --main-color: #fcd116; + --caret-color: #fcd116; + --sub-color: #6d0f19; + --text-color: #ffffff; + --error-color: #fcd116; + --error-extra-color: #fcd116; + --colorful-error-color: #1672fc; + --colorful-error-extra-color: #1672fc; +} + +==> ./monkeytype/static/themes/mr_sleeves.css <== +:root { + --bg-color: #d1d7da; + --main-color: #daa99b; + --caret-color: #8fadc9; + --sub-color: #9a9fa1; + --text-color: #1d1d1d; + --error-color: #bf6464; + --error-extra-color: #793e44; + --colorful-error-color: #8fadc9; + --colorful-error-extra-color: #667c91; +} + +#top .logo .bottom { + color: #8fadc9; +} + +#top .config .group .buttons .text-button.active { + color: #daa99b; +} + +/* #menu .icon-button:nth-child(1){ + color: #daa99b; +} + +#menu .icon-button:nth-child(2){ + color: #daa99b; +} + +#menu .icon-button:nth-child(3){ + color: #8fadc9; +} + +#menu .icon-button:nth-child(4), +#menu .icon-button:nth-child(5){ + color: #8fadc9; +} */ + +==> ./monkeytype/static/themes/retrocast.css <== +:root { + --bg-color: #07737a; + --main-color: #88dbdf; + --caret-color: #88dbdf; + --sub-color: #f3e03b; + --text-color: #ffffff; + --error-color: #ff585d; + --error-extra-color: #c04455; + --colorful-error-color: #ff585d; + --colorful-error-extra-color: #c04455; +} + +#menu .icon-button:nth-child(1) { + color: #88dbdf; +} + +#menu .icon-button:nth-child(2) { + color: #88dbdf; +} + +#menu .icon-button:nth-child(3) { + color: #88dbdf; +} + +#menu .icon-button:nth-child(4) { + color: #ff585d; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #f3e03b; +} + +==> ./monkeytype/static/themes/frozen_llama.css <== +:root { + --bg-color: #9bf2ea; + --main-color: #6d44a6; + --caret-color: #ffffff; + --sub-color: #b690fd; + --text-color: #ffffff; + --error-color: #e42629; + --error-extra-color: #e42629; + --colorful-error-color: #e42629; + --colorful-error-extra-color: #e42629; +} + +==> ./monkeytype/static/themes/solarized_light.css <== +:root { + --bg-color: #fdf6e3; + --main-color: #859900; + --caret-color: #dc322f; + --sub-color: #2aa198; + --text-color: #181819; + --error-color: #d33682; + --error-extra-color: #9b225c; + --colorful-error-color: #d33682; + --colorful-error-extra-color: #9b225c; +} + +==> ./monkeytype/static/themes/bliss.css <== +:root { + --bg-color: #262727; + --main-color: #f0d3c9; + --caret-color: #f0d3c9; + --sub-color: #665957; + --text-color: #fff; + --error-color: #bd4141; + --error-extra-color: #883434; + --colorful-error-color: #bd4141; + --colorful-error-extra-color: #883434; +} + +==> ./monkeytype/static/themes/repose_light.css <== +:root { + --bg-color: #EFEAD0; + --main-color: #5F605E; + --caret-color: #5F605E; + --sub-color: #8F8E84; + --text-color: #333538; + --error-color: #C43C53; + --error-extra-color: #A52632; + --colorful-error-color: #C43C53; + --colorful-error-extra-color: #A52632; +} + +==> ./monkeytype/static/themes/fundamentals.css <== +:root { + --bg-color: #727474; + --main-color: #7fa482; + --caret-color: #196378; + --sub-color: #cac4be; + --text-color: #131313; + --error-color: #5e477c; + --error-extra-color: #413157; + --colorful-error-color: #5e477c; + --colorful-error-extra-color: #413157; +} + +#top .logo .bottom { + color: #196378; +} + +==> ./monkeytype/static/themes/miami.css <== +:root { + --bg-color: #f35588; + --main-color: #05dfd7; + --caret-color: #a3f7bf; + --text-color: #f0e9ec; + --sub-color: #94294c; + --error-color: #fff591; + --error-extra-color: #b9b269; + --colorful-error-color: #fff591; + --colorful-error-extra-color: #b9b269; +} + +==> ./monkeytype/static/themes/sewing_tin.css <== +:root { + --bg-color: #241963; + --main-color: #f2ce83; + --caret-color: #fbdb8c; + --sub-color: #446ad5; + --text-color: #ffffff; + --error-color: #c6915e; + --error-extra-color: #c6915e; + --colorful-error-color: #c6915e; + --colorful-error-extra-color: #c6915e; +} + +#menu .icon-button { + color: #f2ce83; +} + +#menu .icon-button:hover { + color: #c6915e; +} + +==> ./monkeytype/static/themes/fleuriste.css <== +:root { + --bg-color: #c6b294; + --main-color: #405a52; + --caret-color: #8a785b; + --sub-color: #64374d; + --text-color: #091914; + --error-color: #990000; + --error-extra-color: #8a1414; + --colorful-error-color: #a63a3a; + --colorful-error-extra-color: #bd4c4c; +} + +#menu .icon-button:nth-child(1, 3, 5) { + background: #405a52; +} +#menu .icon-button:nth-child(2, 4) { + background: #64374d; +} + + + +==> ./monkeytype/static/themes/rose_pine.css <== +:root { + --bg-color: #1f1d27; /*Background*/ + --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ + --caret-color: #f6c177; /*Cursor Color*/ + --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ + --text-color: #e0def4; /*Color of text after hovering over it*/ + --error-color: #eb6f92; + --error-extra-color: #ebbcba; + --colorful-error-color: #eb6f92; + --colorful-error-extra-color: #ebbcba; +} + +==> ./monkeytype/static/themes/sonokai.css <== +:root { + --bg-color: #2c2e34; + --main-color: #9ed072; + --caret-color: #f38c71; + --sub-color: #e7c664; + --text-color: #e2e2e3; + --error-color: #fc5d7c; + --error-extra-color: #ecac6a; + --colorful-error-color: #fc5d7c; + --colorful-error-extra-color: #ecac6a; +} + +==> ./monkeytype/static/themes/trackday.css <== +:root { + --bg-color: #464d66; + --main-color: #e0513e; + --caret-color: #475782; + --sub-color: #5c7eb9; + --text-color: #cfcfcf; + --error-color: #e44e4e; + --error-extra-color: #fd3f3f; + --colorful-error-color: #ff2e2e; + --colorful-error-extra-color: #bb2525; +} + +#menu .icon-button:nth-child(1) { + color: #e0513e; +} + +#menu .icon-button:nth-child(3) { + color: #cfcfcf; +} + +#menu .icon-button:nth-child(2) { + color: #ccc500; +} + +==> ./monkeytype/static/themes/milkshake.css <== +:root { + --bg-color: #ffffff; + --main-color: #212b43; + --caret-color: #212b43; + --sub-color: #62cfe6; + --text-color: #212b43; + --error-color: #f19dac; + --error-extra-color: #e58c9d; + --colorful-error-color: #f19dac; + --colorful-error-extra-color: #e58c9d; +} + +#menu .icon-button:nth-child(1) { + color: #f19dac; +} + +#menu .icon-button:nth-child(2) { + color: #f6f4a0; +} + +#menu .icon-button:nth-child(3) { + color: #73e4d0; +} + +#menu .icon-button:nth-child(4) { + color: #61cfe6; +} + +#menu .icon-button:nth-child(5) { + color: #ba96db; +} + +#menu .icon-button:nth-child(6) { + color: #ba96db; +} + +==> ./monkeytype/static/themes/trance.css <== +:root { + --bg-color: #00021b; + --main-color: #e51376; + --caret-color: #e51376; + --sub-color: #3c4c79; + --text-color: #fff; + --error-color: #02d3b0; + --error-extra-color: #3f887c; + --colorful-error-color: #02d3b0; + --colorful-error-extra-color: #3f887c; +} + +@keyframes rgb { + 0% { + color: #e51376; + } + 50% { + color: #0e77ee; + } + 100% { + color: #e51376; + } +} + +@keyframes rgb-bg { + 0% { + background: #e51376; + } + 50% { + background: #0e77ee; + } + 100% { + background: #e51376; + } +} + +.button.discord::after, +#caret, +.pageSettings .section .buttons .button.active, +.pageSettings .section.languages .buttons .language.active, +.pageAccount .group.filterButtons .buttons .button.active { + animation: rgb-bg 5s linear infinite; +} + +#top.focus .button.discord::after, +#top .button.discord.dotHidden::after { + animation-name: none !important; +} + +.logo .bottom, +#top .config .group .buttons .text-button.active, +#result .stats .group .bottom, +#menu .icon-button:hover, +#top .config .group .buttons .text-button:hover, +a:hover, +#words.flipped .word { + animation: rgb 5s linear infinite; +} + +#words.flipped .word letter.correct { + color: var(--sub-color); +} + +#words:not(.flipped) .word letter.correct { + animation: rgb 5s linear infinite; +} + +==> ./monkeytype/static/themes/nausea.css <== +:root { + --bg-color: #323437; + --main-color: #e2b714; + --caret-color: #e2b714; + --sub-color: #646669; + --text-color: #d1d0c5; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +@keyframes woah { + 0% { + transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) + scaleY(0.9); + } + + 25% { + transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1) scaleY(0.8); + } + + 50% { + transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(0.9) + scaleY(0.9); + } + + 75% { + transform: rotateY(15deg) skewY(-10deg) rotateX(15deg) scaleX(1.5) + scaleY(1.1); + } + + 100% { + transform: rotateY(-15deg) skewY(10deg) rotateX(-15deg) scaleX(1.2) + scaleY(0.9); + } +} + +@keyframes plsstop { + 0% { + background: #323437; + } + + 50% { + background: #3e4146; + } + + 100% { + background: #323437; + } +} + +#middle { + animation: woah 7s infinite cubic-bezier(0.5, 0, 0.5, 1); +} + +#centerContent { + transform: rotate(5deg); + perspective: 500px; +} + +body { + animation: plsstop 10s infinite cubic-bezier(0.5, 0, 0.5, 1); + overflow: hidden; +} + +==> ./monkeytype/static/themes/superuser.css <== +:root { + --bg-color: #262a33; + --main-color: #43ffaf; + --caret-color: #43ffaf; + --sub-color: #526777; + --text-color: #e5f7ef; + --error-color: #ff5f5f; + --error-extra-color: #d22a2a; + --colorful-error-color: #ff5f5f; + --colorful-error-extra-color: #d22a2a; +} + +==> ./monkeytype/static/themes/serika.css <== +:root { + --main-color: #e2b714; + --caret-color: #e2b714; + --sub-color: #aaaeb3; + --bg-color: #e1e1e3; + --text-color: #323437; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #da3333; + --colorful-error-extra-color: #791717; +} + +==> ./monkeytype/static/themes/gruvbox_dark.css <== +:root { + --bg-color: #282828; + --main-color: #d79921; + --caret-color: #fabd2f; + --sub-color: #665c54; + --text-color: #ebdbb2; + --error-color: #fb4934; + --error-extra-color: #cc241d; + --colorful-error-color: #cc241d; + --colorful-error-extra-color: #9d0006; +} + +==> ./monkeytype/static/themes/godspeed.css <== +:root { + --bg-color: #eae4cf; + --main-color: #9abbcd; + --caret-color: #f4d476; + --sub-color: #c0bcab; + --text-color: #646669; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/joker.css <== +:root { + --bg-color: #1a0e25; + --main-color: #99de1e; + --caret-color: #99de1e; + --sub-color: #7554a3; + --text-color: #e9e2f5; + --error-color: #e32b2b; + --error-extra-color: #a62626; + --colorful-error-color: #e32b2b; + --colorful-error-extra-color: #a62626; +} + +==> ./monkeytype/static/themes/rose_pine_dawn.css <== +:root { + --bg-color: #fffaf3; /*Background*/ + --main-color: #56949f; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ + --caret-color: #ea9d34; /*Cursor Color*/ + --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ + --text-color: #286983; /*Color of text after hovering over it*/ + --error-color: #b4637a; + --error-extra-color: #d7827e; + --colorful-error-color: #b4637a; + --colorful-error-extra-color: #d7827e; +} + +==> ./monkeytype/static/themes/grand_prix.css <== +:root { + --bg-color: #36475c; + --main-color: #c0d036; + --caret-color: #c0d036; + --sub-color: #5c6c80; + --text-color: #c1c7d7; + --error-color: #fc5727; + --error-extra-color: #fc5727; + --colorful-error-color: #fc5727; + --colorful-error-extra-color: #fc5727; +} + +==> ./monkeytype/static/themes/lavender.css <== +:root { + --bg-color: #ada6c2; + --main-color: #e4e3e9; + --caret-color: #e4e3e9; + --sub-color: #e4e3e9; + --text-color: #2f2a41; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; + } + + #menu .icon-button { + border-radius: 10rem !important; + background: #2f2a41; + color: #e4e3e9; + + } + + #menu .icon-button:hover { + color: #ada6c2; + } + +==> ./monkeytype/static/themes/watermelon.css <== +:root { + --bg-color: #1f4437; + --main-color: #d6686f; + --caret-color: #d6686f; + --sub-color: #3e7a65; + --text-color: #cdc6bc; + --error-color: #c82931; + --error-extra-color: #ac1823; + --colorful-error-color: #c82931; + --colorful-error-extra-color: #ac1823; +} + +==> ./monkeytype/static/themes/copper.css <== +:root { + --bg-color: #442f29; + --main-color: #b46a55; + --caret-color: #c25c42; + --sub-color: #7ebab5; + --text-color: #e7e0de; + --error-color: #a32424; + --error-extra-color: #ec0909; + --colorful-error-color: #a32424; + --colorful-error-extra-color: #ec0909; +} + +==> ./monkeytype/static/themes/beach.css <== +:root { + --bg-color: #ffeead; + --main-color: #96ceb4; + --caret-color: #ffcc5c; + --sub-color: #ffcc5c; + --text-color: #5b7869; + --error-color: #ff6f69; + --error-extra-color: #ff6f69; + --colorful-error-color: #ff6f69; + --colorful-error-extra-color: #ff6f69; + } + + #menu .icon-button:nth-child(1), + #menu .icon-button:nth-child(2), + #menu .icon-button:nth-child(3), + #menu .icon-button:nth-child(4), + #menu .icon-button:nth-child(5), + #menu .icon-button:nth-child(6) { + color: #ff6f69; + } + +==> ./monkeytype/static/themes/pulse.css <== +:root { + --bg-color: #181818; + --main-color: #17b8bd; + --caret-color: #17b8bd; + --sub-color: #53565a; + --text-color: #e5f4f4; + --error-color: #da3333; + --error-extra-color: #791717; + --colorful-error-color: #da3333; + --colorful-error-extra-color: #791717; +} + +==> ./monkeytype/static/themes/drowning.css <== +:root { + --bg-color: #191826; + --main-color: #4a6fb5; + --caret-color: #4f85e8; + --sub-color: #50688c; + --text-color: #9393a7; + --error-color: #be555f; + --error-extra-color: #7e2a33; + --colorful-error-color: #be555f; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/blueberry_dark.css <== +:root { + --bg-color: #212b42; + --main-color: #add7ff; + --caret-color: #962f7e; + --sub-color: #5c7da5; + --text-color: #91b4d5; + --error-color: #df4576; + --error-extra-color: #d996ac; + --colorful-error-color: #df4576; + --colorful-error-extra-color: #d996ac; +} + +#top .logo .bottom { + color: #962f7e; +} + +==> ./monkeytype/static/themes/arch.css <== +:root { + --bg-color: #0c0d11; + --main-color: #7ebab5; + --caret-color: #7ebab5; + --sub-color: #454864; + --text-color: #f6f5f5; + --error-color: #ff4754; + --error-extra-color: #b02a33; + --colorful-error-color: #ff4754; + --colorful-error-extra-color: #b02a33; +} + +==> ./monkeytype/static/themes/olive.css <== +:root { + --bg-color: #e9e5cc; + --caret-color: #92946f; + --main-color: #92946f; + --sub-color: #b7b39e; + --text-color: #373731; + --error-color: #cf2f2f; + --error-extra-color: #a22929; + --colorful-error-color: #cf2f2f; + --colorful-error-extra-color: #a22929; +} + +==> ./monkeytype/static/themes/luna.css <== +:root { + --bg-color: #221c35; + --main-color: #f67599; + --caret-color: #f67599; + --sub-color: #5a3a7e; + --text-color: #ffe3eb; + --error-color: #efc050; + --error-extra-color: #c5972c; + --colorful-error-color: #efc050; + --colorful-error-extra-color: #c5972c; +} + +==> ./monkeytype/static/themes/red_samurai.css <== +:root { + --bg-color: #84202c; + --main-color: #c79e6e; + --caret-color: #c79e6e; + --sub-color: #55131b; + --text-color: #e2dad0; + --error-color: #33bbda; + --error-extra-color: #176b79; + --colorful-error-color: #33bbda; + --colorful-error-extra-color: #176779; +} + +==> ./monkeytype/static/themes/cyberspace.css <== +:root { + --bg-color: #181c18; + --main-color: #00ce7c; + --caret-color: #00ce7c; + --sub-color: #9578d3; + --text-color: #c2fbe1; + --error-color: #ff5f5f; + --error-extra-color: #d22a2a; + --colorful-error-color: #ff5f5f; + --colorful-error-extra-color: #d22a2a; +} + +==> ./monkeytype/static/themes/shoko.css <== +:root { + --bg-color: #ced7e0; + --main-color: #81c4dd; + --caret-color: #81c4dd; + --sub-color: #7599b1; + --text-color: #3b4c58; + --error-color: #bf616a; + --error-extra-color: #793e44; + --colorful-error-color: #bf616a; + --colorful-error-extra-color: #793e44; +} + +==> ./monkeytype/static/themes/oblivion.css <== +:root { + --bg-color: #313231; + --main-color: #a5a096; + --caret-color: #a5a096; + --sub-color: #5d6263; + --text-color: #f7f5f1; + --error-color: #dd452e; + --error-extra-color: #9e3423; + --colorful-error-color: #dd452e; + --colorful-error-extra-color: #9e3423; +} + +#menu .icon-button:nth-child(1) { + color: #9a90b4; +} + +#menu .icon-button:nth-child(2) { + color: #8db14b; +} + +#menu .icon-button:nth-child(3) { + color: #fca321; +} + +#menu .icon-button:nth-child(4) { + color: #2984a5; +} + +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #dd452e; +} + +==> ./monkeytype/static/themes/comfy.css <== +:root { + --bg-color: #4a5b6e; + --main-color: #f8cdc6; + --caret-color: #9ec1cc; + --sub-color: #9ec1cc; + --text-color: #f5efee; + --error-color: #c9465e; + --error-extra-color: #c9465e; + --colorful-error-color: #c9465e; + --colorful-error-extra-color: #c9465e; +} + +==> ./monkeytype/static/themes/chaos_theory.css <== +:root { + --bg-color: #141221; + --main-color: #fd77d7; + --caret-color: #dde5ed; + --text-color: #dde5ed; + --error-color: #fd77d7; + --sub-color: #676e8a; + --error-color: #FF5869; + --error-extra-color: #b03c47; + --colorful-error-color: #FF5869; + --colorful-error-extra-color: #b03c47; +} + +#top .logo .text { + -webkit-transform: rotateY(180deg); + unicode-bidi: bidi-override; + transition: 0.5s; +} + +#top .logo .top { + font-family: "Comic Sans MS", "Comic Sans", cursive; +} + +#top .logo .icon { + -webkit-transform: rotateX(180deg); + transition: 0.5s; +} + +#words .incorrect.extra { + -webkit-transform: rotateY(180deg); + unicode-bidi: bidi-override; + direction: rtl; +} + +#bottom .leftright .right .current-theme .text { + /* font-family: "Comic Sans MS", "Comic Sans", cursive; */ +} + +#caret { + background-image: url(https://i.imgur.com/yN31JmJ.png); + background-color: transparent; + background-size: 1rem; + background-position: center; + background-repeat: no-repeat; +} + +#caret.default { + width: 4px; +} + +.config .toggleButton { + -webkit-transform: rotateY(180deg); + unicode-bidi: bidi-override; + direction: rtl; + align-content: right; +} + +.config .mode .text-button { + -webkit-transform: rotateY(180deg); + unicode-bidi: bidi-override; + direction: rtl; + align-content: right; +} + +.config .wordCount .text-button, +.config .time .text-button, +.config .quoteLength .text-button, +.config .customText .text-button { + -webkit-transform: rotateY(180deg); + unicode-bidi: bidi-override; + direction: rtl; + align-content: right; +} + +#top.focus #menu .icon-button, +#top.focus #menu:before, +#top.focus #menu:after { + background: var(--sub-color); + -webkit-transform: rotateY(180deg) !important; +} + +#top.focus .logo .text, +#top.focus .logo:before, +#top.focus .logo:after { + -webkit-transform: rotateY(0deg); + direction: ltr; +} + +#top.focus .logo .icon, +#top.focus .logo:before, +#top.focus .logo:after { + -webkit-transform: rotateX(0deg); + direction: ltr; +} + +#bottom .leftright .right .current-theme:hover .fas.fa-fw.fa-palette { + -webkit-transform: rotateY(180deg); + transition: 0.5s; +} +#menu { + gap: 0.5rem; +} + +#menu .icon-button { + border-radius: 10rem i !important; + color: var(--bg-color); + transition: 0.5s; +} + +#menu .icon-button:nth-child(1) { + background: #ab92e1; +} + +#menu .icon-button:nth-child(2) { + background: #f3ea5d; +} + +#menu .icon-button:nth-child(3) { + background: #7ae1bf; +} + +#menu .icon-button:nth-child(4) { + background: #ff5869; +} + +#menu .icon-button:nth-child(5) { + background: #fc76d9; +} + +#menu .icon-button:nth-child(6) { + background: #fc76d9; +} + +==> ./monkeytype/static/themes/bouquet.css <== +:root { + --bg-color: #173f35; + --main-color: #eaa09c; + --caret-color: #eaa09c; + --sub-color: #408e7b; + --text-color: #e9e0d2; + --error-color: #d44729; + --error-extra-color: #8f2f19; + --colorful-error-color: #d44729; + --colorful-error-extra-color: #8f2f19; +} + +==> ./monkeytype/static/themes/ryujinscales.css <== +:root { + --bg-color: #081426; + --main-color: #f17754; + --caret-color: #ef6d49; + --sub-color: #ffbc90; + --text-color: #ffe4bc; + --error-color: #ca4754; + --error-extra-color: #7e2a33; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; + } + +/* your theme has been added to the _list file and the textColor property is the theme's main color */ +==> ./monkeytype/static/themes/graen.css <== +:root { + --bg-color: #303c36; + --main-color: #a59682; + --caret-color: #601420; + --sub-color: #181d1a; + --text-color: #a59682; + --error-color: #601420; + --error-extra-color: #5f0715; + --colorful-error-color: #601420; + --colorful-error-extra-color: #5f0715; +} + +#menu .icon-button:nth-child(1), +#menu .icon-button:nth-child(2), +#menu .icon-button:nth-child(3), +#menu .icon-button:nth-child(4), +#menu .icon-button:nth-child(5), +#menu .icon-button:nth-child(6) { + color: #601420; +} + +==> ./monkeytype/static/themes/mountain.css <== +:root { + --bg-color: #0f0f0f; + --main-color: #e7e7e7; + --caret-color: #f5f5f5; + --sub-color: #4c4c4c; + --text-color: #e7e7e7; + --error-color: #ac8c8c; + --error-extra-color: #c49ea0; + --colorful-error-color: #aca98a; + --colorful-error-extra-color: #c4c19e; +} + +==> ./monkeytype/static/themes/voc.css <== +:root { + --bg-color: #190618; + --main-color: #e0caac; + --caret-color: #e0caac; + --sub-color: #4c1e48; + --text-color: #eeeae4; + --error-color: #af3735; + --error-extra-color: #7e2a29; + --colorful-error-color: #af3735; + --colorful-error-extra-color: #7e2a29; +} + +==> ./monkeytype/static/themes/norse.css <== +:root { + --bg-color: #242425; + --main-color: #2b5f6d; + --caret-color: #2b5f6d; + --sub-color: #505b5e; + --text-color: #ccc2b1; + --error-color: #7e2a2a; + --error-extra-color: #771d1d; + --colorful-error-color: #ca4754; + --colorful-error-extra-color: #7e2a33; +} + +==> ./monkeytype/static/themes/rose_pine_moon.css <== +:root { + --bg-color: #2a273f; /*Background*/ + --main-color: #9ccfd8; /*Color after typing, monkeytype logo, WPM Number acc number etc*/ + --caret-color: #f6c177; /*Cursor Color*/ + --sub-color: #c4a7e7; /*WPM text color of scrollbar and general color, before typed color*/ + --text-color: #e0def4; /*Color of text after hovering over it*/ + --error-color: #eb6f92; + --error-extra-color: #ebbcba; + --colorful-error-color: #eb6f92; + --colorful-error-extra-color: #ebbcba; +} + +==> ./monkeytype/static/themes/80s_after_dark.css <== +:root { + --bg-color: #1b1d36; + --main-color: #fca6d1; + --caret-color: #99d6ea; + --sub-color: #99d6ea; + --text-color: #e1e7ec; + --error-color: #fffb85; + --error-extra-color: #fffb85; + --colorful-error-color: #fffb85; + --colorful-error-extra-color: #fffb85; +} + +==> ./monkeytype/static/themes/peaches.css <== +:root { + --bg-color: #e0d7c1; + --main-color: #dd7a5f; + --caret-color: #dd7a5f; + --sub-color: #e7b28e; + --text-color: #5f4c41; + --error-color: #ff6961; + --error-extra-color: #c23b22; + --colorful-error-color: #ff6961; + --colorful-error-extra-color: #c23b22; +} + +==> ./monkeytype/static/sw.js <== +const staticCacheName = "sw-cache"; // this is given a unique name on build + +self.addEventListener("activate", (event) => { + caches.keys().then((names) => { + for (let name of names) { + if (name !== staticCacheName) event.waitUntil(caches.delete(name)); + } + }); + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("install", (event) => { + event.waitUntil(self.skipWaiting()); + event.waitUntil( + caches.open(staticCacheName).then((cache) => { + // Cache the base file(s) + return cache.add("/"); + }) + ); +}); + +self.addEventListener("fetch", async (event) => { + const host = new URL(event.request.url).host; + if ( + [ + "localhost:5005", + "api.monkeytype.com", + "api.github.com", + "www.google-analytics.com", + ].includes(host) || + host.endsWith("wikipedia.org") + ) { + // if hostname is a non-static api, fetch request + event.respondWith(fetch(event.request)); + } else { + // Otherwise, assume host is serving a static file, check cache and add response to cache if not found + event.respondWith( + caches.open(staticCacheName).then((cache) => { + return cache.match(event.request).then(async (response) => { + // Check if request in cache + if (response) { + // if response was found in the cache, send from cache + return response; + } else { + // if response was not found in cache fetch from server, cache it and send it + response = await fetch(event.request); + cache.put(event.request.url, response.clone()); + return response; + } + }); + }) + ); + } +}); + +==> ./monkeytype/.nvmrc <== +14.18.1 +==> ./monkeytype/.prettierrc <== +{ + "tabWidth": 2, + "useTabs": false, + "htmlWhitespaceSensitivity": "ignore" +} + +==> ./monkeytype/gulpfile.js <== +const { task, src, dest, series, watch } = require("gulp"); +const axios = require("axios"); +const browserify = require("browserify"); +const babelify = require("babelify"); +const concat = require("gulp-concat"); +const del = require("del"); +const source = require("vinyl-source-stream"); +const buffer = require("vinyl-buffer"); +const vinylPaths = require("vinyl-paths"); +const eslint = require("gulp-eslint"); +var sass = require("gulp-sass")(require("dart-sass")); +const replace = require("gulp-replace"); +const uglify = require("gulp-uglify"); +// sass.compiler = require("dart-sass"); + +let eslintConfig = { + parser: "babel-eslint", + globals: [ + "jQuery", + "$", + "firebase", + "moment", + "html2canvas", + "ClipboardItem", + "grecaptcha", + ], + envs: ["es6", "browser", "node"], + plugins: ["json"], + extends: ["plugin:json/recommended"], + rules: { + "json/*": ["error"], + "constructor-super": "error", + "for-direction": "error", + "getter-return": "error", + "no-async-promise-executor": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-else-if": "warn", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": ["warn", { allowEmptyCatch: true }], + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-extra-semi": "error", + "no-fallthrough": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-import-assign": "error", + "no-inner-declarations": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "warn", + "no-misleading-character-class": "error", + "no-mixed-spaces-and-tabs": "error", + "no-new-symbol": "error", + "no-obj-calls": "error", + "no-octal": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-self-assign": "error", + "no-setter-return": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-this-before-super": "error", + "no-undef": "error", + "no-unexpected-multiline": "warn", + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unused-labels": "error", + "no-unused-vars": ["warn", { argsIgnorePattern: "e|event" }], + "no-use-before-define": "warn", + "no-useless-catch": "error", + "no-useless-escape": "error", + "no-with": "error", + "require-yield": "error", + "use-isnan": "error", + "valid-typeof": "error", + }, +}; + +//refactored files, which should be es6 modules +//once all files are moved here, then can we use a bundler to its full potential +const refactoredSrc = [ + "./src/js/axios-instance.js", + "./src/js/db.js", + "./src/js/misc.js", + "./src/js/layouts.js", + "./src/js/sound.js", + "./src/js/theme-colors.js", + "./src/js/chart-controller.js", + "./src/js/theme-controller.js", + "./src/js/config.js", + "./src/js/tag-controller.js", + "./src/js/preset-controller.js", + "./src/js/ui.js", + "./src/js/commandline.js", + "./src/js/commandline-lists.js", + "./src/js/commandline.js", + "./src/js/challenge-controller.js", + "./src/js/mini-result-chart.js", + "./src/js/account-controller.js", + "./src/js/simple-popups.js", + "./src/js/settings.js", + "./src/js/input-controller.js", + "./src/js/route-controller.js", + "./src/js/ready.js", + "./src/js/monkey-power.js", + + "./src/js/account/all-time-stats.js", + "./src/js/account/pb-tables.js", + "./src/js/account/result-filters.js", + "./src/js/account/verification-controller.js", + "./src/js/account.js", + + "./src/js/elements/monkey.js", + "./src/js/elements/notifications.js", + "./src/js/elements/leaderboards.js", + "./src/js/elements/account-button.js", + "./src/js/elements/loader.js", + "./src/js/elements/sign-out-button.js", + "./src/js/elements/about-page.js", + "./src/js/elements/psa.js", + "./src/js/elements/new-version-notification.js", + "./src/js/elements/mobile-test-config.js", + "./src/js/elements/loading-page.js", + "./src/js/elements/scroll-to-top.js", + + "./src/js/popups/custom-text-popup.js", + "./src/js/popups/pb-tables-popup.js", + "./src/js/popups/quote-search-popup.js", + "./src/js/popups/quote-submit-popup.js", + "./src/js/popups/quote-approve-popup.js", + "./src/js/popups/rate-quote-popup.js", + "./src/js/popups/version-popup.js", + "./src/js/popups/support-popup.js", + "./src/js/popups/contact-popup.js", + "./src/js/popups/custom-word-amount-popup.js", + "./src/js/popups/custom-test-duration-popup.js", + "./src/js/popups/word-filter-popup.js", + "./src/js/popups/result-tags-popup.js", + "./src/js/popups/edit-tags-popup.js", + "./src/js/popups/edit-preset-popup.js", + "./src/js/popups/custom-theme-popup.js", + "./src/js/popups/import-export-settings-popup.js", + "./src/js/popups/custom-background-filter.js", + + "./src/js/settings/language-picker.js", + "./src/js/settings/theme-picker.js", + "./src/js/settings/settings-group.js", + + "./src/js/test/custom-text.js", + "./src/js/test/british-english.js", + "./src/js/test/lazy-mode.js", + "./src/js/test/shift-tracker.js", + "./src/js/test/out-of-focus.js", + "./src/js/test/caret.js", + "./src/js/test/manual-restart-tracker.js", + "./src/js/test/test-stats.js", + "./src/js/test/focus.js", + "./src/js/test/practise-words.js", + "./src/js/test/test-ui.js", + "./src/js/test/keymap.js", + "./src/js/test/result.js", + "./src/js/test/live-wpm.js", + "./src/js/test/caps-warning.js", + "./src/js/test/live-acc.js", + "./src/js/test/live-burst.js", + "./src/js/test/timer-progress.js", + "./src/js/test/test-logic.js", + "./src/js/test/funbox.js", + "./src/js/test/pace-caret.js", + "./src/js/test/pb-crown.js", + "./src/js/test/test-timer.js", + "./src/js/test/test-config.js", + "./src/js/test/layout-emulator.js", + "./src/js/test/poetry.js", + "./src/js/test/wikipedia.js", + "./src/js/test/today-tracker.js", + "./src/js/test/weak-spot.js", + "./src/js/test/wordset.js", + "./src/js/test/tts.js", + "./src/js/replay.js", +]; + +//legacy files +//the order of files is important +const globalSrc = ["./src/js/global-dependencies.js", "./src/js/exports.js"]; + +//concatenates and lints legacy js files and writes the output to dist/gen/index.js +task("cat", function () { + return src(globalSrc) + .pipe(concat("index.js")) + .pipe(eslint(eslintConfig)) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()) + .pipe(dest("./dist/gen")); +}); + +task("sass", function () { + return src("./src/sass/*.scss") + .pipe(concat("style.scss")) + .pipe(sass({ outputStyle: "compressed" }).on("error", sass.logError)) + .pipe(dest("dist/css")); +}); + +task("static", function () { + return src("./static/**/*", { dot: true }).pipe(dest("./dist/")); +}); + +//copies refactored js files to dist/gen so that they can be required by dist/gen/index.js +task("copy-modules", function () { + return src(refactoredSrc, { allowEmpty: true }).pipe(dest("./dist/gen")); +}); + +//bundles the refactored js files together with index.js (the concatenated legacy js files) +//it's odd that the entry point is generated, so we should seek a better way of doing this +task("browserify", function () { + const b = browserify({ + //index.js is generated by task "cat" + entries: "./dist/gen/index.js", + //a source map isn't very useful right now because + //the source files are concatenated together + debug: false, + }); + return b + .transform( + babelify.configure({ + presets: ["@babel/preset-env"], + plugins: ["@babel/transform-runtime"], + }) + ) + .bundle() + .pipe(source("monkeytype.js")) + .pipe(buffer()) + .pipe( + uglify({ + mangle: false, + }) + ) + .pipe(dest("./dist/js")); +}); + +//lints only the refactored files +task("lint", function () { + let filelist = refactoredSrc; + filelist.push("./static/**/*.json"); + return src(filelist) + .pipe(eslint(eslintConfig)) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +task("clean", function () { + return src("./dist/", { allowEmpty: true }).pipe(vinylPaths(del)); +}); + +task("updateSwCacheName", function () { + let date = new Date(); + let dateString = + date.getFullYear() + + "-" + + (date.getMonth() + 1) + + "-" + + date.getDate() + + "-" + + date.getHours() + + "-" + + date.getMinutes() + + "-" + + date.getSeconds(); + return src(["static/sw.js"]) + .pipe( + replace( + /const staticCacheName = .*;/g, + `const staticCacheName = "sw-cache-${dateString}";` + ) + ) + .pipe(dest("./dist/")); +}); + +task( + "compile", + series( + "lint", + "cat", + "copy-modules", + "browserify", + "static", + "sass", + "updateSwCacheName" + ) +); + +task("watch", function () { + watch(["./static/**/*", "./src/**/*"], series("compile")); +}); + +task("build", series("clean", "compile")); + + +==> monkeytype/src/sass/about.scss <== +.pageAbout { + display: grid; + gap: 2rem; + + .created { + text-align: center; + color: var(--sub-color); + a { + text-decoration: none; + } + } + + .section { + display: grid; + gap: 0.25rem; + + .title { + font-size: 2rem; + line-height: 2rem; + color: var(--sub-color); + margin: 1rem 0; + } + + .contactButtons, + .supportButtons { + margin-top: 1rem; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 1rem; + .button { + text-decoration: none; + font-size: 1.5rem; + padding: 2rem 0; + .fas, + .fab { + margin-right: 1rem; + } + } + } + + .supporters, + .contributors { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 0.25rem; + color: var(--text-color); + } + + h1 { + font-size: 1rem; + line-height: 1rem; + color: var(--sub-color); + margin: 0; + font-weight: 300; + } + + p { + margin: 0; + padding: 0; + color: var(--text-color); + } + } +} + +==> monkeytype/src/sass/banners.scss <== +#bannerCenter { + position: fixed; + width: 100%; + z-index: 9999; + .banner { + background: var(--sub-color); + color: var(--bg-color); + display: flex; + justify-content: center; + .container { + max-width: 1000px; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 1rem; + align-items: center; + width: 100%; + justify-items: center; + .image { + // background-image: url(images/merchdropwebsite2.png); + height: 2.3rem; + background-size: cover; + aspect-ratio: 6/1; + background-position: center; + background-repeat: no-repeat; + margin-left: 2rem; + } + .icon { + margin-left: 1rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } + .text { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } + .closeButton { + margin-right: 1rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + transition: 0.125s; + &:hover { + cursor: pointer; + color: var(--text-color); + } + } + } + &.good { + background: var(--main-color); + } + &.bad { + background: var(--error-color); + } + } +} + +==> monkeytype/src/sass/popups.scss <== +.popupWrapper { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + position: fixed; + left: 0; + top: 0; + z-index: 1000; + display: grid; + justify-content: center; + align-items: center; + padding: 2rem 0; +} + +#customTextPopupWrapper { + #customTextPopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 60vw; + .wordfilter { + width: 33%; + justify-self: right; + } + + textarea { + background: rgba(0, 0, 0, 0.1); + padding: 1rem; + color: var(--main-color); + border: none; + outline: none; + font-size: 1rem; + font-family: var(--font); + width: 100%; + border-radius: var(--roundness); + resize: vertical; + height: 200px; + color: var(--text-color); + overflow-x: hidden; + overflow-y: scroll; + } + + .inputs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + align-items: center; + justify-items: left; + } + + .randomInputFields { + display: grid; + grid-template-columns: 1fr auto 1fr; + text-align: center; + align-items: center; + width: 100%; + gap: 1rem; + } + } +} + +#wordFilterPopupWrapper { + #wordFilterPopup { + color: var(--sub-color); + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 400px; + + input { + width: 100%; + } + + .group { + display: grid; + gap: 0.5rem; + } + + .lengthgrid { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto 1fr; + column-gap: 1rem; + } + + .tip { + color: var(--sub-color); + font-size: 0.8rem; + } + + .loadingIndicator { + justify-self: center; + } + } +} + +#quoteRatePopupWrapper { + #quoteRatePopup { + color: var(--sub-color); + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 2rem; + width: 800px; + + display: grid; + grid-template-areas: "ratingStats ratingStats submitButton" "spacer spacer spacer" "quote quote quote"; + grid-template-columns: auto 1fr; + + color: var(--text-color); + + .spacer { + grid-area: spacer; + grid-column: 1/4; + width: 100%; + height: 0.1rem; + border-radius: var(--roundness); + background: var(--sub-color); + opacity: 0.25; + } + + .submitButton { + font-size: 2rem; + grid-area: submitButton; + color: var(--sub-color); + &:hover { + color: var(--text-color); + } + } + + .top { + color: var(--sub-color); + font-size: 0.8rem; + } + + .ratingStats { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; + grid-area: ratingStats; + .top { + font-size: 1rem; + } + .val { + font-size: 2.25rem; + } + } + + .quote { + display: grid; + grid-area: quote; + gap: 1rem; + grid-template-areas: + "text text text" + "id length source"; + grid-template-columns: 1fr 1fr 3fr; + .text { + grid-area: text; + } + .id { + grid-area: id; + } + .length { + grid-area: length; + } + .source { + grid-area: source; + } + } + + .stars { + display: grid; + color: var(--sub-color); + font-size: 2rem; + grid-template-columns: auto auto auto auto auto; + justify-content: flex-start; + align-items: center; + cursor: pointer; + } + .star { + transition: 0.125s; + } + i { + pointer-events: none; + } + .star.active { + color: var(--text-color); + } + } +} + +#simplePopupWrapper { + #simplePopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 400px; + + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + + .inputs { + display: grid; + gap: 1rem; + } + + .text { + font-size: 1rem; + color: var(--text-color); + } + } +} + +#mobileTestConfigPopupWrapper { + #mobileTestConfigPopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 1rem; + display: grid; + gap: 1rem; + width: calc(100vw - 2rem); + // margin-left: 1rem; + max-width: 400px; + + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + + .inputs { + display: grid; + gap: 1rem; + } + + .text { + font-size: 1rem; + color: var(--text-color); + } + + .group { + display: grid; + gap: 0.5rem; + } + } +} + +#customWordAmountPopupWrapper, +#customTestDurationPopupWrapper, +#practiseWordsPopupWrapper, +#pbTablesPopupWrapper { + #customWordAmountPopup, + #customTestDurationPopup, + #practiseWordsPopup, + #pbTablesPopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 400px; + + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + + .tip { + font-size: 0.75rem; + color: var(--sub-color); + } + + .text { + font-size: 1rem; + color: var(--text-color); + } + } + + #customTestDurationPopup { + .preview { + font-size: 0.75rem; + color: var(--sub-color); + } + } +} + +#pbTablesPopupWrapper #pbTablesPopup { + .title { + color: var(--text-color); + } + min-width: 50rem; + max-height: calc(100vh - 10rem); + overflow-y: scroll; + table { + border-spacing: 0; + border-collapse: collapse; + color: var(--text-color); + + td { + padding: 0.5rem 0.5rem; + } + + thead { + color: var(--sub-color); + font-size: 0.75rem; + } + + tbody tr:nth-child(odd) td { + background: rgba(0, 0, 0, 0.1); + } + + td.infoIcons span { + margin: 0 0.1rem; + } + .miniResultChartButton { + opacity: 0.25; + transition: 0.25s; + cursor: pointer; + &:hover { + opacity: 1; + } + } + .sub { + opacity: 0.5; + } + td { + text-align: right; + } + td:nth-child(6), + td:nth-child(7) { + text-align: center; + } + tbody td:nth-child(1) { + font-size: 1.5rem; + } + } +} + +#customThemeShareWrapper { + #customThemeShare { + width: 50vw; + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + overflow-y: scroll; + } +} + +#quoteSearchPopupWrapper { + #quoteSearchPopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 80vw; + max-width: 1000px; + height: 80vh; + grid-template-rows: auto auto auto 1fr; + + #quoteSearchTop { + display: flex; + justify-content: space-between; + + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + + .buttons { + width: 33%; + display: grid; + gap: 0.5rem; + .button { + width: 100%; + } + } + } + + #extraResults { + text-align: center; + color: var(--sub-color); + } + #quoteSearchResults { + display: grid; + gap: 0.5rem; + height: auto; + overflow-y: scroll; + + .searchResult { + display: grid; + grid-template-columns: 1fr 1fr 3fr 0fr; + grid-template-areas: + "text text text text" + "id len source report"; + grid-auto-rows: auto; + width: 100%; + gap: 0.5rem; + transition: 0.25s; + padding: 1rem; + box-sizing: border-box; + user-select: none; + cursor: pointer; + height: min-content; + + .text { + grid-area: text; + overflow: visible; + color: var(--text-color); + } + .id { + grid-area: id; + font-size: 0.8rem; + color: var(--sub-color); + } + .length { + grid-area: len; + font-size: 0.8rem; + color: var(--sub-color); + } + .source { + grid-area: source; + font-size: 0.8rem; + color: var(--sub-color); + } + .resultChevron { + grid-area: chevron; + display: flex; + align-items: center; + justify-items: center; + color: var(--sub-color); + font-size: 2rem; + } + .report { + grid-area: report; + color: var(--sub-color); + transition: 0.25s; + &:hover { + color: var(--text-color); + } + } + .sub { + opacity: 0.5; + } + } + .searchResult:hover { + background: rgba(0, 0, 0, 0.1); + border-radius: 5px; + } + } + } +} +#settingsImportWrapper { + #settingsImport { + width: 50vw; + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + overflow-y: scroll; + } +} + +#quoteSubmitPopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 1000px; + grid-template-rows: auto auto auto auto auto auto auto auto auto; + height: 100%; + max-height: 40rem; + overflow-y: scroll; + + label { + color: var(--sub-color); + margin-bottom: -1rem; + } + + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + textarea { + resize: vertical; + width: 100%; + padding: 10px; + line-height: 1.2rem; + min-height: 5rem; + } + .characterCount { + position: absolute; + top: -1.25rem; + right: 0.25rem; + color: var(--sub-color); + user-select: none; + &.red { + color: var(--error-color); + } + } +} + +#quoteApprovePopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 1000px; + height: 80vh; + grid-template-rows: auto 1fr; + + .top { + display: flex; + justify-content: space-between; + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + .button { + width: 33%; + } + } + + .quotes { + display: grid; + gap: 1rem; + height: auto; + overflow-y: scroll; + align-content: baseline; + + .quote { + display: grid; + grid-template-columns: 1fr auto; + grid-auto-rows: auto 2rem; + width: 100%; + gap: 1rem; + transition: 0.25s; + box-sizing: border-box; + user-select: none; + height: min-content; + margin-bottom: 1rem; + + .text { + grid-column: 1/2; + grid-row: 1/2; + overflow: visible; + color: var(--text-color); + resize: vertical; + min-height: 4rem; + } + .source { + grid-column: 1/2; + grid-row: 2/3; + color: var(--text-color); + } + .buttons { + display: flex; + flex-direction: column; + justify-content: center; + margin-right: 1rem; + grid-column: 2/3; + grid-row: 1/4; + color: var(--sub-color); + } + + .bottom { + display: flex; + justify-content: space-around; + color: var(--sub-color); + .length.red { + color: var(--error-color); + } + } + + .sub { + opacity: 0.5; + } + } + .searchResult:hover { + background: rgba(0, 0, 0, 0.1); + border-radius: 5px; + } + } +} + +#quoteReportPopupWrapper { + #quoteReportPopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 1000px; + grid-template-rows: auto auto auto auto auto auto auto auto auto; + height: auto; + max-height: 40rem; + overflow-y: scroll; + + label { + color: var(--sub-color); + margin-bottom: -1rem; + } + + .text { + color: var(--sub-color); + } + + .quote { + font-size: 1.5rem; + } + + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + + textarea { + resize: vertical; + width: 100%; + padding: 10px; + line-height: 1.2rem; + min-height: 5rem; + } + + .characterCount { + position: absolute; + top: -1.25rem; + right: 0.25rem; + color: var(--sub-color); + user-select: none; + &.red { + color: var(--error-color); + } + } + } +} + +#resultEditTagsPanelWrapper { + #resultEditTagsPanel { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + overflow-y: scroll; + width: 500px; + + .buttons { + display: grid; + gap: 0.1rem; + grid-template-columns: 1fr 1fr 1fr; + } + } +} + +#versionHistoryWrapper { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + position: fixed; + left: 0; + top: 0; + z-index: 1000; + display: grid; + justify-content: center; + align-items: start; + padding: 5rem 0; + + #versionHistory { + width: 75vw; + height: 100%; + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + @extend .ffscroll; + overflow-y: scroll; + + .tip { + text-align: center; + color: var(--sub-color); + } + + .releases { + display: grid; + gap: 4rem; + + .release { + display: grid; + grid-template-areas: + "title date" + "body body"; + + .title { + grid-area: title; + font-size: 2rem; + color: var(--sub-color); + } + + .date { + grid-area: date; + text-align: right; + color: var(--sub-color); + align-self: center; + } + + .body { + grid-area: body; + color: var(--text-color); + } + + &:last-child { + margin-bottom: 2rem; + } + } + } + } +} + +#supportMeWrapper { + #supportMe { + width: 900px; + // height: 400px; + overflow-y: scroll; + max-height: 100%; + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + grid-template-rows: auto auto auto; + gap: 2rem; + @extend .ffscroll; + + .title { + font-size: 2rem; + line-height: 2rem; + color: var(--main-color); + } + + .text { + color: var(--text-color); + } + + .subtext { + color: var(--sub-color); + font-size: 0.75rem; + } + + .buttons { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 1rem; + + .button { + display: block; + width: 100%; + height: 100%; + padding: 2rem 0; + display: grid; + gap: 1rem; + text-decoration: none; + .text { + transition: 0.25s; + } + &:hover .text { + color: var(--bg-color); + } + .icon { + font-size: 5rem; + line-height: 5rem; + } + } + } + } +} + +#contactPopupWrapper { + #contactPopup { + // height: 400px; + overflow-y: scroll; + max-height: 100%; + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + grid-template-rows: auto auto auto; + gap: 2rem; + @extend .ffscroll; + margin: 0 2rem; + max-width: 900px; + + .title { + font-size: 2rem; + line-height: 2rem; + color: var(--main-color); + } + + .text { + color: var(--text-color); + span { + color: var(--error-color); + } + } + + .subtext { + color: var(--sub-color); + font-size: 0.75rem; + grid-area: subtext; + } + + .buttons { + display: grid; + gap: 1rem; + grid-template-columns: 1fr 1fr; + + .button { + display: block; + width: 100%; + height: 100%; + padding: 1rem 0; + display: grid; + // gap: 0.5rem; + text-decoration: none; + grid-template-areas: "icon textgroup"; + grid-template-columns: auto 1fr; + text-align: left; + align-items: center; + .textGroup { + grid-area: textgroup; + } + .text { + font-size: 1.5rem; + line-height: 2rem; + transition: 0.25s; + } + &:hover .text { + color: var(--bg-color); + } + .icon { + grid-area: icon; + font-size: 2rem; + line-height: 2rem; + padding: 0 1rem; + } + } + } + } +} + +#presetWrapper { + #presetEdit { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + overflow-y: scroll; + } +} + +#tagsWrapper { + #tagsEdit { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + overflow-y: scroll; + } +} + +==> monkeytype/src/sass/account.scss <== +.signOut { + font-size: 1rem; + line-height: 1rem; + justify-self: end; + // background: var(--sub-color); + color: var(--sub-color); + width: fit-content; + width: -moz-fit-content; + padding: 0.5rem; + border-radius: var(--roundness); + cursor: pointer; + transition: 0.25s; + float: right; + + &:hover { + color: var(--text-color); + } + + .fas { + margin-right: 0.5rem; + } +} + +.pageAccount { + display: grid; + gap: 1rem; + + .content { + display: grid; + gap: 2rem; + } + + .sendVerificationEmail { + cursor: pointer; + } + + .timePbTable, + .wordsPbTable { + .sub { + opacity: 0.5; + } + td { + text-align: right; + } + tbody td:nth-child(1) { + font-size: 1.5rem; + } + } + + .showAllTimePbs, + .showAllWordsPbs { + margin-top: 1rem; + } + + .topFilters .buttons { + display: flex; + justify-content: space-evenly; + gap: 1rem; + .button { + width: 100%; + } + } + + .miniResultChartWrapper { + // pointer-events: none; + z-index: 999; + display: none; + height: 15rem; + background: var(--bg-color); + width: 45rem; + position: absolute; + border-radius: var(--roundness); + padding: 1rem; + // box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25); + } + + .miniResultChartBg { + display: none; + z-index: 998; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.25); + position: fixed; + left: 0; + top: 0; + } + + .doublegroup { + display: grid; + grid-auto-flow: column; + gap: 1rem; + .titleAndTable { + .title { + color: var(--sub-color); + } + } + } + + .triplegroup { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; + + .text { + align-self: center; + color: var(--sub-color); + } + } + + .group { + &.noDataError { + margin: 20rem 0; + // height: 30rem; + // line-height: 30rem; + text-align: center; + } + + &.createdDate { + text-align: center; + color: var(--sub-color); + } + + &.personalBestTables { + .tables { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + } + } + + &.history { + .active { + animation: flashHighlight 4s linear 0s 1; + } + + .loadMoreButton { + background: rgba(0, 0, 0, 0.1); + color: var(--text-color); + text-align: center; + padding: 0.5rem; + border-radius: var(--roundness); + cursor: pointer; + -webkit-transition: 0.25s; + transition: 0.25s; + -webkit-user-select: none; + display: -ms-grid; + display: grid; + -ms-flex-line-pack: center; + align-content: center; + margin-top: 1rem; + + &:hover, + &:focus { + color: var(--bg-color); + background: var(--text-color); + } + } + } + + .title { + color: var(--sub-color); + } + + .val { + font-size: 3rem; + line-height: 3.5rem; + } + + .chartjs-render-monitor { + width: 100% !important; + } + + &.chart { + position: relative; + + .above { + display: flex; + justify-content: center; + margin-bottom: 1rem; + color: var(--sub-color); + flex-wrap: wrap; + + .group { + display: flex; + align-items: center; + } + + .fas, + .punc { + margin-right: 0.25rem; + } + + .spacer { + width: 1rem; + } + } + + .below { + text-align: center; + color: var(--sub-color); + margin-top: 1rem; + display: grid; + grid-template-columns: auto 300px; + align-items: center; + .text { + height: min-content; + } + .buttons { + display: grid; + gap: 0.5rem; + } + } + .chart { + height: 400px; + } + .chartPreloader { + position: absolute; + width: 100%; + background: rgba(0, 0, 0, 0.5); + height: 100%; + display: grid; + align-items: center; + justify-content: center; + font-size: 5rem; + text-shadow: 0 0 3rem black; + } + } + } + + table { + border-spacing: 0; + border-collapse: collapse; + color: var(--text-color); + + td { + padding: 0.5rem 0.5rem; + } + + thead { + color: var(--sub-color); + font-size: 0.75rem; + } + + tbody tr:nth-child(odd) td { + background: rgba(0, 0, 0, 0.1); + } + + td.infoIcons span { + margin: 0 0.1rem; + } + .miniResultChartButton { + opacity: 0.25; + transition: 0.25s; + cursor: pointer; + &:hover { + opacity: 1; + } + } + } + + #resultEditTags { + transition: 0.25s; + &:hover { + cursor: pointer; + color: var(--text-color); + opacity: 1 !important; + } + } +} + +.pageAccount { + .group.filterButtons { + gap: 1rem; + display: grid; + grid-template-columns: 1fr 1fr; + + .buttonsAndTitle { + height: fit-content; + height: -moz-fit-content; + display: grid; + gap: 0.25rem; + color: var(--sub-color); + line-height: 1rem; + font-size: 1rem; + + &.testDate .buttons, + &.languages .buttons, + &.layouts .buttons, + &.funbox .buttons, + &.tags .buttons { + grid-template-columns: repeat(4, 1fr); + grid-auto-flow: unset; + } + } + + .buttons { + display: grid; + grid-auto-flow: column; + gap: 1rem; + + .button { + background: rgba(0, 0, 0, 0.1); + color: var(--text-color); + text-align: center; + padding: 0.5rem; + border-radius: var(--roundness); + cursor: pointer; + transition: 0.25s; + -webkit-user-select: none; + display: grid; + align-content: center; + + &.active { + background: var(--main-color); + color: var(--bg-color); + } + + &:hover { + color: var(--bg-color); + background: var(--main-color); + } + } + } + } +} + +.header-sorted { + font-weight: bold; +} + +.sortable:hover { + cursor: pointer; + user-select: none; + background-color: rgba(0, 0, 0, 0.1); +} + +==> monkeytype/src/sass/monkey.scss <== +#monkey { + width: 308px; + height: 0; + margin: 0 auto; + animation: shake 0s infinite; + div { + height: 200px; + width: 308px; + position: fixed; + } + .up { + background-image: url("../images/monkey/m3.png"); + } + .left { + background-image: url("../images/monkey/m1.png"); + } + .right { + background-image: url("../images/monkey/m2.png"); + } + .both { + background-image: url("../images/monkey/m4.png"); + } + .fast { + .up { + background-image: url("../images/monkey/m3_fast.png"); + } + .left { + background-image: url("../images/monkey/m1_fast.png"); + } + .right { + background-image: url("../images/monkey/m2_fast.png"); + } + .both { + background-image: url("../images/monkey/m4_fast.png"); + } + } +} + +==> monkeytype/src/sass/core.scss <== +@import url("https://fonts.googleapis.com/css2?family=Fira+Code&family=IBM+Plex+Sans:wght@600&family=Inconsolata&family=Roboto+Mono&family=Source+Code+Pro&family=JetBrains+Mono&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Montserrat&family=Roboto&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Lexend+Deca&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Oxygen&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Nunito&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Itim&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Comfortaa&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Coming+Soon&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Lato&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Lalezar&display=swap"); +@import url("https://fonts.googleapis.com/css?family=Noto+Naskh+Arabic&display=swap"); + +:root { + --roundness: 0.5rem; + --font: "Roboto Mono"; + // scroll-behavior: smooth; + scroll-padding-top: 2rem; +} + +::placeholder { + color: var(--sub-color); + opacity: 1; + /* Firefox */ +} + +#nocss { + display: none !important; + pointer-events: none; +} + +.ffscroll { + scrollbar-width: thin; + scrollbar-color: var(--sub-color) transparent; +} + +html { + @extend .ffscroll; + overflow-y: scroll; +} + +a { + display: inline-block; + color: var(--sub-color); + transition: 0.25s; + &:hover { + color: var(--text-color); + } +} + +body { + margin: 0; + padding: 0; + min-height: 100vh; + font-family: var(--font); + color: var(--text-color); + overflow-x: hidden; + background: var(--bg-color); +} + +.customBackground { + content: ""; + width: 100vw; + height: 100vh; + position: fixed; + left: 0; + top: 0; + background-position: center center; + background-repeat: no-repeat; + z-index: -999; + justify-content: center; + align-items: center; + display: flex; +} + +#backgroundLoader { + height: 3px; + position: fixed; + width: 100%; + background: var(--main-color); + animation: loader 2s cubic-bezier(0.38, 0.16, 0.57, 0.82) infinite; + z-index: 9999; +} + +label.checkbox { + span { + display: block; + font-size: 0.76rem; + color: var(--sub-color); + margin-left: 1.5rem; + } + + input { + margin: 0 !important; + cursor: pointer; + width: 0; + height: 0; + display: none; + + & ~ .customTextCheckbox { + width: 12px; + height: 12px; + background: rgba(0, 0, 0, 0.1); + border-radius: 2px; + box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.1); + display: inline-block; + margin: 0 0.5rem 0 0.25rem; + transition: 0.25s; + } + + &:checked ~ .customTextCheckbox { + background: var(--main-color); + } + } +} + +#centerContent { + max-width: 1000px; + // min-width: 500px; + // margin: 0 auto; + display: grid; + grid-auto-flow: row; + min-height: 100vh; + padding-left: 2rem; + padding-right: 2rem; + padding-top: 2rem; + padding-bottom: 2rem; + gap: 2rem; + align-items: center; + z-index: 999; + grid-template-rows: auto 1fr auto; + width: 100%; + &.wide125 { + max-width: 1250px; + } + &.wide150 { + max-width: 1500px; + } + &.wide200 { + max-width: 2000px; + } + &.widemax { + max-width: unset; + } +} + +key { + color: var(--bg-color); + background-color: var(--sub-color); + /* font-weight: bold; */ + padding: 0.1rem 0.3rem; + margin: 3px 0; + border-radius: 0.1rem; + display: inline-block; + font-size: 0.7rem; + line-height: 0.7rem; +} + +.pageLoading { + display: grid; + justify-content: center; +} + +.pageLoading, +.pageAccount { + .preloader { + text-align: center; + justify-self: center; + display: grid; + .barWrapper { + display: grid; + gap: 1rem; + grid-row: 1; + grid-column: 1; + .bar { + width: 20rem; + height: 0.5rem; + background: rgba(0, 0, 0, 0.1); + border-radius: var(--roundness); + .fill { + height: 100%; + width: 0%; + background: var(--main-color); + border-radius: var(--roundness); + // transition: 1s; + } + } + } + .icon { + grid-row: 1; + grid-column: 1; + font-size: 2rem; + color: var(--main-color); + margin-bottom: 1rem; + } + } +} + +.devIndicator { + position: fixed; + font-size: 3rem; + color: var(--sub-color); + opacity: 0.25; + z-index: -1; + + &.tl { + top: 2rem; + left: 2rem; + } + + &.tr { + top: 2rem; + right: 2rem; + } + + &.bl { + bottom: 2rem; + left: 2rem; + } + + &.br { + bottom: 2rem; + right: 2rem; + } +} + +* { + box-sizing: border-box; +} + +.hidden { + display: none !important; +} + +.invisible { + opacity: 0 !important; + pointer-events: none !important; +} + +.button { + color: var(--text-color); + cursor: pointer; + transition: 0.25s; + padding: 0.4rem; + border-radius: var(--roundness); + background: rgba(0, 0, 0, 0.1); + text-align: center; + -webkit-user-select: none; + // display: grid; + align-content: center; + height: min-content; + height: -moz-min-content; + line-height: 1rem; + + &:hover { + color: var(--bg-color); + background: var(--text-color); + outline: none; + } + &:focus { + color: var(--main-color); + background: var(--sub-color); + outline: none; + } + + &.active { + background: var(--main-color); + color: var(--bg-color); + &:hover { + // color: var(--text-color); + background: var(--text-color); + outline: none; + } + &:focus { + color: var(--bg-color); + background: var(--main-color); + outline: none; + } + } + + &.disabled { + opacity: 0.5; + cursor: default; + pointer-events: none; + &:hover { + color: var(--text-color); + background: rgba(0, 0, 0, 0.1); + outline: none; + } + } + + &.disabled.active { + opacity: 0.5; + cursor: default; + pointer-events: none; + &:hover { + color: var(--bg-color); + background: var(--main-color); + outline: none; + } + } +} + +.text-button { + transition: 0.25s; + color: var(--sub-color); + cursor: pointer; + margin-right: 0.25rem; + cursor: pointer; + outline: none; + + &.active { + color: var(--main-color); + } + + &:hover, + &:focus { + color: var(--text-color); + } +} + +.icon-button { + display: grid; + grid-auto-flow: column; + align-content: center; + transition: 0.25s; + padding: 0.5rem; + border-radius: var(--roundness); + cursor: pointer; + + &:hover { + color: var(--text-color); + } + &:focus { + // background: var(--sub-color); + color: var(--sub-color); + border: none; + outline: none; + } + &.disabled { + opacity: 0.5; + cursor: default; + pointer-events: none; + } +} + +.scrollToTopButton { + bottom: 2rem; + right: 2rem; + position: fixed; + font-size: 2rem; + width: 4rem; + height: 4rem; + text-align: center; + line-height: 4rem; + background: var(--bg-color); + border-radius: 99rem; + z-index: 99; + cursor: pointer; + color: var(--sub-color); + transition: 0.25s; + &:hover { + color: var(--text-color); + } +} + +==> monkeytype/src/sass/login.scss <== +.pageLogin { + display: flex; + grid-auto-flow: column; + gap: 1rem; + justify-content: space-around; + align-items: center; + + .side { + display: grid; + gap: 0.5rem; + justify-content: center; + grid-template-columns: 1fr; + + input { + width: 15rem; + } + + &.login { + grid-template-areas: + "title forgotButton" + "form form"; + + .title { + grid-area: title; + } + + #forgotPasswordButton { + grid-area: forgotButton; + font-size: 0.75rem; + line-height: 0.75rem; + height: fit-content; + height: -moz-fit-content; + align-self: center; + justify-self: right; + padding: 0.25rem 0; + color: var(--sub-color); + cursor: pointer; + transition: 0.25s; + + &:hover { + color: var(--text-color); + } + } + + form { + grid-area: form; + } + } + } + + form { + display: grid; + gap: 0.5rem; + width: 100%; + } + + .preloader { + position: fixed; + left: 50%; + top: 50%; + font-size: 2rem; + transform: translate(-50%, -50%); + color: var(--main-color); + transition: 0.25s; + } +} + +==> monkeytype/src/sass/z_media-queries.scss <== +@media only screen and (max-width: 1200px) { + #leaderboardsWrapper { + #leaderboards { + .tables { + grid-template-columns: unset; + } + .tables .rightTableWrapper, + .tables .leftTableWrapper { + height: calc(50vh - 12rem); + } + } + } +} + +@media only screen and (max-width: 1050px) { + .pageSettings .section.fullWidth .buttons { + grid-template-columns: 1fr 1fr 1fr; + } + + #result .morestats { + gap: 1rem; + grid-template-rows: 1fr 1fr; + } + #supportMe { + width: 90vw !important; + .buttons { + .button { + .icon { + font-size: 3rem !important; + line-height: 3rem !important; + } + } + } + } + #customTextPopup { + width: 80vw !important; + + .wordfilter.button { + width: 50% !important; + } + } +} + +@media only screen and (max-width: 1000px) { + #quoteRatePopup { + width: 90vw !important; + } + #bottom { + .leftright { + .left { + gap: 0.25rem 1rem; + display: grid; + grid-template-rows: 1fr 1fr; + grid-auto-flow: row; + grid-template-columns: auto auto auto auto; + } + .right { + display: grid; + grid-template-rows: 1fr 1fr; + gap: 0.25rem 1rem; + } + } + } +} + +@media only screen and (max-width: 900px) { + // #leaderboards { + // .mainTitle { + // font-size: 1.5rem !important; + // line-height: 1.5rem !important; + // } + // } + .merchBanner { + img { + display: none; + } + .text { + padding: 0.25rem 0; + } + } + .pageAccount { + .group.personalBestTables { + .tables { + grid-template-columns: 1fr; + } + } + .group.history { + table { + thead, + tbody { + td:nth-child(1), + td:nth-child(8), + td:nth-child(9) { + display: none; + } + } + } + } + } +} + +@media only screen and (max-width: 800px) { + .pageSettings .settingsGroup.quickNav .links { + grid-auto-flow: unset; + grid-template-columns: 1fr 1fr 1fr; + justify-items: center; + } + #bannerCenter .banner .container { + grid-template-columns: 1fr auto; + .image { + display: none; + } + .lefticon { + display: none; + } + .text { + margin-left: 2rem; + } + } + #centerContent { + #top { + grid-template-areas: + "logo config" + "menu config"; + grid-template-columns: auto auto; + .logo { + margin-bottom: 0; + } + } + + #menu { + gap: 0.5rem; + font-size: 0.8rem; + line-height: 0.8rem; + margin-top: -0.5rem; + + .icon-button { + padding: 0.25rem; + } + } + } + + #contactPopupWrapper #contactPopup .buttons { + grid-template-columns: 1fr; + } + + .pageAbout .section { + .contributors, + .supporters { + grid-template-columns: 1fr 1fr 1fr; + } + .contactButtons, + .supportButtons { + grid-template-columns: 1fr 1fr; + } + } + + .pageSettings .section.customBackgroundFilter { + .groups { + grid-template-columns: 1fr; + } + .saveContainer { + grid-column: -1/-2; + } + } + + .pageSettings { + .section.themes .tabContent.customTheme { + } + } + + #commandLine, + #commandLineInput { + width: 600px !important; + } +} + +@media only screen and (max-width: 700px) { + #leaderboardsWrapper { + #leaderboards { + .leaderboardsTop { + flex-direction: column; + align-items: baseline; + } + } + } + .pageAccount { + .triplegroup { + grid-template-columns: 1fr 1fr; + .emptygroup { + display: none; + } + } + .group.chart .below { + grid-template-columns: 1fr; + gap: 0.5rem; + } + .group.topFilters .buttonsAndTitle .buttons { + display: grid; + justify-content: unset; + } + .group.history { + table { + thead, + tbody { + td:nth-child(6) { + display: none; + } + } + } + } + } +} + +@media only screen and (max-width: 650px) { + #quoteRatePopup { + .ratingStats { + grid-template-columns: 1fr 1fr !important; + } + .quote { + grid-template-areas: + "text text text" + "source source source" + "id length length" !important; + } + } + .pageSettings .section { + grid-template-columns: 1fr; + grid-template-areas: + "title" + "text" + "buttons"; + + & > .text { + margin-bottom: 1rem; + } + } + + #result { + .buttons { + grid-template-rows: 1fr 1fr 1fr; + #nextTestButton { + grid-column: 1/5; + width: 100%; + text-align: center; + } + } + } + + #supportMe { + width: 80vw !important; + .buttons { + grid-template-columns: none !important; + .button { + grid-template-columns: auto auto; + align-items: center; + .icon { + font-size: 2rem !important; + line-height: 2rem !important; + } + } + } + } + + .pageSettings .section.fullWidth .buttons { + grid-template-columns: 1fr 1fr; + } +} + +@media only screen and (max-width: 600px) { + .pageAbout .section .supporters, + .pageAbout .section .contributors { + grid-template-columns: 1fr 1fr; + } + #top .logo .bottom { + margin-top: 0; + } + .pageLogin { + display: grid; + gap: 5rem; + grid-auto-flow: unset; + } + #middle { + #result { + grid-template-areas: + "stats stats" + "chart chart" + "morestats morestats"; + .stats { + grid-template-areas: "wpm acc"; + gap: 2rem; + } + .stats.morestats { + grid-template-rows: 1fr 1fr 1fr; + gap: 1rem; + } + } + } + #commandLine, + #commandLineInput { + width: 500px !important; + } + #customTextPopupWrapper { + #customTextPopup { + .wordfilter.button { + width: 100% !important; + justify-self: auto; + } + + .inputs { + display: flex !important; + flex-direction: column; + justify-content: flex-start; + } + } + } + #leaderboardsWrapper #leaderboards { + padding: 1rem; + gap: 1rem; + .mainTitle { + font-size: 2rem; + line-height: 2rem; + } + .title { + font-size: 1rem; + } + .leaderboardsTop { + .buttonGroup { + gap: 0.1rem !important; + + .button { + padding: 0.4rem !important; + font-size: 0.7rem !important; + } + } + } + } + .pageAccount { + .group.history { + table { + thead, + tbody { + td:nth-child(7), + td:nth-child(5) { + display: none; + } + } + } + } + } +} + +@media only screen and (max-width: 550px) { + .keymap { + .row { + height: 1.25rem; + } + .keymap-key { + width: 1.25rem; + height: 1.25rem; + border-radius: 0.3rem; + font-size: 0.6rem; + } + } + + #contactPopupWrapper #contactPopup .buttons .button .text { + font-size: 1rem; + } + #contactPopupWrapper #contactPopup .buttons .button .icon { + font-size: 1.5rem; + line-height: 1.5rem; + } + #contactPopupWrapper #contactPopup { + padding: 1rem; + } + .pageAbout .section .supporters, + .pageAbout .section .contributors { + grid-template-columns: 1fr; + } + + #simplePopupWrapper #simplePopup { + width: 90vw; + } + + .pageSettings { + .settingsGroup.quickNav { + display: none; + } + .section.fullWidth .buttons { + grid-template-columns: 1fr; + } + .section .buttons { + grid-auto-flow: row; + } + .section.customBackgroundFilter .groups .group { + grid-template-columns: auto 1fr; + .title { + grid-column: 1/3; + } + } + } + + .pageAbout .section { + .contactButtons, + .supportButtons { + grid-template-columns: 1fr; + } + } + + .pageAccount { + .triplegroup { + grid-template-columns: 1fr; + } + .group.history { + table { + thead, + tbody { + td:nth-child(3) { + display: none; + } + } + } + } + } + + #top { + align-items: self-end; + .logo { + .icon { + width: 1.5rem !important; + } + .text { + font-size: 1.5rem !important; + margin-bottom: 0.3rem !important; + } + .bottom { + font-size: 1.75rem; + line-height: 1.75rem; + margin-top: 0; + } + .top { + display: none; + } + } + #menu { + .icon-button { + padding: 0; + } + } + } + #bottom { + .leftright { + .left { + gap: 0.25rem 1rem; + display: grid; + grid-template-rows: 1fr 1fr 1fr; + grid-template-columns: auto auto auto; + grid-auto-flow: row; + } + .right { + display: grid; + grid-template-rows: 1fr 1fr 1fr; + gap: 0.25rem 1rem; + } + } + } + #centerContent { + #top { + grid-template-columns: 1fr auto; + .desktopConfig { + display: none; + } + .mobileConfig { + display: block; + } + } + padding: 1rem; + } + #middle { + #result { + .stats { + grid-template-areas: + "wpm" + "acc"; + gap: 1rem; + } + } + } + #result { + .buttons { + grid-template-rows: 1fr 1fr 1fr 1fr; + #nextTestButton { + grid-column: 1/3; + width: 100%; + text-align: center; + } + } + } + #commandLine, + #commandLineInput { + width: 400px !important; + } +} + +@media only screen and (max-width: 400px) { + #top .logo .bottom { + font-size: 1.5rem; + line-height: 1.5rem; + margin-top: 0; + } + + #top .config { + grid-gap: 0.25rem; + .group .buttons { + font-size: 0.65rem; + line-height: 0.65rem; + } + } + + #bottom { + font-size: 0.65rem; + .leftright { + grid-template-columns: 1fr 1fr; + .left { + grid-template-rows: 1fr 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr; + grid-auto-flow: row; + } + .right { + // justify-self: left; + // grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; + gap: 0.25rem 1rem; + } + } + } + + #commandLine, + #commandLineInput { + width: 300px !important; + } + + #leaderboardsWrapper #leaderboards .tables .titleAndTable .titleAndButtons { + grid-template-columns: unset; + } +} + +@media only screen and (max-width: 350px) { + .keymap { + display: none !important; + } + .pageLogin .side input { + width: 90vw; + } +} + +@media (hover: none) and (pointer: coarse) { + #commandLineMobileButton { + display: block !important; + } +} + +==> monkeytype/src/sass/inputs.scss <== +input, +textarea { + outline: none; + border: none; + border-radius: var(--roundness); + background: rgba(0, 0, 0, 0.1); + color: var(--text-color); + padding: 0.5rem; + font-size: 1rem; + font-family: var(--font); +} + +input[type="range"] { + -webkit-appearance: none; + padding: 0; + width: 100%; + height: 1rem; + border-radius: var(--roundness); + &::-webkit-slider-thumb { + -webkit-appearance: none; + padding: 0; + border: none; + width: 2rem; + height: 1rem; + border-radius: var(--roundness); + background-color: var(--main-color); + } + + &::-moz-range-thumb { + -webkit-appearance: none; + padding: 0; + border: none; + width: 2rem; + height: 1rem; + border-radius: var(--roundness); + background-color: var(--main-color); + } +} + +input[type="color"] { + height: 3px; //i dont know why its 3, but safari gods have spoken - 3 makes it work + opacity: 0; + padding: 0; + margin: 0; + position: absolute; + pointer-events: none; +} + +::-moz-color-swatch { + border: none; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +.select2-dropdown { + background-color: var(--bg-color); + color: var(--text-color); + outline: none; +} + +.select2-selection { + background: rgba(0, 0, 0, 0.1); + height: fit-content; + height: -moz-fit-content; + padding: 5px; + border-radius: var(--roundness); + color: var(--text-color); + font: var(--font); + border: none; + outline: none; +} + +.select2-container--default + .select2-selection--single + .select2-selection__rendered { + color: var(--text-color); + outline: none; +} + +.select2-container--default + .select2-results__option--highlighted.select2-results__option--selectable { + background-color: var(--text-color); + color: var(--bg-color); +} + +.select2-container--default .select2-results__option--selected { + background-color: var(--bg-color); + color: var(--sub-color); +} + +.select2-container--open .select2-dropdown--below { + border-color: rgba(0, 0, 0, 0.1); + background: var(--bg-color); + color: var(--sub-color); + border-radius: var(--roundness); +} + +.select2-container--default .select2-selection--single { + color: var(--text-color); + background: rgba(0, 0, 0, 0.1); + outline: none; + border: none; + height: auto; +} + +.select2-selection:focus { + height: fit-content; + height: -moz-fit-content; + padding: 5px; + border-radius: var(--roundness); + color: var(--text-color); + font: var(--font); + border: none; + outline: none; +} +.select2-selection:active { + height: fit-content; + height: -moz-fit-content; + padding: 5px; + border-radius: var(--roundness); + color: var(--text-color); + font: var(--font); + border: none; + outline: none; +} + +.select2-container--default + .select2-selection--single + .select2-selection__arrow { + height: 35px; +} + +.select2-container--default + .select2-selection--single + .select2-selection__arrow + b { + border-color: var(--sub-color) transparent transparent transparent; +} + +.select2-container--default.select2-container--open + .select2-selection--single + .select2-selection__arrow + b { + border-color: var(--sub-color) transparent; +} + +.select2-container--default .select2-search--dropdown .select2-search__field { + border-color: rgba(0, 0, 0, 0.1); + background: var(--bg-color); + color: var(--text-color); + border-radius: var(--roundness); +} + +==> monkeytype/src/sass/commandline.scss <== +#commandLineWrapper { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + position: fixed; + left: 0; + top: 0; + z-index: 1000; + display: grid; + justify-content: center; + align-items: start; + padding: 5rem 0; + + #commandInput { + width: 700px; + background: var(--bg-color); + border-radius: var(--roundness); + + input { + background: var(--bg-color); + padding: 1rem; + color: var(--main-color); + border: none; + outline: none; + font-size: 1rem; + width: 100%; + border-radius: var(--roundness); + } + + .shiftEnter { + padding: 0.5rem 1rem; + font-size: 0.75rem; + line-height: 0.75rem; + color: var(--sub-color); + text-align: center; + } + } + + #commandLine { + width: 700px; + background: var(--bg-color); + border-radius: var(--roundness); + + .searchicon { + color: var(--sub-color); + margin: 1px 1rem 0 1rem; + } + + input { + background: var(--bg-color); + padding: 1rem 1rem 1rem 0; + color: var(--text-color); + border: none; + outline: none; + font-size: 1rem; + width: 100%; + border-radius: var(--roundness); + } + + .separator { + background: black; + width: 100%; + height: 1px; + margin-bottom: 0.5rem; + } + + .listTitle { + color: var(--text-color); + padding: 0.5rem 1rem; + font-size: 0.75rem; + line-height: 0.75rem; + } + + .suggestions { + display: block; + @extend .ffscroll; + overflow-y: scroll; + max-height: calc(100vh - 10rem - 3rem); + display: grid; + cursor: pointer; + user-select: none; + + .entry { + padding: 0.5rem 1rem; + font-size: 0.75rem; + line-height: 0.75rem; + color: var(--sub-color); + display: grid; + grid-template-columns: auto 1fr; + + div { + pointer-events: none; + } + + .textIcon { + font-weight: 900; + /* width: 1.25rem; */ + display: inline-block; + letter-spacing: -0.1rem; + margin-right: 0.5rem; + text-align: center; + width: 1.25em; + } + + .fas { + margin-right: 0.5rem; + } + + &:last-child { + border-radius: 0 0 var(--roundness) var(--roundness); + } + + &.activeMouse { + color: var(--bg-color); + background: var(--text-color); + cursor: pointer; + } + + &.activeKeyboard { + color: var(--bg-color); + background: var(--text-color); + } + + // &:hover { + // color: var(--text-color); + // background: var(--sub-color); + // cursor: pointer; + // } + } + } + } +} + +==> monkeytype/src/sass/notifications.scss <== +#notificationCenter { + width: 350px; + z-index: 99999999; + display: grid; + gap: 1rem; + position: fixed; + right: 1rem; + top: 1rem; + .history { + display: grid; + gap: 1rem; + } + .notif { + user-select: none; + .icon { + color: var(--bg-color); + opacity: 0.5; + padding: 1rem 1rem; + align-items: center; + display: grid; + font-size: 1.25rem; + } + .message { + padding: 1rem 1rem 1rem 0; + .title { + color: var(--bg-color); + font-size: 0.75em; + opacity: 0.5; + line-height: 0.75rem; + } + } + + position: relative; + background: var(--sub-color); + color: var(--bg-color); + display: grid; + grid-template-columns: min-content auto min-content; + border-radius: var(--roundness); + border-width: 0.25rem; + + &.bad { + background-color: var(--error-color); + } + + &.good { + background-color: var(--main-color); + } + + &:hover { + // opacity: .5; + // box-shadow: 0 0 20px rgba(0,0,0,.25); + cursor: pointer; + &::after { + opacity: 1; + } + } + &::after { + transition: 0.125s; + font-family: "Font Awesome 5 Free"; + background: rgba(0, 0, 0, 0.5); + opacity: 0; + font-weight: 900; + content: "\f00d"; + position: absolute; + width: 100%; + height: 100%; + color: var(--bg-color); + font-size: 2.5rem; + display: grid; + /* align-self: center; */ + align-items: center; + text-align: center; + border-radius: var(--roundness); + } + } +} + +==> monkeytype/src/sass/caret.scss <== +#caret { + height: 1.5rem; + background: var(--caret-color); + animation: caretFlashSmooth 1s infinite; + position: absolute; + border-radius: var(--roundness); + // transition: 0.05s; + transform-origin: top left; +} + +#paceCaret { + height: 1.5rem; + // background: var(--sub-color); + background: var(--sub-color); + opacity: 0.5; + position: absolute; + border-radius: var(--roundness); + // transition: 0.25s; + transform-origin: top left; + width: 2px; +} + +#caret, +#paceCaret { + &.off { + width: 0; + } + + &.default { + width: 2px; + } + + &.carrot { + background-color: transparent; + background-image: url("../images/carrot.png"); + background-size: contain; + background-position: center; + background-repeat: no-repeat; + width: 0.25rem; + &.size2 { + margin-left: -0.1rem; + } + &.size3 { + margin-left: -0.2rem; + } + &.size4 { + margin-left: -0.3rem; + } + } + + &.banana { + background-color: transparent; + background-image: url("../images/banana.png"); + background-size: contain; + background-position: center; + background-repeat: no-repeat; + width: 1rem; + &.size2 { + margin-left: -0.1rem; + } + &.size3 { + margin-left: -0.5rem; + } + &.size4 { + margin-left: -0.3rem; + } + } + + &.block { + width: 0.7em; + margin-left: 0.25em; + border-radius: 0; + z-index: -1; + } + + &.outline { + @extend #caret, .block; + animation-name: none; + background: transparent; + border: 1px solid var(--caret-color); + } + + &.underline { + height: 2px; + width: 0.8em; + margin-top: 1.3em; + margin-left: 0.3em; + + &.size125 { + margin-top: 1.8em; + } + + &.size15 { + margin-top: 2.1em; + } + + &.size2 { + margin-top: 2.7em; + } + + &.size3 { + margin-top: 3.9em; + } + &.size4 { + margin-top: 4.7em; + } + } + + &.size125 { + transform: scale(1.25); + } + + &.size15 { + transform: scale(1.45); + } + + &.size2 { + transform: scale(1.9); + } + + &.size3 { + transform: scale(2.8); + } + + &.size4 { + transform: scale(3.7); + } +} + +==> monkeytype/src/sass/test.scss <== +#timerWrapper { + opacity: 0; + transition: 0.25s; + z-index: -1; + position: relative; + z-index: 99; + #timer { + position: fixed; + top: 0; + left: 0; + width: 100vw; + /* height: 0.5rem; */ + height: 0.5rem; + background: black; + /* background: #0f0f0f; */ + /* background: red; */ + // transition: 1s linear; + z-index: -1; + + &.timerMain { + background: var(--main-color); + } + + &.timerSub { + background: var(--sub-color); + } + + &.timerText { + background: var(--text-color); + } + } +} + +.pageTest { + position: relative; + + .ssWatermark { + font-size: 1.25rem; + color: var(--sub-color); + line-height: 1rem; + text-align: right; + } + + #timerNumber { + pointer-events: none; + transition: 0.25s; + height: 0; + color: black; + line-height: 0; + z-index: -1; + text-align: center; + left: 0; + width: 100%; + position: relative; + font-size: 10rem; + opacity: 0; + width: 0; + height: 0; + margin: 0 auto; + display: grid; + justify-content: center; + bottom: 6rem; + transition: none; + } + + #largeLiveWpmAndAcc { + font-size: 10rem; + color: black; + width: 100%; + left: 0; + text-align: center; + z-index: -1; + height: 0; + line-height: 0; + top: 5rem; + position: relative; + display: grid; + grid-auto-flow: column; + justify-content: center; + gap: 5rem; + + #liveWpm { + opacity: 0; + } + + #liveAcc { + opacity: 0; + } + + #liveBurst { + opacity: 0; + } + } + + #largeLiveWpmAndAcc.timerMain, + #timerNumber.timerMain { + color: var(--main-color); + } + + #timer.timerMain { + background: var(--main-color); + } + + #largeLiveWpmAndAcc.timerSub, + #timerNumber.timerSub { + color: var(--sub-color); + } + + #timer.timerSub { + background: var(--sub-color); + } + + #largeLiveWpmAndAcc.timerText, + #timerNumber.timerText { + color: var(--text-color); + } + + #timer.timerText { + background: var(--text-color); + } +} + +#words { + height: fit-content; + height: -moz-fit-content; + display: flex; + flex-wrap: wrap; + width: 100%; + align-content: flex-start; + user-select: none; + padding-bottom: 1em; + + .newline { + width: inherit; + } + + letter { + border-bottom-style: solid; + border-bottom-width: 0.05em; + border-bottom-color: transparent; + &.dead { + border-bottom-width: 0.05em; + border-bottom-color: var(--sub-color); + } + &.tabChar, + &.nlChar { + margin: 0 0.25rem; + opacity: 0.2; + } + } + + /* a little hack for right-to-left languages */ + &.rightToLeftTest { + //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages + direction: rtl; + .word { + //flex-direction: row-reverse; + direction: rtl; + } + } + &.withLigatures { + letter { + display: inline; + } + } + &.blurred { + opacity: 0.25; + filter: blur(4px); + -webkit-filter: blur(4px); + } + + &.flipped { + .word { + color: var(--text-color); + + & letter.dead { + border-bottom-color: var(--sub-color) !important; + } + + & letter.correct { + color: var(--sub-color); + } + + & letter.corrected { + color: var(--sub-color); + border-bottom: 2px dotted var(--main-color); + } + + & letter.extraCorrected { + border-right: 2px dotted var(--main-color); + } + } + } + + &.colorfulMode { + .word { + & letter.dead { + border-bottom-color: var(--main-color) !important; + } + + & letter.correct { + color: var(--main-color); + } + + & letter.corrected { + color: var(--main-color); + border-bottom: 2px dotted var(--text-color); + } + + & letter.extraCorrected { + border-right: 2px dotted var(--text-color); + } + + & letter.incorrect { + color: var(--colorful-error-color); + } + + & letter.incorrect.extra { + color: var(--colorful-error-extra-color); + } + } + } + + &.flipped.colorfulMode { + .word { + color: var(--main-color); + + & letter.dead { + border-bottom-color: var(--sub-color) !important; + } + + & letter.correct { + color: var(--sub-color); + } + + & letter.corrected { + color: var(--sub-color); + border-bottom: 2px dotted var(--main-color); + } + + & letter.extraCorrected { + border-right: 2px dotted var(--main-color); + } + + & letter.incorrect { + color: var(--colorful-error-color); + } + + & letter.incorrect.extra { + color: var(--colorful-error-extra-color); + } + } + } +} + +.word { + margin: 0.25rem; + color: var(--sub-color); + font-variant: no-common-ligatures; + // display: flex; + // transition: 0.25s + /* margin-bottom: 1px; */ + border-bottom: 2px solid transparent; + line-height: 1rem; + letter { + display: inline-block; + } + + &.lastbeforenewline::after { + font-family: "Font Awesome 5 Free"; + font-weight: 600; + content: "\f107"; + margin-left: 0.5rem; + opacity: 0.25; + } + + // transition: .25s; + .wordInputAfter { + opacity: 1; + position: absolute; + background: var(--sub-color); + color: var(--bg-color); + /* background: red; */ + padding: 0.5rem; + /* left: .5rem; */ + margin-left: -0.5rem; + // margin-top: -1.5rem; + border-radius: var(--roundness); + // box-shadow: 0 0 10px rgba(0,0,0,.25); + transition: 0.25s; + text-shadow: none; + top: -0.5rem; + z-index: 10; + cursor: text; + .speed { + font-size: 0.75rem; + } + } +} + +#words.size125 .word { + line-height: 1.25rem; + font-size: 1.25rem; + margin: 0.31rem; +} + +#words.size15 .word { + line-height: 1.5rem; + font-size: 1.5rem; + margin: 0.37rem; +} + +#words.size2 .word { + line-height: 2rem; + font-size: 2rem; + margin: 0.5rem; +} + +#words.size3 .word { + line-height: 3rem; + font-size: 3rem; + margin: 0.75rem; +} + +#words.size4 .word { + line-height: 4rem; + font-size: 4rem; + margin: 1rem; +} + +#words.nospace { + .word { + margin: 0.5rem 0; + } +} + +#words.arrows { + .word { + margin: 0.5rem 0; + letter { + margin: 0 0.25rem; + } + } +} + +.word.error { + /* margin-bottom: 1px; */ + border-bottom: 2px solid var(--error-color); + text-shadow: 1px 0px 0px var(--bg-color), + // 2px 0px 0px var(--bg-color), + -1px 0px 0px var(--bg-color), + // -2px 0px 0px var(--bg-color), + 0px 1px 0px var(--bg-color), + 1px 1px 0px var(--bg-color), -1px 1px 0px var(--bg-color); +} + +#words.noErrorBorder, +#resultWordsHistory.noErrorBorder { + .word.error { + text-shadow: none; + } +} +// .word letter { +// transition: .1s; +// height: 1rem; +// line-height: 1rem; +/* margin: 0 1px; */ +// } + +.word letter.correct { + color: var(--text-color); +} + +.word letter.corrected { + color: var(--text-color); + border-bottom: 2px dotted var(--main-color); +} + +.word letter.extraCorrected { + border-right: 2px dotted var(--main-color); +} + +.word letter.incorrect { + color: var(--error-color); + position: relative; +} + +.word letter.incorrect hint { + position: absolute; + bottom: -1em; + color: var(--text-color); + line-height: initial; + font-size: 0.75em; + text-shadow: none; + padding: 1px; + left: 0; + opacity: 0.5; + text-align: center; + width: 100%; +} + +.word letter.incorrect.extra { + color: var(--error-extra-color); +} + +.word letter.missing { + opacity: 0.5; +} + +#words.flipped.colorfulMode .word.error, +#words.colorfulMode .word.error { + border-bottom: 2px solid var(--colorful-error-color); +} + +#wordsInput { + opacity: 0; + padding: 0; + margin: 0; + border: none; + outline: none; + display: block; + resize: none; + position: fixed; + z-index: -1; + cursor: default; + pointer-events: none; +} + +#capsWarning { + background: var(--main-color); + color: var(--bg-color); + display: table; + position: absolute; + left: 50%; + // top: 66vh; + transform: translateX(-50%) translateY(-50%); + padding: 1rem; + border-radius: var(--roundness); + /* margin-top: 1rem; */ + transition: 0.25s; + z-index: 999; + pointer-events: none; + + i { + margin-right: 0.5rem; + } +} + +#result { + display: grid; + // height: 200px; + gap: 1rem; + // grid-template-columns: auto 1fr; + // justify-content: center; + align-items: center; + grid-template-columns: auto 1fr; + grid-template-areas: + "stats chart" + "morestats morestats"; + // "wordsHistory wordsHistory" + // "buttons buttons" + // "login login" + // "ssw ssw"; + + &:focus { + outline: none; + } + + .buttons { + display: grid; + grid-auto-flow: column; + gap: 1rem; + justify-content: center; + // grid-area: buttons; + grid-column: 1/3; + } + + .ssWatermark { + // grid-area: ssw; + grid-column: 1/3; + } + + #resultWordsHistory, + #resultReplay { + // grid-area: wordsHistory; + color: var(--sub-color); + // grid-column: 1/3; + margin-bottom: 1rem; + .icon-button { + padding: 0; + margin-left: 0.5rem; + } + .heatmapLegend { + display: inline-grid; + grid-template-columns: auto auto auto; + gap: 1rem; + font-size: 0.75rem; + color: var(--sub-color); + width: min-content; + .boxes { + display: flex; + .box { + width: 1rem; + height: 1rem; + } + .box:nth-child(1) { + background: var(--colorful-error-color); + border-radius: var(--roundness) 0 0 var(--roundness); + } + .box:nth-child(2) { + background: var(--colorful-error-color); + filter: opacity(0.6); + } + .box:nth-child(3) { + background: var(--sub-color); + } + .box:nth-child(4) { + background: var(--main-color); + filter: opacity(0.6); + } + .box:nth-child(5) { + background: var(--main-color); + border-radius: 0 var(--roundness) var(--roundness) 0; + } + } + } + .title { + user-select: none; + margin-bottom: 0.25rem; + } + .words { + display: flex; + flex-wrap: wrap; + width: 100%; + align-content: flex-start; + user-select: none; + .word { + position: relative; + margin: 0.18rem 0.6rem 0.15rem 0; + letter.correct { + color: var(--text-color); + } + letter.incorrect { + color: var(--error-color); + } + letter.incorrect.extra { + color: var(--error-extra-color); + } + &.heatmap-0 letter { + color: var(--colorful-error-color); + } + &.heatmap-1 letter { + color: var(--colorful-error-color); + filter: opacity(0.6); + } + &.heatmap-2 letter { + color: var(--sub-color); + } + &.heatmap-3 letter { + color: var(--main-color); + filter: opacity(0.6); + } + &.heatmap-4 letter { + color: var(--main-color); + } + } + &.rightToLeftTest { + //flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages + direction: rtl; + .word { + //flex-direction: row-reverse; + direction: rtl; + } + } + &.withLigatures { + letter { + display: inline; + } + } + } + } + + .chart { + grid-area: chart; + width: 100%; + + canvas { + width: 100% !important; + height: 100%; + } + + max-height: 200px; + height: 200px; + + .title { + color: var(--sub-color); + margin-bottom: 1rem; + } + } + + .loginTip { + grid-column: 1/3; + text-align: center; + color: var(--sub-color); + // grid-area: login; + grid-column: 1/3; + .link { + text-decoration: underline; + display: inline-block; + cursor: pointer; + } + } + + .stats { + grid-area: stats; + display: grid; + // column-gap: 0.5rem; + gap: 0.5rem; + justify-content: center; + align-items: center; + // grid-template-areas: + // "wpm acc" + // "wpm key" + // "raw time" + // "consistency consistency" + // "source source" + // "leaderboards leaderboards" + // "testType infoAndTags"; + // grid-template-areas: + // "wpm acc key consistency testType leaderboards source" + // "wpm raw time nothing infoAndTags leaderboards source"; + grid-template-areas: + "wpm" + "acc"; + margin-bottom: 1rem; + + &.morestats { + display: grid; + grid-auto-flow: column; + grid-template-areas: none; + align-items: flex-start; + justify-content: space-between; + column-gap: 2rem; + grid-area: morestats; + + // grid-template-areas: "raw consistency testType infoAndTags leaderboards source" + // "key time testType infoAndTags leaderboards source"; + .subgroup { + display: grid; + gap: 0.5rem; + } + } + + .group { + // margin-bottom: 0.5rem; + + .top { + color: var(--sub-color); + font-size: 1rem; + line-height: 1rem; + margin-bottom: 0.25rem; + } + + .bottom { + color: var(--main-color); + font-size: 2rem; + line-height: 2rem; + } + + &.time { + .afk, + .timeToday { + color: var(--sub-color); + font-size: 0.75rem; + line-height: 0.75rem; + margin-left: 0.2rem; + } + } + + &.source { + #rateQuoteButton, + #reportQuoteButton { + padding: 0 0.25rem; + } + #rateQuoteButton { + display: inline-grid; + gap: 0.25rem; + } + } + } + + // .infoAndTags { + // display: grid; + // gap: 0.5rem; + // align-self: baseline; + // // grid-area: infoAndTags; + // color: var(--sub-color); + + // .top { + // font-size: 1rem; + // line-height: 1rem; + // } + + // .bottom { + // font-size: 1rem; + // line-height: 1rem; + // } + // } + + .info, + .tags, + .source { + .top { + font-size: 1rem; + line-height: 1rem; + } + + .bottom { + font-size: 1rem; + line-height: 1rem; + } + } + + .source { + max-width: 30rem; + } + + .tags .bottom .fas { + margin-left: 0.5rem; + } + + .wpm { + grid-area: wpm; + + .top { + font-size: 2rem; + line-height: 1.5rem; + display: flex; + // margin-top: -0.5rem; + + // .crownWrapper { + // width: 1.7rem; + // overflow: hidden; + // height: 1.7rem; + // margin-left: 0.5rem; + // // margin-top: 0.98rem; + // margin-top: -0.5rem; + + .crown { + height: 1.7rem; + width: 1.7rem; + margin-left: 0.5rem; + margin-top: -0.2rem; + font-size: 0.7rem; + line-height: 1.7rem; + background: var(--main-color); + color: var(--bg-color); + border-radius: 0.6rem; + text-align: center; + align-self: center; + width: 1.7rem; + height: 1.7rem; + } + // } + } + + .bottom { + font-size: 4rem; + line-height: 4rem; + } + } + + .testType, + .leaderboards { + .bottom { + font-size: 1rem; + line-height: 1rem; + .lbChange .fas { + margin-right: 0.15rem; + } + } + } + + .acc { + grid-area: acc; + + .top { + font-size: 2rem; + line-height: 1.5rem; + } + + .bottom { + font-size: 4rem; + line-height: 4rem; + } + } + + .burst { + grid-area: burst; + + .top { + font-size: 2rem; + line-height: 1.5rem; + } + + .bottom { + font-size: 4rem; + line-height: 4rem; + } + } + + // .key { + // grid-area: key; + // } + + // .time { + // grid-area: time; + // } + + // .raw { + // grid-area: raw; + // } + } +} + +#restartTestButton, +#showWordHistoryButton, +#saveScreenshotButton, +#restartTestButtonWithSameWordset, +#nextTestButton, +#practiseWordsButton, +#watchReplayButton { + position: relative; + border-radius: var(--roundness); + padding: 1rem 2rem; + width: min-content; + width: -moz-min-content; + color: var(--sub-color); + transition: 0.25s; + cursor: pointer; + + &:hover, + &:focus { + color: var(--main-color); + outline: none; + } + + &:focus { + background: var(--sub-color); + } +} + +#retrySavingResultButton { + position: relative; + border-radius: var(--roundness); + padding: 1rem 2rem; + color: var(--error-color); + transition: 0.25s; + cursor: pointer; + width: max-content; + width: -moz-max-content; + background: var(--colorful-error-color); + color: var(--bg-color); + justify-self: center; + justify-content: center; + margin: 0 auto 1rem auto; + user-select: none; + + &:hover, + &:focus { + background: var(--text-color); + outline: none; + } + + &:focus { + background: var(--text-color); + } +} + +#showWordHistoryButton { + opacity: 1; +} + +#replayWords { + cursor: pointer; +} + +#replayStopwatch { + color: var(--main-color); + display: inline-block; + margin: 0; +} + +#restartTestButton { + margin: 0 auto; + margin-top: 1rem; +} + +.pageTest { + #wordsWrapper { + position: relative; + } + #memoryTimer { + background: var(--main-color); + color: var(--bg-color); + padding: 1rem; + border-radius: var(--roundness); + /* width: min-content; */ + text-align: center; + width: max-content; + /* justify-self: center; */ + left: 50%; + position: absolute; + transform: translateX(-50%); + top: -6rem; + user-select: none; + pointer-events: none; + opacity: 0; + } + .outOfFocusWarning { + text-align: center; + height: 0; + line-height: 150px; + z-index: 999; + position: relative; + user-select: none; + pointer-events: none; + } + + #testModesNotice { + display: grid; + grid-auto-flow: column; + gap: 1rem; + color: var(--sub-color); + text-align: center; + margin-bottom: 1.25rem; + height: 1rem; + line-height: 1rem; + transition: 0.125s; + justify-content: center; + user-select: none; + + .fas { + margin-right: 0.5rem; + } + } + #miniTimerAndLiveWpm { + height: 0; + margin-left: 0.37rem; + display: flex; + font-size: 1rem; + line-height: 1rem; + margin-top: -1.5rem; + position: absolute; + color: black; + + .time { + margin-right: 2rem; + } + + .wpm, + .acc { + margin-right: 2rem; + } + + .time, + .wpm, + .acc, + .burst { + opacity: 0; + } + + &.timerMain { + color: var(--main-color); + } + + &.timerSub { + color: var(--sub-color); + } + + &.timerText { + color: var(--text-color); + } + + &.size125 { + margin-top: -1.75rem; + font-size: 1.25rem; + line-height: 1.25rem; + } + &.size15 { + margin-top: -2rem; + font-size: 1.5rem; + line-height: 1.5rem; + } + &.size2 { + margin-top: -2.5rem; + font-size: 2rem; + line-height: 2rem; + } + &.size3 { + margin-top: -3.5rem; + font-size: 3rem; + line-height: 3rem; + } + &.size4 { + margin-top: -4.5rem; + font-size: 4rem; + line-height: 4rem; + } + } +} + +#middle.focus .pageTest { + #testModesNotice { + opacity: 0 !important; + } +} + +==> monkeytype/src/sass/leaderboards.scss <== +#leaderboardsWrapper { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + position: fixed; + left: 0; + top: 0; + z-index: 1000; + display: grid; + justify-content: center; + align-items: center; + padding: 5rem 0; + + #leaderboards { + width: 85vw; + // height: calc(95vh - 5rem); + overflow-y: auto; + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 2rem 0; + grid-template-rows: 3rem auto; + grid-template-areas: + "title buttons" + "tables tables"; + grid-template-columns: 1fr 1fr; + + .leaderboardsTop { + width: 200%; + min-width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + .buttonGroup .button { + padding: 0.4rem 2.18rem; + } + } + + .mainTitle { + font-size: 3rem; + line-height: 3rem; + grid-area: title; + } + + .subTitle { + color: var(--sub-color); + } + + .title { + font-size: 2rem; + line-height: 2rem; + margin-bottom: 0.5rem; + } + + .tables { + grid-area: tables; + display: grid; + gap: 1rem; + grid-template-columns: 1fr 1fr; + font-size: 0.8rem; + width: 100%; + + .sub { + opacity: 0.5; + } + + .alignRight { + text-align: right; + } + + .titleAndTable { + display: grid; + width: 100%; + + .titleAndButtons { + display: grid; + grid-template-columns: 1fr auto; + .buttons { + display: grid; + grid-template-columns: auto 1fr 1fr; + align-items: center; + // margin-top: .1rem; + gap: 1rem; + color: var(--sub-color); + .button { + padding-left: 1rem; + padding-right: 1rem; + } + } + } + + .title { + grid-area: 1/1; + margin-bottom: 0; + line-height: 2.5rem; + } + + .subtitle { + grid-area: 1/1; + align-self: center; + justify-self: right; + color: var(--sub-color); + } + } + + .leftTableWrapper, + .rightTableWrapper { + height: calc(100vh - 22rem); + @extend .ffscroll; + overflow-y: scroll; + overflow-x: auto; + } + + .leftTableWrapper::-webkit-scrollbar, + .rightTableWrapper::-webkit-scrollbar { + height: 5px; + width: 5px; + } + + table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + + tr td:first-child { + text-align: center; + } + + tr.me { + td { + color: var(--main-color); + // font-weight: 900; + } + } + + td { + padding: 0.5rem 0.5rem; + } + + thead { + color: var(--sub-color); + font-size: 0.75rem; + + td { + padding: 0.5rem; + background: var(--bg-color); + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 99; + } + } + + tbody { + color: var(--text-color); + + tr:nth-child(odd) td { + background: rgba(0, 0, 0, 0.1); + } + } + + tfoot { + td { + padding: 1rem 0.5rem; + position: -webkit-sticky; + position: sticky; + bottom: -5px; + background: var(--bg-color); + color: var(--main-color); + z-index: 4; + } + } + + tr { + td:first-child { + padding-left: 1rem; + } + td:last-child { + padding-right: 1rem; + } + } + } + } + + .buttons { + .buttonGroup { + display: grid; + grid-auto-flow: column; + gap: 1rem; + grid-area: 1/2; + } + } + } +} + +==> monkeytype/src/sass/keymap.scss <== +.keymap { + display: grid; + grid-template-rows: 1fr 1fr 1fr; + justify-content: center; + white-space: nowrap; + // height: 140px; + gap: 0.25rem; + margin-top: 1rem; + user-select: none; + + .row { + height: 2rem; + gap: 0.25rem; + } + + .keymap-key { + display: flex; + background-color: transparent; + color: var(--sub-color); + border-radius: var(--roundness); + border: 0.05rem solid; + border-color: var(--sub-color); + text-align: center; + justify-content: center; + align-items: center; + width: 2rem; + height: 2rem; + position: relative; + + .bump { + width: 0.75rem; + height: 0.05rem; + background: var(--sub-color); + position: absolute; + border-radius: var(--roundness); + // margin-top: 1.5rem; + bottom: 0.15rem; + } + + &.active-key { + color: var(--bg-color); + background-color: var(--main-color); + border-color: var(--main-color); + + .bump { + background: var(--bg-color); + } + } + + &#KeySpace { + &:hover { + cursor: pointer; + color: var(--main-color); + } + } + + &#KeySpace, + &#KeySpace2 { + width: 100%; + } + + &#KeySpace2 { + opacity: 0; + } + + &.flash { + animation: flashKey 1s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } + } + + .hidden-key, + .hide-key { + opacity: 0; + } + + .keymap-split-spacer, + .keymap-stagger-split-spacer, + .keymap-matrix-split-spacer { + display: none; + } + + .r1 { + display: grid; + grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + } + + .r2 { + display: grid; + grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1rem; + } + + .r3 { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + } + + .r4 { + display: grid; + grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2.75fr; + } + + .r5 { + display: grid; + grid-template-columns: 3.5fr 6fr 3.5fr; + font-size: 0.5rem; + // &.matrixSpace { + // // grid-template-columns: 6.75fr 1.9fr 6.75fr; + // grid-template-columns: 6.9fr 4.6fr 6.9fr; // wider spacebar + // } + // &.splitSpace { + // // grid-template-columns: 6.75fr 1.9fr 6.75fr; + // grid-template-columns: 4fr 7.5fr 4fr; + // } + } + &.matrix { + .r1, + .r2, + .r3 { + grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + } + + .r4 { + grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + } + + .r5 { + grid-template-columns: 3.25fr 5fr 2fr 1fr; + } + + .r1, + .r2, + .r3 { + :nth-child(13) { + opacity: 0; + } + + :nth-child(14) { + opacity: 0; + } + } + } + &.split { + .keymap-split-spacer { + display: block; + } + .keymap-stagger-split-spacer { + display: block; + } + + .r1 { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; + } + + .r2 { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + } + + .r3 { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; + } + + .r4 { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr; + } + .r5 { + grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr; + } + #KeySpace2 { + opacity: 1; + } + } + &.split_matrix { + .keymap-split-spacer { + display: block; + width: 2rem; + height: 2rem; + } + .keymap-stagger-split-spacer { + display: none; + } + .keymap-matrix-split-spacer { + display: block; + width: 2rem; + height: 2rem; + } + .r1, + .r2, + .r3 { + grid-template-columns: 1.125fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + } + + .r4 { + grid-template-columns: 0fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + } + + .r5 { + grid-template-columns: 3.225fr 3fr 1fr 3fr 2fr; + } + #KeySpace2 { + opacity: 1; + } + + .r1 { + :nth-child(12) { + opacity: 0; + } + } + + .r1, + .r2, + .r3 { + :nth-child(13) { + opacity: 0; + } + + :nth-child(14) { + opacity: 0; + } + } + } + &.alice { + .keymap-split-spacer { + display: block; + } + .r4 .keymap-split-spacer { + display: none; + } + .keymap-stagger-split-spacer { + display: block; + } + + .r1 { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; + .keymap-key:nth-child(2) { + //1 + margin-left: 45%; + } + .keymap-key:nth-child(3) { + //2 + margin-top: -2px; + margin-left: 45%; + } + .keymap-key:nth-child(4), + .keymap-key:nth-child(5), + .keymap-key:nth-child(6), + .keymap-key:nth-child(7) { + //3456 + transform: rotate(10deg); + margin-left: 45%; + } + .keymap-key:nth-child(4) { + //3 + margin-top: 3px; + } + .keymap-key:nth-child(5) { + //4 + margin-top: 10px; + } + .keymap-key:nth-child(6) { + //5 + margin-top: 17px; + } + .keymap-key:nth-child(7) { + //6 + margin-top: 24px; + } + .keymap-key:nth-child(9), + .keymap-key:nth-child(10), + .keymap-key:nth-child(11), + .keymap-key:nth-child(12) { + //7890 + transform: rotate(-10deg); + margin-left: -48%; + } + .keymap-key:nth-child(12) { + //7 + margin-top: -1px; + } + .keymap-key:nth-child(11) { + //8 + margin-top: 6px; + } + .keymap-key:nth-child(10) { + //9 + margin-top: 13px; + } + .keymap-key:nth-child(9) { + //10 + margin-top: 20px; + } + .keymap-key:nth-child(13), + .keymap-key:nth-child(14) { + //-= + margin-left: -40%; + } + } + + .r2 { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + .keymap-key:nth-child(2) { + //Q + margin-left: 20%; + } + .keymap-key:nth-child(3), + .keymap-key:nth-child(4), + .keymap-key:nth-child(5), + .keymap-key:nth-child(6) { + //WERT + transform: rotate(10deg); + margin-left: 45%; + } + .keymap-key:nth-child(4), + .keymap-key:nth-child(10) { + //EI + margin-top: 8px; + } + .keymap-key:nth-child(5), + .keymap-key:nth-child(9) { + //RU + margin-top: 15px; + } + .keymap-key:nth-child(6), + .keymap-key:nth-child(8) { + //TY + margin-top: 22px; + } + + .keymap-key:nth-child(8), + .keymap-key:nth-child(9), + .keymap-key:nth-child(10), + .keymap-key:nth-child(11) { + //YUIO + transform: rotate(-10deg); + margin-left: -12%; + } + } + + .r3 { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1.5fr; + .keymap-key:nth-child(2) { + //A + margin-left: -5px; + } + .keymap-key:nth-child(3), + .keymap-key:nth-child(4), + .keymap-key:nth-child(5), + .keymap-key:nth-child(6) { + //SDFG + margin-left: -1px; + transform: rotate(10deg); + } + .keymap-key:nth-child(4), + .keymap-key:nth-child(10) { + //DK + margin-top: 8px; + } + .keymap-key:nth-child(5), + .keymap-key:nth-child(9) { + //FJ + margin-top: 15px; + } + .keymap-key:nth-child(6), + .keymap-key:nth-child(8) { + //GH + margin-top: 22px; + } + + .keymap-key:nth-child(8), + .keymap-key:nth-child(9), + .keymap-key:nth-child(10), + .keymap-key:nth-child(11) { + //HJKL + transform: rotate(-10deg); + margin-left: -25%; + } + } + + .r4 { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr; + .keymap-key:nth-child(2) { + margin-left: -18px; + } + .keymap-key:nth-child(3) { + //Z + margin-left: -15px; + } + .keymap-key:nth-child(4), + .keymap-key:nth-child(5), + .keymap-key:nth-child(6), + .keymap-key:nth-child(7) { + //XCVB + margin-left: -11px; + transform: rotate(10deg); + margin-top: 2px; + } + .keymap-key:nth-child(12) { + //, + margin-top: 4px; + margin-left: -5px; + } + .keymap-key:nth-child(5), + .keymap-key:nth-child(11) { + //CM + margin-top: 10px; + } + .keymap-key:nth-child(6), + .keymap-key:nth-child(10) { + //VN + margin-top: 18px; + } + .keymap-key:nth-child(7) { + //B + margin-top: 24px; + } + + .keymap-key:nth-child(10), + .keymap-key:nth-child(11), + .keymap-key:nth-child(12) { + //NM, + transform: rotate(-10deg); + margin-left: -25%; + } + } + .r5 { + grid-template-columns: 5fr 3fr 1fr 3fr 4.5fr; + } + #KeySpace2 { + opacity: 1; + } + + // div#KeyE.keymap-key, + // div#KeyD.keymap-key { + // margin-top: 6px; + // } + // div#KeyC.keymap-key { + // margin-top: 8px; + // } + // div#KeyR.keymap-key, + // div#KeyF.keymap-key { + // margin-top: 12px; + // } + // div#KeyV.keymap-key { + // margin-top: 14px; + // } + // div#KeyT.keymap-key, + // div#KeyG.keymap-key { + // margin-top: 18px; + // } + // div#KeyB.keymap-key { + // margin-top: 20px; + // } + // div#KeyY.keymap-key, + // div#KeyU.keymap-key, + // div#KeyI.keymap-key, + // div#KeyO.keymap-key { + // transform: rotate(-10deg); + // margin-left: -25%; + // } + // div#KeyH.keymap-key, + // div#KeyJ.keymap-key, + // div#KeyK.keymap-key, + // div#KeyL.keymap-key { + // transform: rotate(-10deg); + // margin-left: -35%; + // } + // div#KeyN.keymap-key, + // div#KeyM.keymap-key, + // div#KeyComma.keymap-key { + // transform: rotate(-10deg); + // margin-left: -16%; + // } + // div#KeyP.keymap-key, + // div#KeyLeftBracket.keymap-key, + // div#KeyRightBracket.keymap-key { + // margin-left: 5%; + // } + // div#KeySemicolon.keymap-key, + // div#KeyQuote.keymap-key { + // margin-left: -25%; + // } + // div#KeyPeriod.keymap-key, + // div#KeySlash.keymap-key { + // margin-left: -3px; + // } + // div#KeyO.keymap-key, + // div#KeyComma.keymap-key { + // margin-top: 3px; + // } + // div#KeyL.keymap-key { + // margin-top: 1px; + // } + // div#KeyI.keymap-key, + // div#KeyM.keymap-key { + // margin-top: 9px; + // } + // div#KeyK.keymap-key { + // margin-top: 7px; + // } + // div#KeyU.keymap-key, + // div#KeyN.keymap-key { + // margin-top: 15px; + // } + // div#KeyJ.keymap-key { + // margin-top: 13px; + // } + // div#KeyY.keymap-key { + // margin-top: 21px; + // } + // div#KeyH.keymap-key { + // margin-top: 19px; + // } + div#KeySpace.keymap-key { + transform: rotate(10deg); + margin-left: -5%; + margin-top: 21%; + } + div#KeySpace2.keymap-key { + transform: rotate(-10deg); + margin-left: -33%; + margin-top: 20%; + } + div#KeyBackslash.keymap-key { + visibility: hidden; + } + + div.extraKey { + margin-top: 25px; + transform: rotate(-10deg) !important; + margin-left: -7px !important; + display: flex; + background-color: transparent; + color: var(--sub-color); + border-radius: var(--roundness); + border: 0.05rem solid; + border-color: var(--sub-color); + text-align: center; + justify-content: center; + align-items: center; + width: 2rem; + height: 2rem; + position: relative; + } + // div#KeySpace.keymap-key:after { + // content: 'Alice'; + // text-indent: 0; + // font-weight: 600!important; + // margin: auto; + // font-size: 0.9rem; + // color: var(--bg-color) + // } + } +} + +==> monkeytype/src/sass/nav.scss <== +#menu { + font-size: 1rem; + line-height: 1rem; + color: var(--sub-color); + display: grid; + grid-auto-flow: column; + gap: 0.5rem; + // margin-bottom: -0.4rem; + width: fit-content; + width: -moz-fit-content; + + .icon-button { + // .icon { + // display: grid; + // align-items: center; + // justify-items: center; + // text-align: center; + // width: 1.25rem; + // height: 1.25rem; + // } + text-decoration: none; + + .text { + font-size: 0.65rem; + line-height: 0.65rem; + align-self: center; + margin-left: 0.25rem; + } + + // &:hover { + // cursor: pointer; + // color: var(--main-color); + // } + } + + .separator { + width: 2px; + height: 1rem; + background-color: var(--sub-color); + } +} + +#top.focus #menu .icon-button.discord::after { + background: transparent; +} + +#top.focus #menu { + color: transparent !important; +} + +#top.focus #menu .icon-button { + color: transparent !important; +} + +#top { + grid-template-areas: "logo menu config"; + line-height: 2.3rem; + font-size: 2.3rem; + /* text-align: center; */ + // transition: 0.25s; + padding: 0 5px; + display: grid; + grid-auto-flow: column; + grid-template-columns: auto 1fr auto; + z-index: 2; + align-items: center; + gap: 0.5rem; + user-select: none; + + .logo { + // margin-bottom: 0.6rem; + cursor: pointer; + display: grid; + grid-template-columns: auto 1fr; + gap: 0.5rem; + + .icon { + width: 2.5rem; + display: grid; + align-items: center; + background-color: transparent; + // margin-bottom: 0.15rem; + svg path { + transition: 0.25s; + fill: var(--main-color); + } + } + .text { + .top { + position: absolute; + left: 0.25rem; + top: -0.1rem; + font-size: 0.65rem; + line-height: 0.65rem; + color: var(--sub-color); + transition: 0.25s; + } + position: relative; + font-size: 2rem; + margin-bottom: 0.4rem; + font-family: "Lexend Deca"; + transition: 0.25s; + } + white-space: nowrap; + user-select: none; + + .bottom { + margin-left: -0.15rem; + color: var(--main-color); + transition: 0.25s; + cursor: pointer; + } + } + + .config { + grid-area: config; + transition: 0.125s; + .mobileConfig { + display: none; + .icon-button { + display: grid; + grid-auto-flow: column; + align-content: center; + transition: 0.25s; + margin-right: -1rem; + padding: 0.5rem 1rem; + font-size: 2rem; + border-radius: var(--roundness); + cursor: pointer; + color: var(--sub-color); + &:hover { + color: var(--text-color); + } + } + } + + .desktopConfig { + justify-self: right; + display: grid; + // grid-auto-flow: row; + grid-template-rows: 0.7rem 0.7rem 0.7rem; + grid-gap: 0.2rem; + // width: min-content; + // width: -moz-min-content; + // transition: 0.25s; + /* margin-bottom: 0.1rem; */ + justify-items: self-end; + + .group { + // transition: 0.25s; + + .title { + color: var(--sub-color); + font-size: 0.5rem; + line-height: 0.5rem; + margin-bottom: 0.15rem; + } + + .buttons { + font-size: 0.7rem; + line-height: 0.7rem; + display: flex; + } + &.disabled { + pointer-events: none; + opacity: 0.25; + } + } + + .punctuationMode { + margin-bottom: -0.1rem; + } + + .numbersMode { + margin-bottom: -0.1rem; + } + } + } + + .result { + display: grid; + grid-auto-flow: column; + grid-gap: 1rem; + width: min-content; + width: -moz-min-content; + transition: 0.25s; + grid-column: 3/4; + grid-row: 1/2; + + .group { + .title { + font-size: 0.65rem; + line-height: 0.65rem; + color: var(--sub-color); + } + + .val { + font-size: 1.7rem; + line-height: 1.7rem; + color: var(--main-color); + transition: 0.25s; + } + } + } + + //top focus + &.focus { + color: var(--sub-color) !important; + + .result { + opacity: 0 !important; + } + + .icon svg path { + fill: var(--sub-color) !important; + } + + .logo .text { + color: var(--sub-color) !important; + // opacity: 0 !important; + } + + .logo .top { + opacity: 0 !important; + } + + .config { + opacity: 0 !important; + } + } +} + +==> monkeytype/src/sass/scroll.scss <== +/* width */ +::-webkit-scrollbar { + width: 7px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: transparent; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: var(--sub-color); + transition: 0.25s; + border-radius: 2px !important; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: var(--main-color); +} + +::-webkit-scrollbar-corner { + background: var(--sub-color); +} + +==> monkeytype/src/sass/settings.scss <== +.pageSettings { + display: grid; + // grid-template-columns: 1fr 1fr; + gap: 2rem; + + .tip { + color: var(--sub-color); + } + + .sectionGroupTitle { + font-size: 2rem; + color: var(--sub-color); + line-height: 2rem; + cursor: pointer; + transition: 0.25s; + + &:hover { + color: var(--text-color); + } + + .fas { + margin-left: 0.5rem; + + &.rotate { + transform: rotate(-90deg); + } + } + } + + .sectionSpacer { + height: 1.5rem; + } + + .settingsGroup { + display: grid; + gap: 2rem; + &.quickNav .links { + display: grid; + grid-auto-flow: column; + text-align: center; + a { + text-decoration: none; + width: 100%; + cursor: pointer; + // opacity: 0.5; + &:hover { + opacity: 1; + } + } + } + } + + .section { + display: grid; + // gap: .5rem; + grid-template-areas: + "title title" + "text buttons"; + grid-template-columns: 2fr 1fr; + column-gap: 2rem; + align-items: center; + + .button.danger { + box-shadow: 0px 0px 0px 2px var(--error-color); + color: var(--text-color); + &:hover { + background: var(--text-color); + color: var(--bg-color); + } + } + + .inputAndButton { + display: grid; + grid-template-columns: 8fr 1fr; + gap: 0.5rem; + margin-bottom: 0.5rem; + + .button { + height: auto; + + .fas { + margin-right: 0rem; + vertical-align: sub; + } + } + } + + &.themes .tabContainer [tabcontent="custom"] { + label.button:first-child { + color: var(--text-color); + } + label.button { + color: var(--bg-color); + } + } + + &.customBackgroundFilter { + .groups { + grid-area: buttons; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-top: 2rem; + .group { + display: grid; + grid-template-columns: 1fr auto 2fr; + gap: 1rem; + .title, + .value { + color: var(--text-color); + } + } + } + .saveContainer { + grid-column: -1/-3; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; + } + .fas { + margin-right: 0rem; + } + } + + &.customTheme { + grid-template-columns: 1fr 1fr 1fr 1fr; + justify-items: stretch; + gap: 0.5rem 2rem; + + & p { + grid-area: unset; + grid-column: 1 / span 4; + } + + & .spacer { + grid-column: 3 / 5; + } + } + + h1 { + font-size: 1rem; + line-height: 1rem; + color: var(--sub-color); + margin: 0; + grid-area: title; + font-weight: 300; + } + + p { + grid-area: text; + color: var(--sub-color); + margin: 0; + } + + & > .text { + align-self: normal; + color: var(--text-color); + grid-area: text; + } + + .buttons { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + gap: 0.5rem; + grid-area: buttons; + &.vertical { + grid-auto-flow: unset; + } + } + + &.discordIntegration { + .info { + grid-area: buttons; + text-align: center; + color: var(--main-color); + } + + #unlinkDiscordButton { + margin-top: 0.5rem; + font-size: 0.75rem; + color: var(--sub-color); + &:hover { + color: var(--text-color); + } + } + + .howto { + margin-top: 1rem; + color: var(--text-color); + } + } + + &.tags { + .tagsListAndButton { + grid-area: buttons; + } + + .tag { + grid-template-columns: 6fr 1fr 1fr 1fr; + margin-bottom: 0.5rem; + } + + .addTagButton { + margin-top: 0.5rem; + color: var(--text-color); + cursor: pointer; + transition: 0.25s; + padding: 0.2rem 0.5rem; + border-radius: var(--roundness); + + background: rgba(0, 0, 0, 0.1); + text-align: center; + -webkit-user-select: none; + display: grid; + align-content: center; + height: min-content; + height: -moz-min-content; + + &.active { + background: var(--main-color); + color: var(--bg-color); + } + + &:hover, + &:focus { + color: var(--bg-color); + background: var(--text-color); + outline: none; + } + } + } + + &.presets { + .presetsListAndButton { + grid-area: buttons; + } + + .preset { + grid-template-columns: 7fr 1fr 1fr; + margin-bottom: 0.5rem; + } + + .addPresetButton { + margin-top: 0.5rem; + color: var(--text-color); + cursor: pointer; + transition: 0.25s; + padding: 0.2rem 0.5rem; + border-radius: var(--roundness); + + background: rgba(0, 0, 0, 0.1); + text-align: center; + -webkit-user-select: none; + display: grid; + align-content: center; + height: min-content; + height: -moz-min-content; + + &.active { + background: var(--main-color); + color: var(--bg-color); + } + + &:hover, + &:focus { + color: var(--bg-color); + background: var(--text-color); + outline: none; + } + } + } + + &.fontSize .buttons { + grid-template-columns: 1fr 1fr 1fr 1fr; + } + + &.themes { + .tabContainer { + position: relative; + grid-area: buttons; + + .tabContent { + overflow: revert; + height: auto; + + &.customTheme { + margin-top: 0.5rem; + .colorText { + color: var(--text-color); + } + } + + .text { + align-self: center; + } + } + } + + .theme.button { + display: grid; + grid-template-columns: auto 1fr auto; + .text { + color: inherit; + } + .activeIndicator { + overflow: hidden; + width: 1.25rem; + transition: 0.25s; + opacity: 0; + color: inherit; + .far { + margin: 0; + } + &.active { + width: 1.25rem; + opacity: 1; + } + } + .favButton { + overflow: hidden; + width: 1.25rem; + transition: 0.25s; + opacity: 0; + .far, + .fas { + margin: 0; + pointer-events: none; + } + &:hover { + cursor: pointer; + } + &.active { + width: 1.25rem; + opacity: 1; + } + } + &:hover { + .favButton { + width: 1.25rem; + opacity: 1; + } + } + &.active { + .activeIndicator { + opacity: 1; + } + } + } + } + + &.themes { + grid-template-columns: 2fr 1fr; + grid-template-areas: + "title tabs" + "text text" + "buttons buttons"; + column-gap: 2rem; + // row-gap: 0.5rem; + + .tabs { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + gap: 0.5rem; + grid-area: tabs; + } + + .buttons { + margin-left: 0; + grid-auto-flow: dense; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 0.5rem; + margin-top: 0.5rem; + } + } + + &.fullWidth { + grid-template-columns: 2fr 1fr; + grid-template-areas: + "title tabs" + "text text" + "buttons buttons"; + column-gap: 2rem; + // row-gap: 0.5rem; + + .buttons { + margin-left: 0; + grid-auto-flow: dense; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 0.5rem; + margin-top: 1rem; + } + } + + &.randomTheme .buttons { + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; + } + } +} + +.buttons div.theme:hover { + transform: scale(1.1); +} + +==> monkeytype/src/sass/animations.scss <== +@keyframes loader { + 0% { + width: 0; + left: 0; + } + + 50% { + width: 100%; + left: 0; + } + + 100% { + width: 0; + left: 100%; + } +} + +@keyframes caretFlashSmooth { + 0%, + 100% { + opacity: 0; + } + + 50% { + opacity: 1; + } +} + +@keyframes caretFlashHard { + 0%, + 50% { + opacity: 1; + } + + 51%, + 100% { + opacity: 0; + } +} + +@keyframes flashKey { + from { + color: var(--bg-color); + background-color: var(--main-color); + border-color: var(--main-color); + } + + to { + color: var(--sub-color); + background-color: var(--bg-color); + border-color: var(--sub-color); + } +} + +@keyframes shake { + 0% { + transform: translate(4px, 0) rotate(0deg); + } + 50% { + transform: translate(-4px, 0) rotate(0deg); + } + 100% { + transform: translate(4px, 0) rotate(0deg); + } +} + +@keyframes flashHighlight { + 0% { + background-color: var(--bg-color); + } + 10% { + background-color: var(--main-color); + } + 40% { + background-color: var(--main-color); + } + 100% { + background-color: var(--bg-color); + } +} + +==> monkeytype/src/sass/footer.scss <== +#bottom { + position: relative; + text-align: center; + line-height: 1rem; + font-size: 0.75rem; + color: var(--sub-color); + // transition: 0.25s; + padding: 0 5px; + + // margin-bottom: 2rem; + .keyTips { + transition: 0.25s; + margin-bottom: 2rem; + } + + #supportMeButton { + transition: 0.25s; + &:hover { + color: var(--text-color); + cursor: pointer; + } + } + + #commandLineMobileButton { + display: none; + top: -4rem; + left: 0; + position: absolute; + font-size: 1rem; + width: 3rem; + height: 3rem; + text-align: center; + line-height: 3rem; + background: var(--main-color); + border-radius: 99rem; + z-index: 99; + cursor: pointer; + color: var(--bg-color); + transition: 0.25s; + } + + .leftright { + display: grid; + grid-template-columns: auto auto; + gap: 1rem; + a { + text-decoration: none; + } + .left { + text-align: left; + display: grid; + grid-auto-flow: column; + width: fit-content; + gap: 1rem; + width: -moz-fit-content; + } + .right { + text-align: right; + display: grid; + grid-auto-flow: column; + width: fit-content; + width: -moz-fit-content; + justify-self: right; + gap: 1rem; + // align-items: center; + } + .left a, + .right a { + display: grid; + grid-auto-flow: column; + gap: 0.25rem; + align-items: baseline; + width: max-content; + width: -moz-available; + &:hover { + color: var(--text-color); + cursor: pointer; + } + } + } + + .version { + opacity: 0; + } +} + +#bottom.focus { + .keyTips { + opacity: 0 !important; + } + a { + opacity: 0 !important; + } + #commandLineMobileButton { + opacity: 0 !important; + pointer-events: none !important; + } +} + +==> ./monkeytype/src/js/theme-colors.js <== +// export let bg = "#323437"; +// export let main = "#e2b714"; +// export let caret = "#e2b714"; +// export let sub = "#646669"; +// export let text = "#d1d0c5"; +// export let error = "#ca4754"; +// export let errorExtra = "#7e2a33"; +// export let colorfulError = "#ca4754"; +// export let colorfulErrorExtra = "#7e2a33"; + +let colors = { + bg: "#323437", + main: "#e2b714", + caret: "#e2b714", + sub: "#646669", + text: "#d1d0c5", + error: "#ca4754", + errorExtra: "#7e2a33", + colorfulError: "#ca4754", + colorfulErrorExtra: "#7e2a33", +}; + +export async function get(color) { + let ret; + + if (color === undefined) { + ret = colors; + } else { + ret = colors[color]; + } + + return ret; + + // return check(); + + // async function run() { + // return new Promise(function (resolve, reject) { + // window.setTimeout(() => { + // update(); + // if (color === undefined) { + // ret = colors; + // } else { + // ret = colors[color]; + // } + // resolve(check()); + // }, 250); + // }); + // } + // async function check() { + // if (color === undefined) { + // if (ret.bg === "") { + // return await run(); + // } else { + // return ret; + // } + // } else { + // if (ret === "") { + // return await run(); + // } else { + // return ret; + // } + // } + // } +} + +export function reset() { + colors = { + bg: "", + main: "", + caret: "", + sub: "", + text: "", + error: "", + errorExtra: "", + colorfulError: "", + colorfulErrorExtra: "", + }; +} + +export function update() { + let st = getComputedStyle(document.body); + colors.bg = st.getPropertyValue("--bg-color").replace(" ", ""); + colors.main = st.getPropertyValue("--main-color").replace(" ", ""); + colors.caret = st.getPropertyValue("--caret-color").replace(" ", ""); + colors.sub = st.getPropertyValue("--sub-color").replace(" ", ""); + colors.text = st.getPropertyValue("--text-color").replace(" ", ""); + colors.error = st.getPropertyValue("--error-color").replace(" ", ""); + colors.errorExtra = st + .getPropertyValue("--error-extra-color") + .replace(" ", ""); + colors.colorfulError = st + .getPropertyValue("--colorful-error-color") + .replace(" ", ""); + colors.colorfulErrorExtra = st + .getPropertyValue("--colorful-error-extra-color") + .replace(" ", ""); +} + +==> ./monkeytype/src/js/simple-popups.js <== +import * as Loader from "./loader"; +import * as Notifications from "./notifications"; +import * as AccountController from "./account-controller"; +import * as DB from "./db"; +import * as Settings from "./settings"; +import axiosInstance from "./axios-instance"; +import * as UpdateConfig from "./config"; + +export let list = {}; +class SimplePopup { + constructor( + id, + type, + title, + inputs = [], + text = "", + buttonText = "Confirm", + execFn, + beforeShowFn + ) { + this.parameters = []; + this.id = id; + this.type = type; + this.execFn = execFn; + this.title = title; + this.inputs = inputs; + this.text = text; + this.wrapper = $("#simplePopupWrapper"); + this.element = $("#simplePopup"); + this.buttonText = buttonText; + this.beforeShowFn = beforeShowFn; + } + reset() { + this.element.html(` +
+
+
+
`); + } + + init() { + let el = this.element; + el.find("input").val(""); + // if (el.attr("popupId") !== this.id) { + this.reset(); + el.attr("popupId", this.id); + el.find(".title").text(this.title); + el.find(".text").text(this.text); + + this.initInputs(); + + if (!this.buttonText) { + el.find(".button").remove(); + } else { + el.find(".button").text(this.buttonText); + } + + // } + } + + initInputs() { + let el = this.element; + if (this.inputs.length > 0) { + if (this.type === "number") { + this.inputs.forEach((input) => { + el.find(".inputs").append(` + + `); + }); + } else if (this.type === "text") { + this.inputs.forEach((input) => { + if (input.type) { + el.find(".inputs").append(` + + `); + } else { + el.find(".inputs").append(` + + `); + } + }); + } + el.find(".inputs").removeClass("hidden"); + } else { + el.find(".inputs").addClass("hidden"); + } + } + + exec() { + let vals = []; + $.each($("#simplePopup input"), (index, el) => { + vals.push($(el).val()); + }); + this.execFn(...vals); + this.hide(); + } + + show(parameters) { + this.parameters = parameters; + this.beforeShowFn(); + this.init(); + this.wrapper + .stop(true, true) + .css("opacity", 0) + .removeClass("hidden") + .animate({ opacity: 1 }, 125, () => { + $($("#simplePopup").find("input")[0]).focus(); + }); + } + + hide() { + this.wrapper + .stop(true, true) + .css("opacity", 1) + .removeClass("hidden") + .animate({ opacity: 0 }, 125, () => { + this.wrapper.addClass("hidden"); + }); + } +} + +export function hide() { + $("#simplePopupWrapper") + .stop(true, true) + .css("opacity", 1) + .removeClass("hidden") + .animate({ opacity: 0 }, 125, () => { + $("#simplePopupWrapper").addClass("hidden"); + }); +} + +$("#simplePopupWrapper").mousedown((e) => { + if ($(e.target).attr("id") === "simplePopupWrapper") { + $("#simplePopupWrapper") + .stop(true, true) + .css("opacity", 1) + .removeClass("hidden") + .animate({ opacity: 0 }, 125, () => { + $("#simplePopupWrapper").addClass("hidden"); + }); + } +}); + +$(document).on("click", "#simplePopupWrapper .button", (e) => { + let id = $("#simplePopup").attr("popupId"); + list[id].exec(); +}); + +$(document).on("keyup", "#simplePopupWrapper input", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + let id = $("#simplePopup").attr("popupId"); + list[id].exec(); + } +}); + +list.updateEmail = new SimplePopup( + "updateEmail", + "text", + "Update Email", + [ + { + placeholder: "Password", + type: "password", + initVal: "", + }, + { + placeholder: "New email", + initVal: "", + }, + { + placeholder: "Confirm new email", + initVal: "", + }, + ], + "", + "Update", + async (password, email, emailConfirm) => { + try { + const user = firebase.auth().currentUser; + if (email !== emailConfirm) { + Notifications.add("Emails don't match", 0); + return; + } + if (user.providerData[0].providerId === "password") { + const credential = firebase.auth.EmailAuthProvider.credential( + user.email, + password + ); + await user.reauthenticateWithCredential(credential); + } + Loader.show(); + let response; + try { + response = await axiosInstance.post("/user/updateEmail", { + uid: user.uid, + previousEmail: user.email, + newEmail: email, + }); + } catch (e) { + Loader.hide(); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to update email: " + msg, -1); + return; + } + Loader.hide(); + if (response.status !== 200) { + Notifications.add(response.data.message); + return; + } else { + Notifications.add("Email updated", 1); + } + } catch (e) { + if (e.code == "auth/wrong-password") { + Notifications.add("Incorrect password", -1); + } else { + Notifications.add("Something went wrong: " + e, -1); + } + } + }, + () => { + const user = firebase.auth().currentUser; + if (!user.providerData.find((p) => p.providerId === "password")) { + eval(`this.inputs = []`); + eval(`this.buttonText = undefined`); + eval(`this.text = "Password authentication is not enabled";`); + } + } +); + +list.updateName = new SimplePopup( + "updateName", + "text", + "Update Name", + [ + { + placeholder: "Password", + type: "password", + initVal: "", + }, + { + placeholder: "New name", + type: "text", + initVal: "", + }, + ], + "", + "Update", + async (pass, newName) => { + try { + const user = firebase.auth().currentUser; + if (user.providerData[0].providerId === "password") { + const credential = firebase.auth.EmailAuthProvider.credential( + user.email, + pass + ); + await user.reauthenticateWithCredential(credential); + } else if (user.providerData[0].providerId === "google.com") { + await user.reauthenticateWithPopup(AccountController.gmailProvider); + } + Loader.show(); + + let response; + try { + response = await axiosInstance.post("/user/checkName", { + name: newName, + }); + } catch (e) { + Loader.hide(); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to check name: " + msg, -1); + return; + } + Loader.hide(); + if (response.status !== 200) { + Notifications.add(response.data.message); + return; + } + try { + response = await axiosInstance.post("/user/updateName", { + name: newName, + }); + } catch (e) { + Loader.hide(); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to update name: " + msg, -1); + return; + } + Loader.hide(); + if (response.status !== 200) { + Notifications.add(response.data.message); + return; + } else { + Notifications.add("Name updated", 1); + DB.getSnapshot().name = newName; + $("#menu .icon-button.account .text").text(newName); + } + } catch (e) { + Loader.hide(); + if (e.code == "auth/wrong-password") { + Notifications.add("Incorrect password", -1); + } else { + Notifications.add("Something went wrong: " + e, -1); + } + } + }, + () => { + const user = firebase.auth().currentUser; + if (user.providerData[0].providerId === "google.com") { + eval(`this.inputs[0].hidden = true`); + eval(`this.buttonText = "Reauthenticate to update"`); + } + } +); + +list.updatePassword = new SimplePopup( + "updatePassword", + "text", + "Update Password", + [ + { + placeholder: "Password", + type: "password", + initVal: "", + }, + { + placeholder: "New password", + type: "password", + initVal: "", + }, + { + placeholder: "Confirm new password", + type: "password", + initVal: "", + }, + ], + "", + "Update", + async (previousPass, newPass, newPassConfirm) => { + try { + const user = firebase.auth().currentUser; + const credential = firebase.auth.EmailAuthProvider.credential( + user.email, + previousPass + ); + if (newPass !== newPassConfirm) { + Notifications.add("New passwords don't match", 0); + return; + } + Loader.show(); + await user.reauthenticateWithCredential(credential); + await user.updatePassword(newPass); + Loader.hide(); + Notifications.add("Password updated", 1); + } catch (e) { + Loader.hide(); + if (e.code == "auth/wrong-password") { + Notifications.add("Incorrect password", -1); + } else { + Notifications.add("Something went wrong: " + e, -1); + } + } + }, + () => { + const user = firebase.auth().currentUser; + if (!user.providerData.find((p) => p.providerId === "password")) { + eval(`this.inputs = []`); + eval(`this.buttonText = undefined`); + eval(`this.text = "Password authentication is not enabled";`); + } + } +); + +list.addPasswordAuth = new SimplePopup( + "addPasswordAuth", + "text", + "Add Password Authentication", + [ + { + placeholder: "email", + type: "email", + initVal: "", + }, + { + placeholder: "confirm email", + type: "email", + initVal: "", + }, + { + placeholder: "new password", + type: "password", + initVal: "", + }, + { + placeholder: "confirm new password", + type: "password", + initVal: "", + }, + ], + "", + "Add", + async (email, emailConfirm, pass, passConfirm) => { + if (email !== emailConfirm) { + Notifications.add("Emails don't match", 0); + return; + } + + if (pass !== passConfirm) { + Notifications.add("Passwords don't match", 0); + return; + } + + AccountController.addPasswordAuth(email, pass); + }, + () => {} +); + +list.deleteAccount = new SimplePopup( + "deleteAccount", + "text", + "Delete Account", + [ + { + placeholder: "Password", + type: "password", + initVal: "", + }, + ], + "This is the last time you can change your mind. After pressing the button everything is gone.", + "Delete", + async (password) => { + // + try { + const user = firebase.auth().currentUser; + if (user.providerData[0].providerId === "password") { + const credential = firebase.auth.EmailAuthProvider.credential( + user.email, + password + ); + await user.reauthenticateWithCredential(credential); + } else if (user.providerData[0].providerId === "google.com") { + await user.reauthenticateWithPopup(AccountController.gmailProvider); + } + Loader.show(); + + Notifications.add("Deleting stats...", 0); + let response; + try { + response = await axiosInstance.post("/user/delete"); + } catch (e) { + Loader.hide(); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to delete user stats: " + msg, -1); + return; + } + if (response.status !== 200) { + throw response.data.message; + } + + Notifications.add("Deleting results...", 0); + try { + response = await axiosInstance.post("/results/deleteAll"); + } catch (e) { + Loader.hide(); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to delete user results: " + msg, -1); + return; + } + if (response.status !== 200) { + throw response.data.message; + } + + Notifications.add("Deleting login information...", 0); + await firebase.auth().currentUser.delete(); + + Notifications.add("Goodbye", 1, 5); + + setTimeout(() => { + location.reload(); + }, 3000); + } catch (e) { + Loader.hide(); + if (e.code == "auth/wrong-password") { + Notifications.add("Incorrect password", -1); + } else { + Notifications.add("Something went wrong: " + e, -1); + } + } + }, + () => { + const user = firebase.auth().currentUser; + if (user.providerData[0].providerId === "google.com") { + eval(`this.inputs = []`); + eval(`this.buttonText = "Reauthenticate to delete"`); + } + } +); + +list.clearTagPb = new SimplePopup( + "clearTagPb", + "text", + "Clear Tag PB", + [], + `Are you sure you want to clear this tags PB?`, + "Clear", + () => { + let tagid = eval("this.parameters[0]"); + Loader.show(); + axiosInstance + .post("/user/tags/clearPb", { + tagid: tagid, + }) + .then((res) => { + Loader.hide(); + if (res.data.resultCode === 1) { + let tag = DB.getSnapshot().tags.filter((t) => t.id === tagid)[0]; + tag.pb = 0; + $( + `.pageSettings .section.tags .tagsList .tag[id="${tagid}"] .clearPbButton` + ).attr("aria-label", "No PB found"); + Notifications.add("Tag PB cleared.", 0); + } else { + Notifications.add("Something went wrong: " + res.data.message, -1); + } + }) + .catch((e) => { + Loader.hide(); + if (e.code == "auth/wrong-password") { + Notifications.add("Incorrect password", -1); + } else { + Notifications.add("Something went wrong: " + e, -1); + } + }); + // console.log(`clearing for ${eval("this.parameters[0]")} ${eval("this.parameters[1]")}`); + }, + () => { + eval( + "this.text = `Are you sure you want to clear PB for tag ${eval('this.parameters[1]')}?`" + ); + } +); + +list.applyCustomFont = new SimplePopup( + "applyCustomFont", + "text", + "Custom font", + [{ placeholder: "Font name", initVal: "" }], + "Make sure you have the font installed on your computer before applying.", + "Apply", + (fontName) => { + if (fontName === "") return; + Settings.groups.fontFamily?.setValue(fontName.replace(/\s/g, "_")); + }, + () => {} +); + +list.resetPersonalBests = new SimplePopup( + "resetPersonalBests", + "text", + "Reset Personal Bests", + [ + { + placeholder: "Password", + type: "password", + initVal: "", + }, + ], + "", + "Reset", + async (password) => { + try { + const user = firebase.auth().currentUser; + if (user.providerData[0].providerId === "password") { + const credential = firebase.auth.EmailAuthProvider.credential( + user.email, + password + ); + await user.reauthenticateWithCredential(credential); + } else if (user.providerData[0].providerId === "google.com") { + await user.reauthenticateWithPopup(AccountController.gmailProvider); + } + Loader.show(); + + let response; + try { + response = await axiosInstance.post("/user/clearPb"); + } catch (e) { + Loader.hide(); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to reset personal bests: " + msg, -1); + return; + } + Loader.hide(); + if (response.status !== 200) { + Notifications.add(response.data.message); + } else { + Notifications.add("Personal bests have been reset", 1); + DB.getSnapshot().personalBests = {}; + } + } catch (e) { + Loader.hide(); + Notifications.add(e, -1); + } + }, + () => { + const user = firebase.auth().currentUser; + if (user.providerData[0].providerId === "google.com") { + eval(`this.inputs = []`); + eval(`this.buttonText = "Reauthenticate to reset"`); + } + } +); + +list.resetSettings = new SimplePopup( + "resetSettings", + "text", + "Reset Settings", + [], + "Are you sure you want to reset all your settings?", + "Reset", + () => { + UpdateConfig.reset(); + // setTimeout(() => { + // location.reload(); + // }, 1000); + }, + () => {} +); + +list.unlinkDiscord = new SimplePopup( + "unlinkDiscord", + "text", + "Unlink Discord", + [], + "Are you sure you want to unlink your Discord account?", + "Unlink", + async () => { + Loader.show(); + let response; + try { + response = await axiosInstance.post("/user/discord/unlink", {}); + } catch (e) { + Loader.hide(); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to unlink Discord: " + msg, -1); + return; + } + Loader.hide(); + if (response.status !== 200) { + Notifications.add(response.data.message); + } else { + Notifications.add("Accounts unlinked", 1); + DB.getSnapshot().discordId = undefined; + Settings.updateDiscordSection(); + } + }, + () => {} +); + +==> ./monkeytype/src/js/settings/settings-group.js <== +import Config from "./config"; + +export default class SettingsGroup { + constructor( + configName, + toggleFunction, + setCallback = null, + updateCallback = null + ) { + this.configName = configName; + this.configValue = Config[configName]; + this.onOff = typeof this.configValue === "boolean"; + this.toggleFunction = toggleFunction; + this.setCallback = setCallback; + this.updateCallback = updateCallback; + + this.updateButton(); + + $(document).on( + "click", + `.pageSettings .section.${this.configName} .button`, + (e) => { + let target = $(e.currentTarget); + if (target.hasClass("disabled") || target.hasClass("no-auto-handle")) + return; + if (this.onOff) { + if (target.hasClass("on")) { + this.setValue(true); + } else { + this.setValue(false); + } + this.updateButton(); + if (this.setCallback !== null) this.setCallback(); + } else { + const value = target.attr(configName); + const params = target.attr("params"); + if (!value && !params) return; + this.setValue(value, params); + } + } + ); + } + + setValue(value, params = undefined) { + if (params === undefined) { + this.toggleFunction(value); + } else { + this.toggleFunction(value, ...params); + } + this.updateButton(); + if (this.setCallback !== null) this.setCallback(); + } + + updateButton() { + this.configValue = Config[this.configName]; + $(`.pageSettings .section.${this.configName} .button`).removeClass( + "active" + ); + if (this.onOff) { + const onOffString = this.configValue ? "on" : "off"; + $( + `.pageSettings .section.${this.configName} .buttons .button.${onOffString}` + ).addClass("active"); + } else { + $( + `.pageSettings .section.${this.configName} .button[${this.configName}='${this.configValue}']` + ).addClass("active"); + } + if (this.updateCallback !== null) this.updateCallback(); + } +} + +==> ./monkeytype/src/js/settings/language-picker.js <== +import * as Misc from "./misc"; +import Config, * as UpdateConfig from "./config"; + +export async function setActiveGroup(groupName, clicked = false) { + let currentGroup; + if (groupName === undefined) { + currentGroup = await Misc.findCurrentGroup(Config.language); + } else { + let groups = await Misc.getLanguageGroups(); + groups.forEach((g) => { + if (g.name === groupName) { + currentGroup = g; + } + }); + } + $(`.pageSettings .section.languageGroups .button`).removeClass("active"); + $( + `.pageSettings .section.languageGroups .button[group='${currentGroup.name}']` + ).addClass("active"); + + let langEl = $(".pageSettings .section.language .buttons").empty(); + currentGroup.languages.forEach((language) => { + langEl.append( + `
${language.replace( + /_/g, + " " + )}
` + ); + }); + + if (clicked) { + $($(`.pageSettings .section.language .buttons .button`)[0]).addClass( + "active" + ); + UpdateConfig.setLanguage(currentGroup.languages[0]); + } else { + $( + `.pageSettings .section.language .buttons .button[language=${Config.language}]` + ).addClass("active"); + } +} + +==> ./monkeytype/src/js/settings/theme-picker.js <== +import Config, * as UpdateConfig from "./config"; +import * as ThemeController from "./theme-controller"; +import * as Misc from "./misc"; +import * as Notifications from "./notifications"; +import * as CommandlineLists from "./commandline-lists"; +import * as ThemeColors from "./theme-colors"; +import * as ChartController from "./chart-controller"; + +export function updateActiveButton() { + let activeThemeName = Config.theme; + if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) { + activeThemeName = ThemeController.randomTheme; + } + $(`.pageSettings .section.themes .theme`).removeClass("active"); + $(`.pageSettings .section.themes .theme[theme=${activeThemeName}]`).addClass( + "active" + ); +} + +function updateColors(colorPicker, color, onlyStyle, noThemeUpdate = false) { + if (onlyStyle) { + let colorid = colorPicker.find("input[type=color]").attr("id"); + if (!noThemeUpdate) + document.documentElement.style.setProperty(colorid, color); + let pickerButton = colorPicker.find("label"); + pickerButton.val(color); + pickerButton.attr("value", color); + if (pickerButton.attr("for") !== "--bg-color") + pickerButton.css("background-color", color); + colorPicker.find("input[type=text]").val(color); + colorPicker.find("input[type=color]").attr("value", color); + return; + } + let colorREGEX = [ + { + rule: /\b[0-9]{1,3},\s?[0-9]{1,3},\s?[0-9]{1,3}\s*\b/, + start: "rgb(", + end: ")", + }, + { + rule: /\b[A-Z, a-z, 0-9]{6}\b/, + start: "#", + end: "", + }, + { + rule: /\b[0-9]{1,3},\s?[0-9]{1,3}%,\s?[0-9]{1,3}%?\s*\b/, + start: "hsl(", + end: ")", + }, + ]; + + color = color.replace("°", ""); + + for (let regex of colorREGEX) { + if (color.match(regex.rule)) { + color = regex.start + color + regex.end; + break; + } + } + + $(".colorConverter").css("color", color); + color = Misc.convertRGBtoHEX($(".colorConverter").css("color")); + if (!color) { + return; + } + + let colorid = colorPicker.find("input[type=color]").attr("id"); + + if (!noThemeUpdate) + document.documentElement.style.setProperty(colorid, color); + + let pickerButton = colorPicker.find("label"); + + pickerButton.val(color); + pickerButton.attr("value", color); + if (pickerButton.attr("for") !== "--bg-color") + pickerButton.css("background-color", color); + colorPicker.find("input[type=text]").val(color); + colorPicker.find("input[type=color]").attr("value", color); +} + +export function refreshButtons() { + let favThemesEl = $( + ".pageSettings .section.themes .favThemes.buttons" + ).empty(); + let themesEl = $(".pageSettings .section.themes .allThemes.buttons").empty(); + + let activeThemeName = Config.theme; + if (Config.randomTheme !== "off" && ThemeController.randomTheme !== null) { + activeThemeName = ThemeController.randomTheme; + } + + Misc.getSortedThemesList().then((themes) => { + //first show favourites + if (Config.favThemes.length > 0) { + favThemesEl.css({ paddingBottom: "1rem" }); + themes.forEach((theme) => { + if (Config.favThemes.includes(theme.name)) { + let activeTheme = activeThemeName === theme.name ? "active" : ""; + favThemesEl.append( + `
+
+
${theme.name.replace(/_/g, " ")}
+
` + ); + } + }); + } else { + favThemesEl.css({ paddingBottom: "0" }); + } + //then the rest + themes.forEach((theme) => { + if (!Config.favThemes.includes(theme.name)) { + let activeTheme = activeThemeName === theme.name ? "active" : ""; + themesEl.append( + `
+
+
${theme.name.replace(/_/g, " ")}
+
` + ); + } + }); + updateActiveButton(); + }); +} + +export function setCustomInputs(noThemeUpdate) { + $( + ".pageSettings .section.themes .tabContainer .customTheme .colorPicker" + ).each((n, index) => { + let currentColor = + Config.customThemeColors[ + ThemeController.colorVars.indexOf( + $(index).find("input[type=color]").attr("id") + ) + ]; + + //todo check if needed + // $(index).find("input[type=color]").val(currentColor); + // $(index).find("input[type=color]").attr("value", currentColor); + // $(index).find("input[type=text]").val(currentColor); + updateColors($(index), currentColor, false, noThemeUpdate); + }); +} + +function toggleFavourite(themename) { + if (Config.favThemes.includes(themename)) { + //already favourite, remove + UpdateConfig.setFavThemes( + Config.favThemes.filter((t) => { + if (t !== themename) { + return t; + } + }) + ); + } else { + //add to favourites + let newlist = Config.favThemes; + newlist.push(themename); + UpdateConfig.setFavThemes(newlist); + } + UpdateConfig.saveToLocalStorage(); + refreshButtons(); + // showFavouriteThemesAtTheTop(); + CommandlineLists.updateThemeCommands(); +} + +export function updateActiveTab() { + $(".pageSettings .section.themes .tabs .button").removeClass("active"); + if (!Config.customTheme) { + $(".pageSettings .section.themes .tabs .button[tab='preset']").addClass( + "active" + ); + + // UI.swapElements( + // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), + // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), + // 250 + // ); + } else { + $(".pageSettings .section.themes .tabs .button[tab='custom']").addClass( + "active" + ); + + // UI.swapElements( + // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), + // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), + // 250 + // ); + } +} + +$(".pageSettings .section.themes .tabs .button").click((e) => { + $(".pageSettings .section.themes .tabs .button").removeClass("active"); + var $target = $(e.currentTarget); + $target.addClass("active"); + setCustomInputs(); + if ($target.attr("tab") == "preset") { + UpdateConfig.setCustomTheme(false); + // ThemeController.set(Config.theme); + // applyCustomThemeColors(); + // UI.swapElements( + // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), + // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), + // 250 + // ); + } else { + UpdateConfig.setCustomTheme(true); + // ThemeController.set("custom"); + // applyCustomThemeColors(); + // UI.swapElements( + // $('.pageSettings .section.themes .tabContainer [tabContent="preset"]'), + // $('.pageSettings .section.themes .tabContainer [tabContent="custom"]'), + // 250 + // ); + } +}); + +$(document).on( + "click", + ".pageSettings .section.themes .theme .favButton", + (e) => { + let theme = $(e.currentTarget).parents(".theme.button").attr("theme"); + toggleFavourite(theme); + } +); + +$(document).on("click", ".pageSettings .section.themes .theme.button", (e) => { + let theme = $(e.currentTarget).attr("theme"); + if (!$(e.target).hasClass("favButton")) { + UpdateConfig.setTheme(theme); + // ThemePicker.refreshButtons(); + updateActiveButton(); + } +}); + +$( + ".pageSettings .section.themes .tabContainer .customTheme input[type=color]" +).on("input", (e) => { + // UpdateConfig.setCustomTheme(true, true); + let $colorVar = $(e.currentTarget).attr("id"); + let $pickedColor = $(e.currentTarget).val(); + + //todo check if needed + // document.documentElement.style.setProperty($colorVar, $pickedColor); + // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); + // $(".colorPicker #" + $colorVar).val($pickedColor); + // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); + // }); + + // $( + // ".pageSettings .section.themes .tabContainer .customTheme input[type=text]" + // ).on("input", (e) => { + // // UpdateConfig.setCustomTheme(true, true); + // let $colorVar = $(e.currentTarget).attr("id").replace("-txt", ""); + // let $pickedColor = $(e.currentTarget).val(); + + // document.documentElement.style.setProperty($colorVar, $pickedColor); + // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); + // $(".colorPicker #" + $colorVar).val($pickedColor); + // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); + updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor, true); +}); + +$( + ".pageSettings .section.themes .tabContainer .customTheme input[type=color]" +).on("change", (e) => { + // UpdateConfig.setCustomTheme(true, true); + let $colorVar = $(e.currentTarget).attr("id"); + let $pickedColor = $(e.currentTarget).val(); + + //todo check if needed + // document.documentElement.style.setProperty($colorVar, $pickedColor); + // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); + // $(".colorPicker #" + $colorVar).val($pickedColor); + // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); + // }); + + // $( + // ".pageSettings .section.themes .tabContainer .customTheme input[type=text]" + // ).on("input", (e) => { + // // UpdateConfig.setCustomTheme(true, true); + // let $colorVar = $(e.currentTarget).attr("id").replace("-txt", ""); + // let $pickedColor = $(e.currentTarget).val(); + + // document.documentElement.style.setProperty($colorVar, $pickedColor); + // $(".colorPicker #" + $colorVar).attr("value", $pickedColor); + // $(".colorPicker #" + $colorVar).val($pickedColor); + // $(".colorPicker #" + $colorVar + "-txt").val($pickedColor); + updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); +}); + +$(".pageSettings .section.themes .tabContainer .customTheme input[type=text]") + .on("blur", (e) => { + let $colorVar = $(e.currentTarget).attr("id"); + let $pickedColor = $(e.currentTarget).val(); + + updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); + }) + .on("keypress", function (e) { + if (e.which === 13) { + $(this).attr("disabled", "disabled"); + let $colorVar = $(e.currentTarget).attr("id"); + let $pickedColor = $(e.currentTarget).val(); + + updateColors($(".colorPicker #" + $colorVar).parent(), $pickedColor); + $(this).removeAttr("disabled"); + } + }); + +$(".pageSettings .saveCustomThemeButton").click((e) => { + let save = []; + $.each( + $(".pageSettings .section.customTheme [type='color']"), + (index, element) => { + save.push($(element).attr("value")); + } + ); + UpdateConfig.setCustomThemeColors(save); + ThemeController.set("custom"); + Notifications.add("Custom theme colors saved", 1); +}); + +$(".pageSettings #loadCustomColorsFromPreset").click((e) => { + // previewTheme(Config.theme); + $("#currentTheme").attr("href", `themes/${Config.theme}.css`); + + ThemeController.colorVars.forEach((e) => { + document.documentElement.style.setProperty(e, ""); + }); + + setTimeout(async () => { + ChartController.updateAllChartColors(); + + let themecolors = await ThemeColors.get(); + + ThemeController.colorVars.forEach((colorName) => { + let color; + if (colorName === "--bg-color") { + color = themecolors.bg; + } else if (colorName === "--main-color") { + color = themecolors.main; + } else if (colorName === "--sub-color") { + color = themecolors.sub; + } else if (colorName === "--caret-color") { + color = themecolors.caret; + } else if (colorName === "--text-color") { + color = themecolors.text; + } else if (colorName === "--error-color") { + color = themecolors.error; + } else if (colorName === "--error-extra-color") { + color = themecolors.errorExtra; + } else if (colorName === "--colorful-error-color") { + color = themecolors.colorfulError; + } else if (colorName === "--colorful-error-extra-color") { + color = themecolors.colorfulErrorExtra; + } + + updateColors($(".colorPicker #" + colorName).parent(), color); + }); + }, 250); +}); + +==> ./monkeytype/src/js/challenge-controller.js <== +import * as Misc from "./misc"; +import * as Notifications from "./notifications"; +import * as ManualRestart from "./manual-restart-tracker"; +import * as CustomText from "./custom-text"; +import * as TestLogic from "./test-logic"; +import * as Funbox from "./funbox"; +import Config, * as UpdateConfig from "./config"; +import * as UI from "./ui"; +import * as TestUI from "./test-ui"; + +export let active = null; +let challengeLoading = false; + +export function clearActive() { + if (active && !challengeLoading && !TestUI.testRestarting) { + Notifications.add("Challenge cleared", 0); + active = null; + } +} + +export function verify(result) { + try { + if (active) { + let afk = (result.afkDuration / result.testDuration) * 100; + + if (afk > 10) { + Notifications.add(`Challenge failed: AFK time is greater than 10%`, 0); + return null; + } + + if (!active.requirements) { + Notifications.add(`${active.display} challenge passed!`, 1); + return active.name; + } else { + let requirementsMet = true; + let failReasons = []; + for (let requirementType in active.requirements) { + if (requirementsMet == false) return; + let requirementValue = active.requirements[requirementType]; + if (requirementType == "wpm") { + let wpmMode = Object.keys(requirementValue)[0]; + if (wpmMode == "exact") { + if (Math.round(result.wpm) != requirementValue.exact) { + requirementsMet = false; + failReasons.push(`WPM not ${requirementValue.exact}`); + } + } else if (wpmMode == "min") { + if (result.wpm < requirementValue.min) { + requirementsMet = false; + failReasons.push(`WPM below ${requirementValue.min}`); + } + } + } else if (requirementType == "acc") { + let accMode = Object.keys(requirementValue)[0]; + if (accMode == "exact") { + if (result.acc != requirementValue.exact) { + requirementsMet = false; + failReasons.push(`Accuracy not ${requirementValue.exact}`); + } + } else if (accMode == "min") { + if (result.acc < requirementValue.min) { + requirementsMet = false; + failReasons.push(`Accuracy below ${requirementValue.min}`); + } + } + } else if (requirementType == "afk") { + let afkMode = Object.keys(requirementValue)[0]; + if (afkMode == "max") { + if (Math.round(afk) > requirementValue.max) { + requirementsMet = false; + failReasons.push( + `AFK percentage above ${requirementValue.max}` + ); + } + } + } else if (requirementType == "time") { + let timeMode = Object.keys(requirementValue)[0]; + if (timeMode == "min") { + if (Math.round(result.testDuration) < requirementValue.min) { + requirementsMet = false; + failReasons.push(`Test time below ${requirementValue.min}`); + } + } + } else if (requirementType == "funbox") { + let funboxMode = requirementValue; + if (funboxMode != result.funbox) { + requirementsMet = false; + failReasons.push(`${funboxMode} funbox not active`); + } + } else if (requirementType == "raw") { + let rawMode = Object.keys(requirementValue)[0]; + if (rawMode == "exact") { + if (Math.round(result.rawWpm) != requirementValue.exact) { + requirementsMet = false; + failReasons.push(`Raw WPM not ${requirementValue.exact}`); + } + } + } else if (requirementType == "con") { + let conMode = Object.keys(requirementValue)[0]; + if (conMode == "exact") { + if (Math.round(result.consistency) != requirementValue.exact) { + requirementsMet = false; + failReasons.push(`Consistency not ${requirementValue.exact}`); + } + } + } else if (requirementType == "config") { + for (let configKey in requirementValue) { + let configValue = requirementValue[configKey]; + if (Config[configKey] != configValue) { + requirementsMet = false; + failReasons.push(`${configKey} not set to ${configValue}`); + } + } + } + } + if (requirementsMet) { + if (active.autoRole) { + Notifications.add( + "You will receive a role shortly. Please don't post a screenshot in challenge submissions.", + 1, + 5 + ); + } + Notifications.add(`${active.display} challenge passed!`, 1); + return active.name; + } else { + Notifications.add( + `${active.display} challenge failed: ${failReasons.join(", ")}`, + 0 + ); + return null; + } + } + } else { + return null; + } + } catch (e) { + console.error(e); + Notifications.add( + `Something went wrong when verifying challenge: ${e.message}`, + 0 + ); + return null; + } +} + +export async function setup(challengeName) { + challengeLoading = true; + if (UI.getActivePage() !== "pageTest") { + UI.changePage("", true); + } + + let list = await Misc.getChallengeList(); + let challenge = list.filter((c) => c.name === challengeName)[0]; + let notitext; + try { + if (challenge === undefined) { + Notifications.add("Challenge not found", 0); + ManualRestart.set(); + TestLogic.restart(false, true); + setTimeout(() => { + $("#top .config").removeClass("hidden"); + $(".page.pageTest").removeClass("hidden"); + }, 250); + return; + } + if (challenge.type === "customTime") { + UpdateConfig.setTimeConfig(challenge.parameters[0], true); + UpdateConfig.setMode("time", true); + UpdateConfig.setDifficulty("normal", true); + if (challenge.name === "englishMaster") { + UpdateConfig.setLanguage("english_10k", true); + UpdateConfig.setNumbers(true, true); + UpdateConfig.setPunctuation(true, true); + } + } else if (challenge.type === "customWords") { + UpdateConfig.setWordCount(challenge.parameters[0], true); + UpdateConfig.setMode("words", true); + UpdateConfig.setDifficulty("normal", true); + } else if (challenge.type === "customText") { + CustomText.setText(challenge.parameters[0].split(" ")); + CustomText.setIsWordRandom(challenge.parameters[1]); + CustomText.setWord(parseInt(challenge.parameters[2])); + UpdateConfig.setMode("custom", true); + UpdateConfig.setDifficulty("normal", true); + } else if (challenge.type === "script") { + let scriptdata = await fetch("/challenges/" + challenge.parameters[0]); + scriptdata = await scriptdata.text(); + let text = scriptdata.trim(); + text = text.replace(/[\n\rt ]/gm, " "); + text = text.replace(/ +/gm, " "); + CustomText.setText(text.split(" ")); + CustomText.setIsWordRandom(false); + UpdateConfig.setMode("custom", true); + UpdateConfig.setDifficulty("normal", true); + if (challenge.parameters[1] != null) { + UpdateConfig.setTheme(challenge.parameters[1]); + } + if (challenge.parameters[2] != null) { + Funbox.activate(challenge.parameters[2]); + } + } else if (challenge.type === "accuracy") { + UpdateConfig.setTimeConfig(0, true); + UpdateConfig.setMode("time", true); + UpdateConfig.setDifficulty("master", true); + } else if (challenge.type === "funbox") { + UpdateConfig.setFunbox(challenge.parameters[0], true); + UpdateConfig.setDifficulty("normal", true); + if (challenge.parameters[1] === "words") { + UpdateConfig.setWordCount(challenge.parameters[2], true); + } else if (challenge.parameters[1] === "time") { + UpdateConfig.setTimeConfig(challenge.parameters[2], true); + } + UpdateConfig.setMode(challenge.parameters[1], true); + if (challenge.parameters[3] !== undefined) { + UpdateConfig.setDifficulty(challenge.parameters[3], true); + } + } else if (challenge.type === "special") { + if (challenge.name === "semimak") { + // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout? + UpdateConfig.setMode("time", true); + UpdateConfig.setTimeConfig(120, true); + UpdateConfig.setLanguage("english_10k", true); + UpdateConfig.setPunctuation(true, true); + UpdateConfig.setStopOnError("word", true); + UpdateConfig.setLayout("semimak", true); + UpdateConfig.setKeymapLayout("overrideSync", true); + UpdateConfig.setKeymapMode("static", true); + } + } + ManualRestart.set(); + TestLogic.restart(false, true); + notitext = challenge.message; + $("#top .config").removeClass("hidden"); + $(".page.pageTest").removeClass("hidden"); + + if (notitext === undefined) { + Notifications.add(`Challenge '${challenge.display}' loaded.`, 0); + } else { + Notifications.add("Challenge loaded. " + notitext, 0); + } + active = challenge; + challengeLoading = false; + } catch (e) { + Notifications.add("Something went wrong: " + e, -1); + } +} + +==> ./monkeytype/src/js/ui.js <== +import Config, * as UpdateConfig from "./config"; +import * as Notifications from "./notifications"; +import * as Caret from "./caret"; +import * as TestLogic from "./test-logic"; +import * as CustomText from "./custom-text"; +import * as CommandlineLists from "./commandline-lists"; +import * as Commandline from "./commandline"; +import * as TestUI from "./test-ui"; +import * as TestConfig from "./test-config"; +import * as SignOutButton from "./sign-out-button"; +import * as TestStats from "./test-stats"; +import * as ManualRestart from "./manual-restart-tracker"; +import * as Settings from "./settings"; +import * as Account from "./account"; +import * as Leaderboards from "./leaderboards"; +import * as Funbox from "./funbox"; +import * as About from "./about-page"; + +export let pageTransition = true; +let activePage = "pageLoading"; + +export function getActivePage() { + return activePage; +} + +export function setActivePage(active) { + activePage = active; +} + +export function setPageTransition(val) { + pageTransition = val; +} + +export function updateKeytips() { + if (Config.swapEscAndTab) { + $(".pageSettings .tip").html(` + tip: You can also change all these settings quickly using the + command line ( + tab + )`); + $("#bottom .keyTips").html(` + esc - restart test
+ tab - command line`); + } else { + $(".pageSettings .tip").html(` + tip: You can also change all these settings quickly using the + command line ( + esc + )`); + $("#bottom .keyTips").html(` + tab - restart test
+ esc or ctrl/cmd+shift+p - command line`); + } +} + +export function swapElements( + el1, + el2, + totalDuration, + callback = function () { + return; + }, + middleCallback = function () { + return; + } +) { + if ( + (el1.hasClass("hidden") && !el2.hasClass("hidden")) || + (!el1.hasClass("hidden") && el2.hasClass("hidden")) + ) { + //one of them is hidden and the other is visible + if (el1.hasClass("hidden")) { + callback(); + return false; + } + $(el1) + .removeClass("hidden") + .css("opacity", 1) + .animate( + { + opacity: 0, + }, + totalDuration / 2, + () => { + middleCallback(); + $(el1).addClass("hidden"); + $(el2) + .removeClass("hidden") + .css("opacity", 0) + .animate( + { + opacity: 1, + }, + totalDuration / 2, + () => { + callback(); + } + ); + } + ); + } else if (el1.hasClass("hidden") && el2.hasClass("hidden")) { + //both are hidden, only fade in the second + $(el2) + .removeClass("hidden") + .css("opacity", 0) + .animate( + { + opacity: 1, + }, + totalDuration, + () => { + callback(); + } + ); + } else { + callback(); + } +} + +export function changePage(page, norestart = false) { + if (pageTransition) { + console.log(`change page ${page} stopped`); + return; + } + + if (page == undefined) { + //use window loacation + let pages = { + "/": "test", + "/login": "login", + "/settings": "settings", + "/about": "about", + "/account": "account", + }; + let path = pages[window.location.pathname]; + if (!path) { + path = "test"; + } + page = path; + } + + console.log(`change page ${page}`); + let activePageElement = $(".page.active"); + let check = activePage + ""; + setTimeout(() => { + if (check === "pageAccount" && page !== "account") { + Account.reset(); + } else if (check === "pageSettings" && page !== "settings") { + Settings.reset(); + } else if (check === "pageAbout" && page !== "about") { + About.reset(); + } + }, 250); + + activePage = undefined; + $(".page").removeClass("active"); + $("#wordsInput").focusout(); + if (page == "test" || page == "") { + setPageTransition(true); + swapElements( + activePageElement, + $(".page.pageTest"), + 250, + () => { + setPageTransition(false); + TestUI.focusWords(); + $(".page.pageTest").addClass("active"); + activePage = "pageTest"; + history.pushState("/", null, "/"); + }, + () => { + TestConfig.show(); + } + ); + SignOutButton.hide(); + // restartCount = 0; + // incompleteTestSeconds = 0; + TestStats.resetIncomplete(); + ManualRestart.set(); + if (!norestart) TestLogic.restart(); + Funbox.activate(Config.funbox); + } else if (page == "about") { + setPageTransition(true); + TestLogic.restart(); + swapElements(activePageElement, $(".page.pageAbout"), 250, () => { + setPageTransition(false); + history.pushState("about", null, "about"); + $(".page.pageAbout").addClass("active"); + activePage = "pageAbout"; + }); + About.fill(); + Funbox.activate("none"); + TestConfig.hide(); + SignOutButton.hide(); + } else if (page == "settings") { + setPageTransition(true); + TestLogic.restart(); + swapElements(activePageElement, $(".page.pageSettings"), 250, () => { + setPageTransition(false); + history.pushState("settings", null, "settings"); + $(".page.pageSettings").addClass("active"); + activePage = "pageSettings"; + }); + Funbox.activate("none"); + Settings.fillSettingsPage().then(() => { + Settings.update(); + }); + // Settings.update(); + TestConfig.hide(); + SignOutButton.hide(); + } else if (page == "account") { + if (!firebase.auth().currentUser) { + console.log( + `current user is ${firebase.auth().currentUser}, going back to login` + ); + changePage("login"); + } else { + setPageTransition(true); + TestLogic.restart(); + swapElements(activePageElement, $(".page.pageAccount"), 250, () => { + setPageTransition(false); + history.pushState("account", null, "account"); + $(".page.pageAccount").addClass("active"); + activePage = "pageAccount"; + }); + Funbox.activate("none"); + Account.update(); + TestConfig.hide(); + } + } else if (page == "login") { + if (firebase.auth().currentUser != null) { + changePage("account"); + } else { + setPageTransition(true); + TestLogic.restart(); + swapElements(activePageElement, $(".page.pageLogin"), 250, () => { + setPageTransition(false); + history.pushState("login", null, "login"); + $(".page.pageLogin").addClass("active"); + activePage = "pageLogin"; + }); + Funbox.activate("none"); + TestConfig.hide(); + SignOutButton.hide(); + } + } +} + +//checking if the project is the development site +/* +if (firebase.app().options.projectId === "monkey-type-dev-67af4") { + $("#top .logo .bottom").text("monkey-dev"); + $("head title").text("Monkey Dev"); + $("body").append( + `
DEV
DEV
` + ); +} +*/ + +if (window.location.hostname === "localhost") { + window.onerror = function (error) { + Notifications.add(error, -1); + }; + $("#top .logo .top").text("localhost"); + $("head title").text($("head title").text() + " (localhost)"); + //firebase.functions().useFunctionsEmulator("http://localhost:5001"); + $("body").append( + `
local
local
` + ); + $(".pageSettings .discordIntegration .buttons a").attr( + "href", + "https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fverify&response_type=token&scope=identify" + ); +} + +//stop space scrolling +window.addEventListener("keydown", function (e) { + if (e.keyCode == 32 && e.target == document.body) { + e.preventDefault(); + } +}); + +$(document).on("click", "#bottom .leftright .right .current-theme", (e) => { + if (e.shiftKey) { + UpdateConfig.toggleCustomTheme(); + } else { + // if (Config.customTheme) { + // toggleCustomTheme(); + // } + CommandlineLists.pushCurrent(CommandlineLists.themeCommands); + Commandline.show(); + } +}); + +$(document.body).on("click", ".pageAbout .aboutEnableAds", () => { + CommandlineLists.pushCurrent(CommandlineLists.commandsEnableAds); + Commandline.show(); +}); + +window.addEventListener("beforeunload", (event) => { + // Cancel the event as stated by the standard. + if ( + (Config.mode === "words" && Config.words < 1000) || + (Config.mode === "time" && Config.time < 3600) || + Config.mode === "quote" || + (Config.mode === "custom" && + CustomText.isWordRandom && + CustomText.word < 1000) || + (Config.mode === "custom" && + CustomText.isTimeRandom && + CustomText.time < 1000) || + (Config.mode === "custom" && + !CustomText.isWordRandom && + CustomText.text.length < 1000) + ) { + //ignore + } else { + if (TestLogic.active) { + event.preventDefault(); + // Chrome requires returnValue to be set. + event.returnValue = ""; + } + } +}); + +$(window).resize(() => { + Caret.updatePosition(); +}); + +$(document).on("click", "#top .logo", (e) => { + changePage("test"); +}); + +$(document).on("click", "#top #menu .icon-button", (e) => { + if ($(e.currentTarget).hasClass("leaderboards")) { + Leaderboards.show(); + } else { + const href = $(e.currentTarget).attr("href"); + ManualRestart.set(); + changePage(href.replace("/", "")); + } + return false; +}); + +==> ./monkeytype/src/js/axios-instance.js <== +import axios from "axios"; + +let apiPath = ""; + +let baseURL; +if (window.location.hostname === "localhost") { + baseURL = "http://localhost:5005" + apiPath; +} else { + baseURL = "https://api.monkeytype.com" + apiPath; +} + +const axiosInstance = axios.create({ + baseURL: baseURL, + timeout: 10000, +}); + +// Request interceptor for API calls +axiosInstance.interceptors.request.use( + async (config) => { + let idToken; + if (firebase.auth().currentUser != null) { + idToken = await firebase.auth().currentUser.getIdToken(); + } else { + idToken = null; + } + if (idToken) { + config.headers = { + Authorization: `Bearer ${idToken}`, + Accept: "application/json", + "Content-Type": "application/json", + }; + } else { + config.headers = { + Accept: "application/json", + "Content-Type": "application/json", + }; + } + return config; + }, + (error) => { + Promise.reject(error); + } +); + +axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + // whatever you want to do with the error + // console.log('interctepted'); + // if(error.response.data.message){ + // Notifications.add(`${error.response.data.message}`); + // }else{ + // Notifications.add(`${error.response.status} ${error.response.statusText}`); + // } + // return error.response; + throw error; + } +); + +export default axiosInstance; + +==> ./monkeytype/src/js/commandline.js <== +import * as Leaderboards from "./leaderboards"; +import * as ThemeController from "./theme-controller"; +import Config, * as UpdateConfig from "./config"; +import * as Focus from "./focus"; +import * as CommandlineLists from "./commandline-lists"; +import * as TestUI from "./test-ui"; +import * as PractiseWords from "./practise-words"; +import * as SimplePopups from "./simple-popups"; +import * as CustomWordAmountPopup from "./custom-word-amount-popup"; +import * as CustomTestDurationPopup from "./custom-test-duration-popup"; +import * as CustomTextPopup from "./custom-text-popup"; +import * as QuoteSearchPopupWrapper from "./quote-search-popup"; + +let commandLineMouseMode = false; + +function showInput(command, placeholder, defaultValue = "") { + $("#commandLineWrapper").removeClass("hidden"); + $("#commandLine").addClass("hidden"); + $("#commandInput").removeClass("hidden"); + $("#commandInput input").attr("placeholder", placeholder); + $("#commandInput input").val(defaultValue); + $("#commandInput input").focus(); + $("#commandInput input").attr("command", ""); + $("#commandInput input").attr("command", command); + if (defaultValue != "") { + $("#commandInput input").select(); + } +} + +export function isSingleListCommandLineActive() { + return $("#commandLine").hasClass("allCommands"); +} + +function showFound() { + $("#commandLine .suggestions").empty(); + let commandsHTML = ""; + let list = CommandlineLists.current[CommandlineLists.current.length - 1]; + $.each(list.list, (index, obj) => { + if (obj.found && (obj.available !== undefined ? obj.available() : true)) { + let icon = obj.icon ?? "fa-chevron-right"; + let faIcon = /^fa-/g.test(icon); + if (!faIcon) { + icon = `
${icon}
`; + } else { + icon = ``; + } + if (list.configKey) { + if ( + (obj.configValueMode && + obj.configValueMode === "include" && + Config[list.configKey].includes(obj.configValue)) || + Config[list.configKey] === obj.configValue + ) { + icon = ``; + } else { + icon = ``; + } + } + let iconHTML = `
${icon}
`; + if (obj.noIcon && !isSingleListCommandLineActive()) { + iconHTML = ""; + } + commandsHTML += `
${iconHTML}
${obj.display}
`; + } + }); + $("#commandLine .suggestions").html(commandsHTML); + if ($("#commandLine .suggestions .entry").length == 0) { + $("#commandLine .separator").css({ height: 0, margin: 0 }); + } else { + $("#commandLine .separator").css({ + height: "1px", + "margin-bottom": ".5rem", + }); + } + let entries = $("#commandLine .suggestions .entry"); + if (entries.length > 0) { + $(entries[0]).addClass("activeKeyboard"); + try { + $.each(list.list, (index, obj) => { + if (obj.found) { + if ( + (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && + !ThemeController.randomTheme + ) + ThemeController.clearPreview(); + if (!/font/gi.test(obj.id)) + UpdateConfig.previewFontFamily(Config.fontFamily); + obj.hover(); + return false; + } + }); + } catch (e) {} + } + $("#commandLine .listTitle").remove(); +} + +function updateSuggested() { + let inputVal = $("#commandLine input") + .val() + .toLowerCase() + .split(" ") + .filter((s, i) => s || i == 0); //remove empty entries after first + let list = CommandlineLists.current[CommandlineLists.current.length - 1]; + if ( + inputVal[0] === "" && + Config.singleListCommandLine === "on" && + CommandlineLists.current.length === 1 + ) { + $.each(list.list, (index, obj) => { + obj.found = false; + }); + showFound(); + return; + } + //ignore the preceeding ">"s in the command line input + if (inputVal[0] && inputVal[0][0] == ">") + inputVal[0] = inputVal[0].replace(/^>+/, ""); + if (inputVal[0] == "" && inputVal.length == 1) { + $.each(list.list, (index, obj) => { + if (obj.visible !== false) obj.found = true; + }); + } else { + $.each(list.list, (index, obj) => { + let foundcount = 0; + $.each(inputVal, (index2, obj2) => { + if (obj2 == "") return; + let escaped = obj2.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + let re = new RegExp("\\b" + escaped, "g"); + let res = obj.display.toLowerCase().match(re); + let res2 = + obj.alias !== undefined ? obj.alias.toLowerCase().match(re) : null; + if ( + (res != null && res.length > 0) || + (res2 != null && res2.length > 0) + ) { + foundcount++; + } else { + foundcount--; + } + }); + if (foundcount > inputVal.length - 1) { + obj.found = true; + } else { + obj.found = false; + } + }); + } + showFound(); +} + +export let show = () => { + if (!$(".page.pageLoading").hasClass("hidden")) return; + Focus.set(false); + $("#commandLine").removeClass("hidden"); + $("#commandInput").addClass("hidden"); + if ($("#commandLineWrapper").hasClass("hidden")) { + $("#commandLineWrapper") + .stop(true, true) + .css("opacity", 0) + .removeClass("hidden") + .animate( + { + opacity: 1, + }, + 100 + ); + } + $("#commandLine input").val(""); + updateSuggested(); + $("#commandLine input").focus(); +}; + +function hide() { + UpdateConfig.previewFontFamily(Config.fontFamily); + // applyCustomThemeColors(); + if (!ThemeController.randomTheme) { + ThemeController.clearPreview(); + } + $("#commandLineWrapper") + .stop(true, true) + .css("opacity", 1) + .animate( + { + opacity: 0, + }, + 100, + () => { + $("#commandLineWrapper").addClass("hidden"); + $("#commandLine").removeClass("allCommands"); + TestUI.focusWords(); + } + ); + TestUI.focusWords(); +} + +function trigger(command) { + let subgroup = false; + let input = false; + let list = CommandlineLists.current[CommandlineLists.current.length - 1]; + let sticky = false; + $.each(list.list, (i, obj) => { + if (obj.id == command) { + if (obj.input) { + input = true; + let escaped = obj.display.split("")[1] ?? obj.display; + showInput(obj.id, escaped, obj.defaultValue); + } else if (obj.subgroup) { + subgroup = true; + if (obj.beforeSubgroup) { + obj.beforeSubgroup(); + } + CommandlineLists.current.push(obj.subgroup); + show(); + } else { + obj.exec(); + if (obj.sticky === true) { + sticky = true; + } + } + } + }); + if (!subgroup && !input && !sticky) { + try { + firebase.analytics().logEvent("usedCommandLine", { + command: command, + }); + } catch (e) { + console.log("Analytics unavailable"); + } + hide(); + } +} + +function addChildCommands( + unifiedCommands, + commandItem, + parentCommandDisplay = "", + parentCommand = "" +) { + let commandItemDisplay = commandItem.display.replace(/\s?\.\.\.$/g, ""); + let icon = ``; + if ( + commandItem.configValue !== undefined && + Config[parentCommand.configKey] === commandItem.configValue + ) { + icon = ``; + } + if (commandItem.noIcon) { + icon = ""; + } + + if (parentCommandDisplay) + commandItemDisplay = + parentCommandDisplay + " > " + icon + commandItemDisplay; + if (commandItem.subgroup) { + if (commandItem.beforeSubgroup) commandItem.beforeSubgroup(); + try { + commandItem.subgroup.list.forEach((cmd) => { + commandItem.configKey = commandItem.subgroup.configKey; + addChildCommands(unifiedCommands, cmd, commandItemDisplay, commandItem); + }); + // commandItem.exec(); + // const currentCommandsIndex = CommandlineLists.current.length - 1; + // CommandlineLists.current[currentCommandsIndex].list.forEach((cmd) => { + // if (cmd.alias === undefined) cmd.alias = commandItem.alias; + // addChildCommands(unifiedCommands, cmd, commandItemDisplay); + // }); + // CommandlineLists.current.pop(); + } catch (e) {} + } else { + let tempCommandItem = { ...commandItem }; + tempCommandItem.icon = parentCommand.icon; + if (parentCommandDisplay) tempCommandItem.display = commandItemDisplay; + unifiedCommands.push(tempCommandItem); + } +} + +function generateSingleListOfCommands() { + const allCommands = []; + const oldShowCommandLine = show; + show = () => {}; + CommandlineLists.defaultCommands.list.forEach((c) => + addChildCommands(allCommands, c) + ); + show = oldShowCommandLine; + return { + title: "All Commands", + list: allCommands, + }; +} + +function useSingleListCommandLine(sshow = true) { + let allCommands = generateSingleListOfCommands(); + // if (Config.singleListCommandLine == "manual") { + // CommandlineLists.pushCurrent(allCommands); + // } else if (Config.singleListCommandLine == "on") { + CommandlineLists.setCurrent([allCommands]); + // } + if (Config.singleListCommandLine != "off") + $("#commandLine").addClass("allCommands"); + if (sshow) show(); +} + +function restoreOldCommandLine(sshow = true) { + if (isSingleListCommandLineActive()) { + $("#commandLine").removeClass("allCommands"); + CommandlineLists.setCurrent( + CommandlineLists.current.filter((l) => l.title != "All Commands") + ); + if (CommandlineLists.current.length < 1) + CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); + } + if (sshow) show(); +} + +$("#commandLine input").keyup((e) => { + commandLineMouseMode = false; + $("#commandLineWrapper #commandLine .suggestions .entry").removeClass( + "activeMouse" + ); + if ( + e.key === "ArrowUp" || + e.key === "ArrowDown" || + e.key === "Enter" || + e.key === "Tab" || + e.code == "AltLeft" || + (e.key.length > 1 && e.key !== "Backspace" && e.key !== "Delete") + ) + return; + updateSuggested(); +}); + +$(document).ready((e) => { + $(document).keydown((event) => { + // opens command line if escape, ctrl/cmd + shift + p, or tab is pressed if the setting swapEscAndTab is enabled + if ( + event.key === "Escape" || + (event.key && + event.key.toLowerCase() === "p" && + (event.metaKey || event.ctrlKey) && + event.shiftKey) || + (event.key === "Tab" && Config.swapEscAndTab) + ) { + event.preventDefault(); + if (!$("#leaderboardsWrapper").hasClass("hidden")) { + //maybe add more condition for closing other dialogs in the future as well + event.preventDefault(); + Leaderboards.hide(); + } else if (!$("#practiseWordsPopupWrapper").hasClass("hidden")) { + event.preventDefault(); + PractiseWords.hide(); + } else if (!$("#simplePopupWrapper").hasClass("hidden")) { + event.preventDefault(); + SimplePopups.hide(); + } else if (!$("#customWordAmountPopupWrapper").hasClass("hidden")) { + event.preventDefault(); + CustomWordAmountPopup.hide(); + } else if (!$("#customTestDurationPopupWrapper").hasClass("hidden")) { + event.preventDefault(); + CustomTestDurationPopup.hide(); + } else if (!$("#customTextPopupWrapper").hasClass("hidden")) { + event.preventDefault(); + CustomTextPopup.hide(); + } else if (!$("#quoteSearchPopupWrapper").hasClass("hidden")) { + event.preventDefault(); + QuoteSearchPopupWrapper.hide(); + } else if (!$("#commandLineWrapper").hasClass("hidden")) { + if (CommandlineLists.current.length > 1) { + CommandlineLists.current.pop(); + $("#commandLine").removeClass("allCommands"); + show(); + } else { + hide(); + } + UpdateConfig.setFontFamily(Config.fontFamily, true); + } else if (event.key === "Tab" || !Config.swapEscAndTab) { + if (Config.singleListCommandLine == "on") { + useSingleListCommandLine(false); + } else { + CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); + } + show(); + } + } + }); +}); + +$("#commandInput input").keydown((e) => { + if (e.key === "Enter") { + //enter + e.preventDefault(); + let command = $("#commandInput input").attr("command"); + let value = $("#commandInput input").val(); + let list = CommandlineLists.current[CommandlineLists.current.length - 1]; + $.each(list.list, (i, obj) => { + if (obj.id == command) { + obj.exec(value); + if (obj.subgroup !== null && obj.subgroup !== undefined) { + //TODO: what is this for? + // subgroup = obj.subgroup; + } + } + }); + try { + firebase.analytics().logEvent("usedCommandLine", { + command: command, + }); + } catch (e) { + console.log("Analytics unavailable"); + } + hide(); + } + return; +}); + +$(document).on("mousemove", () => { + if (!commandLineMouseMode) commandLineMouseMode = true; +}); + +$(document).on( + "mouseenter", + "#commandLineWrapper #commandLine .suggestions .entry", + (e) => { + if (!commandLineMouseMode) return; + $(e.target).addClass("activeMouse"); + } +); + +$(document).on( + "mouseleave", + "#commandLineWrapper #commandLine .suggestions .entry", + (e) => { + if (!commandLineMouseMode) return; + $(e.target).removeClass("activeMouse"); + } +); + +$("#commandLineWrapper #commandLine .suggestions").on("mouseover", (e) => { + if (!commandLineMouseMode) return; + // console.log("clearing keyboard active"); + $("#commandLineWrapper #commandLine .suggestions .entry").removeClass( + "activeKeyboard" + ); + let hoverId = $(e.target).attr("command"); + try { + let list = CommandlineLists.current[CommandlineLists.current.length - 1]; + $.each(list.list, (index, obj) => { + if (obj.id == hoverId) { + if ( + (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && + !ThemeController.randomTheme + ) + ThemeController.clearPreview(); + if (!/font/gi.test(obj.id)) + UpdateConfig.previewFontFamily(Config.fontFamily); + obj.hover(); + } + }); + } catch (e) {} +}); + +$(document).on( + "click", + "#commandLineWrapper #commandLine .suggestions .entry", + (e) => { + $(".suggestions .entry").removeClass("activeKeyboard"); + trigger($(e.currentTarget).attr("command")); + } +); + +$("#commandLineWrapper").click((e) => { + if ($(e.target).attr("id") === "commandLineWrapper") { + hide(); + UpdateConfig.setFontFamily(Config.fontFamily, true); + // if (Config.customTheme === true) { + // applyCustomThemeColors(); + // } else { + // setTheme(Config.theme, true); + // } + } +}); + +//might come back to it later +// function shiftCommand(){ +// let activeEntries = $("#commandLineWrapper #commandLine .suggestions .entry.activeKeyboard, #commandLineWrapper #commandLine .suggestions .entry.activeMouse"); +// activeEntries.each((index, activeEntry) => { +// let commandId = activeEntry.getAttribute('command'); +// let foundCommand = null; +// CommandlineLists.defaultCommands.list.forEach(command => { +// if(foundCommand === null && command.id === commandId){ +// foundCommand = command; +// } +// }) +// if(foundCommand.shift){ +// $(activeEntry).find('div').text(foundCommand.shift.display); +// } +// }) +// } + +// let shiftedCommands = false; +// $(document).keydown((e) => { +// if (e.key === "Shift") { +// if(shiftedCommands === false){ +// shiftedCommands = true; +// shiftCommand(); +// } + +// } +// }); + +// $(document).keyup((e) => { +// if (e.key === "Shift") { +// shiftedCommands = false; +// } +// }); + +$(document).keydown((e) => { + // if (isPreviewingTheme) { + // console.log("applying theme"); + // applyCustomThemeColors(); + // previewTheme(Config.theme, false); + // } + if (!$("#commandLineWrapper").hasClass("hidden")) { + $("#commandLine input").focus(); + if (e.key == ">" && Config.singleListCommandLine == "manual") { + if (!isSingleListCommandLineActive()) { + useSingleListCommandLine(false); + return; + } else if ($("#commandLine input").val() == ">") { + //so that it will ignore succeeding ">" when input is already ">" + e.preventDefault(); + return; + } + } + + if (e.key === "Backspace" || e.key === "Delete") { + setTimeout(() => { + let inputVal = $("#commandLine input").val(); + if ( + Config.singleListCommandLine == "manual" && + isSingleListCommandLineActive() && + inputVal[0] !== ">" + ) { + restoreOldCommandLine(false); + } + }, 1); + } + + if (e.key === "Enter") { + //enter + e.preventDefault(); + let command = $(".suggestions .entry.activeKeyboard").attr("command"); + trigger(command); + return; + } + if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Tab") { + e.preventDefault(); + $("#commandLineWrapper #commandLine .suggestions .entry").unbind( + "mouseenter mouseleave" + ); + let entries = $(".suggestions .entry"); + let activenum = -1; + let hoverId; + $.each(entries, (index, obj) => { + if ($(obj).hasClass("activeKeyboard")) activenum = index; + }); + if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { + entries.removeClass("activeKeyboard"); + if (activenum == 0) { + $(entries[entries.length - 1]).addClass("activeKeyboard"); + hoverId = $(entries[entries.length - 1]).attr("command"); + } else { + $(entries[--activenum]).addClass("activeKeyboard"); + hoverId = $(entries[activenum]).attr("command"); + } + } + if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) { + entries.removeClass("activeKeyboard"); + if (activenum + 1 == entries.length) { + $(entries[0]).addClass("activeKeyboard"); + hoverId = $(entries[0]).attr("command"); + } else { + $(entries[++activenum]).addClass("activeKeyboard"); + hoverId = $(entries[activenum]).attr("command"); + } + } + try { + let scroll = + Math.abs( + $(".suggestions").offset().top - + $(".entry.activeKeyboard").offset().top - + $(".suggestions").scrollTop() + ) - + $(".suggestions").outerHeight() / 2 + + $($(".entry")[0]).outerHeight(); + $(".suggestions").scrollTop(scroll); + } catch (e) { + console.log("could not scroll suggestions: " + e.message); + } + // console.log(`scrolling to ${scroll}`); + try { + let list = + CommandlineLists.current[CommandlineLists.current.length - 1]; + $.each(list.list, (index, obj) => { + if (obj.id == hoverId) { + if ( + (!/theme/gi.test(obj.id) || obj.id === "toggleCustomTheme") && + !ThemeController.randomTheme + ) + ThemeController.clearPreview(); + if (!/font/gi.test(obj.id)) + UpdateConfig.previewFontFamily(Config.fontFamily); + obj.hover(); + } + }); + } catch (e) {} + + return false; + } + } +}); + +$(document).on("click", "#commandLineMobileButton", () => { + CommandlineLists.setCurrent([CommandlineLists.defaultCommands]); + show(); +}); + +==> ./monkeytype/src/js/test/poetry.js <== +const bannedChars = ["—", "_", " "]; +const maxWords = 100; +const apiURL = "https://poetrydb.org/random"; + +export class Poem { + constructor(title, author, words) { + this.title = title; + this.author = author; + this.words = words; + + this.cleanUpText(); + } + + cleanUpText() { + var count = 0; + var scrubbedWords = []; + for (var i = 0; i < this.words.length; i++) { + let scrubbed = ""; + for (var j = 0; j < this.words[i].length; j++) { + if (!bannedChars.includes(this.words[i][j])) + scrubbed += this.words[i][j]; + } + + if (scrubbed == "") continue; + + scrubbedWords.push(scrubbed); + count++; + + if (count == maxWords) break; + } + + this.words = scrubbedWords; + } +} + +export async function getPoem() { + return new Promise((res, rej) => { + console.log("Getting poem"); + var poemReq = new XMLHttpRequest(); + poemReq.onload = () => { + if (poemReq.readyState == 4) { + if (poemReq.status == 200) { + let poemObj = JSON.parse(poemReq.responseText)[0]; + let words = []; + poemObj.lines.forEach((line) => { + line.split(" ").forEach((word) => { + words.push(word); + }); + }); + + let poem = new Poem(poemObj.title, poemObj.author, words); + res(poem); + } else { + rej(poemReq.status); + } + } + }; + poemReq.open("GET", apiURL); + poemReq.send(); + }); +} + +==> ./monkeytype/src/js/test/test-timer.js <== +//most of the code is thanks to +//https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript + +import Config, * as UpdateConfig from "./config"; +import * as CustomText from "./custom-text"; +import * as TimerProgress from "./timer-progress"; +import * as LiveWpm from "./live-wpm"; +import * as TestStats from "./test-stats"; +import * as Monkey from "./monkey"; +import * as Misc from "./misc"; +import * as Notifications from "./notifications"; +import * as TestLogic from "./test-logic"; +import * as Caret from "./caret"; + +export let slowTimer = false; +let slowTimerCount = 0; +export let time = 0; +let timer = null; +const interval = 1000; +let expected = 0; + +function setSlowTimer() { + if (slowTimer) return; + slowTimer = true; + console.error("Slow timer, disabling animations"); + // Notifications.add("Slow timer detected", -1, 5); +} + +function clearSlowTimer() { + slowTimer = false; + slowTimerCount = 0; +} + +let timerDebug = false; +export function enableTimerDebug() { + timerDebug = true; +} + +export function clear() { + time = 0; + clearTimeout(timer); +} + +function premid() { + if (timerDebug) console.time("premid"); + document.querySelector("#premidSecondsLeft").innerHTML = Config.time - time; + if (timerDebug) console.timeEnd("premid"); +} + +function updateTimer() { + if (timerDebug) console.time("timer progress update"); + if ( + Config.mode === "time" || + (Config.mode === "custom" && CustomText.isTimeRandom) + ) { + TimerProgress.update(time); + } + if (timerDebug) console.timeEnd("timer progress update"); +} + +function calculateWpmRaw() { + if (timerDebug) console.time("calculate wpm and raw"); + let wpmAndRaw = TestLogic.calculateWpmAndRaw(); + if (timerDebug) console.timeEnd("calculate wpm and raw"); + if (timerDebug) console.time("update live wpm"); + LiveWpm.update(wpmAndRaw.wpm, wpmAndRaw.raw); + if (timerDebug) console.timeEnd("update live wpm"); + if (timerDebug) console.time("push to history"); + TestStats.pushToWpmHistory(wpmAndRaw.wpm); + TestStats.pushToRawHistory(wpmAndRaw.raw); + if (timerDebug) console.timeEnd("push to history"); + return wpmAndRaw; +} + +function monkey(wpmAndRaw) { + if (timerDebug) console.time("update monkey"); + Monkey.updateFastOpacity(wpmAndRaw.wpm); + if (timerDebug) console.timeEnd("update monkey"); +} + +function calculateAcc() { + if (timerDebug) console.time("calculate acc"); + let acc = Misc.roundTo2(TestStats.calculateAccuracy()); + if (timerDebug) console.timeEnd("calculate acc"); + return acc; +} + +function layoutfluid() { + if (timerDebug) console.time("layoutfluid"); + if (Config.funbox === "layoutfluid" && Config.mode === "time") { + const layouts = Config.customLayoutfluid + ? Config.customLayoutfluid.split("#") + : ["qwerty", "dvorak", "colemak"]; + // console.log(Config.customLayoutfluid); + // console.log(layouts); + const numLayouts = layouts.length; + let index = 0; + index = Math.floor(time / (Config.time / numLayouts)); + + if ( + time == Math.floor(Config.time / numLayouts) - 3 || + time == (Config.time / numLayouts) * 2 - 3 + ) { + Notifications.add("3", 0, 1); + } + if ( + time == Math.floor(Config.time / numLayouts) - 2 || + time == Math.floor(Config.time / numLayouts) * 2 - 2 + ) { + Notifications.add("2", 0, 1); + } + if ( + time == Math.floor(Config.time / numLayouts) - 1 || + time == Math.floor(Config.time / numLayouts) * 2 - 1 + ) { + Notifications.add("1", 0, 1); + } + + if (Config.layout !== layouts[index] && layouts[index] !== undefined) { + Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0); + UpdateConfig.setLayout(layouts[index], true); + UpdateConfig.setKeymapLayout(layouts[index], true); + } + } + if (timerDebug) console.timeEnd("layoutfluid"); +} + +function checkIfFailed(wpmAndRaw, acc) { + if (timerDebug) console.time("fail conditions"); + TestStats.pushKeypressesToHistory(); + if ( + Config.minWpm === "custom" && + wpmAndRaw.wpm < parseInt(Config.minWpmCustomSpeed) && + TestLogic.words.currentIndex > 3 + ) { + clearTimeout(timer); + TestLogic.fail("min wpm"); + clearSlowTimer(); + return; + } + if ( + Config.minAcc === "custom" && + acc < parseInt(Config.minAccCustom) && + TestLogic.words.currentIndex > 3 + ) { + clearTimeout(timer); + TestLogic.fail("min accuracy"); + clearSlowTimer(); + return; + } + if (timerDebug) console.timeEnd("fail conditions"); +} + +function checkIfTimeIsUp() { + if (timerDebug) console.time("times up check"); + if ( + Config.mode == "time" || + (Config.mode === "custom" && CustomText.isTimeRandom) + ) { + if ( + (time >= Config.time && Config.time !== 0 && Config.mode === "time") || + (time >= CustomText.time && + CustomText.time !== 0 && + Config.mode === "custom") + ) { + //times up + clearTimeout(timer); + Caret.hide(); + TestLogic.input.pushHistory(); + TestLogic.corrected.pushHistory(); + TestLogic.finish(); + clearSlowTimer(); + return; + } + } + if (timerDebug) console.timeEnd("times up check"); +} + +// --------------------------------------- + +let timerStats = []; + +export function getTimerStats() { + return timerStats; +} + +async function timerStep() { + if (timerDebug) console.time("timer step -----------------------------"); + time++; + premid(); + updateTimer(); + let wpmAndRaw = calculateWpmRaw(); + let acc = calculateAcc(); + monkey(wpmAndRaw); + layoutfluid(); + checkIfFailed(wpmAndRaw, acc); + checkIfTimeIsUp(); + if (timerDebug) console.timeEnd("timer step -----------------------------"); +} + +export async function start() { + clearSlowTimer(); + timerStats = []; + expected = TestStats.start + interval; + (function loop() { + const delay = expected - performance.now(); + timerStats.push({ + dateNow: Date.now(), + now: performance.now(), + expected: expected, + nextDelay: delay, + }); + if ( + (Config.mode === "time" && Config.time < 130 && Config.time > 0) || + (Config.mode === "words" && Config.words < 250 && Config.words > 0) + ) { + if (delay < interval / 2) { + //slow timer + setSlowTimer(); + } + if (delay < interval / 10) { + slowTimerCount++; + if (slowTimerCount > 5) { + //slow timer + Notifications.add( + "Stopping the test due to bad performance. This would cause test calculations to be incorrect. If this happens a lot, please report this.", + -1 + ); + TestLogic.fail("slow timer"); + } + } + } + timer = setTimeout(function () { + // time++; + + if (!TestLogic.active) { + clearTimeout(timer); + clearSlowTimer(); + return; + } + + timerStep(); + + expected += interval; + loop(); + }, delay); + })(); +} + +==> ./monkeytype/src/js/test/caps-warning.js <== +import Config from "./config"; + +function show() { + if ($("#capsWarning").hasClass("hidden")) { + $("#capsWarning").removeClass("hidden"); + } +} + +function hide() { + if (!$("#capsWarning").hasClass("hidden")) { + $("#capsWarning").addClass("hidden"); + } +} + +$(document).keydown(function (event) { + try { + if ( + Config.capsLockWarning && + event.originalEvent.getModifierState("CapsLock") + ) { + show(); + } else { + hide(); + } + } catch {} +}); + +$(document).keyup(function (event) { + try { + if ( + Config.capsLockWarning && + event.originalEvent.getModifierState("CapsLock") + ) { + show(); + } else { + hide(); + } + } catch {} +}); + +==> ./monkeytype/src/js/test/shift-tracker.js <== +import Config from "./config"; +import Layouts from "./layouts"; + +export let leftState = false; +export let rightState = false; + +let keymapStrings = { + left: null, + right: null, + keymap: null, +}; + +function buildKeymapStrings() { + if (keymapStrings.keymap === Config.keymapLayout) return; + + let layout = Layouts[Config.keymapLayout]?.keys; + + if (!layout) { + keymapStrings = { + left: null, + right: null, + keymap: Config.keymapLayout, + }; + } else { + keymapStrings.left = ( + layout.slice(0, 7).join(" ") + + " " + + layout.slice(13, 19).join(" ") + + " " + + layout.slice(26, 31).join(" ") + + " " + + layout.slice(38, 43).join(" ") + ).replace(/ /g, ""); + keymapStrings.right = ( + layout.slice(6, 13).join(" ") + + " " + + layout.slice(18, 26).join(" ") + + " " + + layout.slice(31, 38).join(" ") + + " " + + layout.slice(42, 48).join(" ") + ).replace(/ /g, ""); + keymapStrings.keymap = Config.keymapLayout; + } +} + +$(document).keydown((e) => { + if (e.code === "ShiftLeft") { + leftState = true; + rightState = false; + } else if (e.code === "ShiftRight") { + leftState = false; + rightState = true; + } +}); + +$(document).keyup((e) => { + if (e.code === "ShiftLeft" || e.code === "ShiftRight") { + leftState = false; + rightState = false; + } +}); + +export function reset() { + leftState = false; + rightState = false; +} + +let leftSideKeys = [ + "KeyQ", + "KeyW", + "KeyE", + "KeyR", + "KeyT", + + "KeyA", + "KeyS", + "KeyD", + "KeyF", + "KeyG", + + "KeyZ", + "KeyX", + "KeyC", + "KeyV", + + "Backquote", + "Digit1", + "Digit2", + "Digit3", + "Digit4", + "Digit5", +]; + +let rightSideKeys = [ + "KeyU", + "KeyI", + "KeyO", + "KeyP", + + "KeyH", + "KeyJ", + "KeyK", + "KeyL", + + "KeyN", + "KeyM", + + "Digit7", + "Digit8", + "Digit9", + "Digit0", + + "Backslash", + "BracketLeft", + "BracketRight", + "Semicolon", + "Quote", + "Comma", + "Period", + "Slash", +]; + +export function isUsingOppositeShift(event) { + if (!leftState && !rightState) return null; + + if (Config.oppositeShiftMode === "on") { + if ( + !rightSideKeys.includes(event.code) && + !leftSideKeys.includes(event.code) + ) + return null; + + if ( + (leftState && rightSideKeys.includes(event.code)) || + (rightState && leftSideKeys.includes(event.code)) + ) { + return true; + } else { + return false; + } + } else if (Config.oppositeShiftMode === "keymap") { + buildKeymapStrings(); + + if (!keymapStrings.left || !keymapStrings.right) return null; + + if ( + (leftState && keymapStrings.right.includes(event.key)) || + (rightState && keymapStrings.left.includes(event.key)) + ) { + return true; + } else { + return false; + } + } +} + +==> ./monkeytype/src/js/test/keymap.js <== +import Config, * as UpdateConfig from "./config"; +import * as ThemeColors from "./theme-colors"; +import layouts from "./layouts"; +import * as CommandlineLists from "./commandline-lists"; +import * as Commandline from "./commandline"; +import * as TestTimer from "./test-timer"; + +export function highlightKey(currentKey) { + if (Config.mode === "zen") return; + try { + if ($(".active-key") != undefined) { + $(".active-key").removeClass("active-key"); + } + + let highlightKey; + switch (currentKey) { + case "\\": + case "|": + highlightKey = "#KeyBackslash"; + break; + case "}": + case "]": + highlightKey = "#KeyRightBracket"; + break; + case "{": + case "[": + highlightKey = "#KeyLeftBracket"; + break; + case '"': + case "'": + highlightKey = "#KeyQuote"; + break; + case ":": + case ";": + highlightKey = "#KeySemicolon"; + break; + case "<": + case ",": + highlightKey = "#KeyComma"; + break; + case ">": + case ".": + highlightKey = "#KeyPeriod"; + break; + case "?": + case "/": + highlightKey = "#KeySlash"; + break; + case "": + highlightKey = "#KeySpace"; + break; + default: + highlightKey = `#Key${currentKey}`; + } + + $(highlightKey).addClass("active-key"); + if (highlightKey === "#KeySpace") { + $("#KeySpace2").addClass("active-key"); + } + } catch (e) { + console.log("could not update highlighted keymap key: " + e.message); + } +} + +export async function flashKey(key, correct) { + if (key == undefined) return; + switch (key) { + case "\\": + case "|": + key = "#KeyBackslash"; + break; + case "}": + case "]": + key = "#KeyRightBracket"; + break; + case "{": + case "[": + key = "#KeyLeftBracket"; + break; + case '"': + case "'": + key = "#KeyQuote"; + break; + case ":": + case ";": + key = "#KeySemicolon"; + break; + case "<": + case ",": + key = "#KeyComma"; + break; + case ">": + case ".": + key = "#KeyPeriod"; + break; + case "?": + case "/": + key = "#KeySlash"; + break; + case "" || "Space": + key = "#KeySpace"; + break; + default: + key = `#Key${key.toUpperCase()}`; + } + + if (key == "#KeySpace") { + key = ".key-split-space"; + } + + let themecolors = await ThemeColors.get(); + + try { + if (correct || Config.blindMode) { + $(key) + .stop(true, true) + .css({ + color: themecolors.bg, + backgroundColor: themecolors.main, + borderColor: themecolors.main, + }) + .animate( + { + color: themecolors.sub, + backgroundColor: "transparent", + borderColor: themecolors.sub, + }, + TestTimer.slowTimer ? 0 : 500, + "easeOutExpo" + ); + } else { + $(key) + .stop(true, true) + .css({ + color: themecolors.bg, + backgroundColor: themecolors.error, + borderColor: themecolors.error, + }) + .animate( + { + color: themecolors.sub, + backgroundColor: "transparent", + borderColor: themecolors.sub, + }, + TestTimer.slowTimer ? 0 : 500, + "easeOutExpo" + ); + } + } catch (e) {} +} + +export function hide() { + $(".keymap").addClass("hidden"); +} + +export function show() { + $(".keymap").removeClass("hidden"); +} + +export function refreshKeys(layout) { + try { + let lts = layouts[layout]; //layout to show + let layoutString = layout; + if (Config.keymapLayout === "overrideSync") { + if (Config.layout === "default") { + lts = layouts["qwerty"]; + layoutString = "default"; + } else { + lts = layouts[Config.layout]; + layoutString = Config.layout; + } + } + + if (lts.keymapShowTopRow) { + $(".keymap .r1").removeClass("hidden"); + } else { + $(".keymap .r1").addClass("hidden"); + } + + if (Config.keymapStyle === "alice") { + $(".keymap .extraKey").removeClass("hidden"); + } else { + $(".keymap .extraKey").addClass("hidden"); + } + + $($(".keymap .r5 .keymap-key .letter")[0]).text( + layoutString.replace(/_/g, " ") + ); + + if (lts.iso) { + $(".keymap .r4 .keymap-key.first").removeClass("hidden-key"); + } else { + $(".keymap .r4 .keymap-key.first").addClass("hidden-key"); + } + + var toReplace = lts.keys.slice(1, 48); + var count = 0; + + // let repeatB = false; + $(".keymap .keymap-key .letter") + .map(function () { + if (count < toReplace.length) { + var key = toReplace[count].charAt(0); + this.innerHTML = key; + + switch (key) { + case "\\": + case "|": + this.parentElement.id = "KeyBackslash"; + break; + case "}": + case "]": + this.parentElement.id = "KeyRightBracket"; + break; + case "{": + case "[": + this.parentElement.id = "KeyLeftBracket"; + break; + case '"': + case "'": + this.parentElement.id = "KeyQuote"; + break; + case ":": + case ";": + this.parentElement.id = "KeySemicolon"; + break; + case "<": + case ",": + this.parentElement.id = "KeyComma"; + break; + case ">": + case ".": + this.parentElement.id = "KeyPeriod"; + break; + case "?": + case "/": + this.parentElement.id = "KeySlash"; + break; + case "": + this.parentElement.id = "KeySpace"; + break; + default: + this.parentElement.id = `Key${key.toUpperCase()}`; + } + } + + // if (count == 41 && !repeatB) { + // repeatB = true; + // }else{ + // repeatB = false; + // count++; + // } + + count++; + + // } + }) + .get(); + } catch (e) { + console.log( + "something went wrong when changing layout, resettings: " + e.message + ); + UpdateConfig.setKeymapLayout("qwerty", true); + } +} + +$(document).on("click", ".keymap .r5 #KeySpace", (e) => { + CommandlineLists.setCurrent([CommandlineLists.commandsKeymapLayouts]); + Commandline.show(); +}); + +==> ./monkeytype/src/js/test/wordset.js <== +import Config from "./config"; + +let currentWordset = null; +let currentWordGenerator = null; + +class Wordset { + constructor(words) { + this.words = words; + this.length = this.words.length; + } + + randomWord() { + return this.words[Math.floor(Math.random() * this.length)]; + } +} + +const prefixSize = 2; + +class CharDistribution { + constructor() { + this.chars = {}; + this.count = 0; + } + + addChar(char) { + this.count++; + if (char in this.chars) { + this.chars[char]++; + } else { + this.chars[char] = 1; + } + } + + randomChar() { + const randomIndex = Math.floor(Math.random() * this.count); + let runningCount = 0; + for (const [char, charCount] of Object.entries(this.chars)) { + runningCount += charCount; + if (runningCount > randomIndex) { + return char; + } + } + } +} + +class WordGenerator extends Wordset { + constructor(words) { + super(words); + // Can generate an unbounded number of words in theory. + this.length = Infinity; + + this.ngrams = {}; + for (let word of words) { + // Mark the end of each word with a space. + word += " "; + let prefix = ""; + for (const c of word) { + // Add `c` to the distribution of chars that can come after `prefix`. + if (!(prefix in this.ngrams)) { + this.ngrams[prefix] = new CharDistribution(); + } + this.ngrams[prefix].addChar(c); + prefix = (prefix + c).substr(-prefixSize); + } + } + } + + randomWord() { + let word = ""; + for (;;) { + const prefix = word.substr(-prefixSize); + let charDistribution = this.ngrams[prefix]; + if (!charDistribution) { + // This shouldn't happen if this.ngrams is complete. If it does + // somehow, start generating a new word. + word = ""; + continue; + } + // Pick a random char from the distribution that comes after `prefix`. + const nextChar = charDistribution.randomChar(); + if (nextChar == " ") { + // A space marks the end of the word, so stop generating and return. + break; + } + word += nextChar; + } + return word; + } +} + +export function withWords(words) { + if (Config.funbox == "pseudolang") { + if (currentWordGenerator == null || words !== currentWordGenerator.words) { + currentWordGenerator = new WordGenerator(words); + } + return currentWordGenerator; + } else { + if (currentWordset == null || words !== currentWordset.words) { + currentWordset = new Wordset(words); + } + return currentWordset; + } +} + +==> ./monkeytype/src/js/test/live-acc.js <== +import Config from "./config"; +import * as TestLogic from "./test-logic"; + +export function update(acc) { + let number = Math.floor(acc); + if (Config.blindMode) { + number = 100; + } + document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = number + "%"; + document.querySelector("#liveAcc").innerHTML = number + "%"; +} + +export function show() { + if (!Config.showLiveAcc) return; + if (!TestLogic.active) return; + if (Config.timerStyle === "mini") { + // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity); + if (!$("#miniTimerAndLiveWpm .acc").hasClass("hidden")) return; + $("#miniTimerAndLiveWpm .acc") + .removeClass("hidden") + .css("opacity", 0) + .animate( + { + opacity: Config.timerOpacity, + }, + 125 + ); + } else { + // $("#liveWpm").css("opacity", Config.timerOpacity); + if (!$("#liveAcc").hasClass("hidden")) return; + $("#liveAcc").removeClass("hidden").css("opacity", 0).animate( + { + opacity: Config.timerOpacity, + }, + 125 + ); + } +} + +export function hide() { + // $("#liveWpm").css("opacity", 0); + // $("#miniTimerAndLiveWpm .wpm").css("opacity", 0); + $("#liveAcc").animate( + { + opacity: Config.timerOpacity, + }, + 125, + () => { + $("#liveAcc").addClass("hidden"); + } + ); + $("#miniTimerAndLiveWpm .acc").animate( + { + opacity: Config.timerOpacity, + }, + 125, + () => { + $("#miniTimerAndLiveWpm .acc").addClass("hidden"); + } + ); +} + +==> ./monkeytype/src/js/test/weak-spot.js <== +import * as TestStats from "./test-stats"; + +// Changes how quickly it 'learns' scores - very roughly the score for a char +// is based on last perCharCount occurrences. Make it smaller to adjust faster. +const perCharCount = 50; + +// Choose the highest scoring word from this many random words. Higher values +// will choose words with more weak letters on average. +const wordSamples = 20; + +// Score penatly (in milliseconds) for getting a letter wrong. +const incorrectPenalty = 5000; + +let scores = {}; + +class Score { + constructor() { + this.average = 0.0; + this.count = 0; + } + + update(score) { + if (this.count < perCharCount) { + this.count++; + } + const adjustRate = 1.0 / this.count; + // Keep an exponential moving average of the score over time. + this.average = score * adjustRate + this.average * (1 - adjustRate); + } +} + +export function updateScore(char, isCorrect) { + const timings = TestStats.keypressTimings.spacing.array; + if (timings.length == 0) { + return; + } + let score = timings[timings.length - 1]; + if (!isCorrect) { + score += incorrectPenalty; + } + if (!(char in scores)) { + scores[char] = new Score(); + } + scores[char].update(score); +} + +function score(word) { + let total = 0.0; + let numChars = 0; + for (const c of word) { + if (c in scores) { + total += scores[c].average; + numChars++; + } + } + return numChars == 0 ? 0.0 : total / numChars; +} + +export function getWord(wordset) { + let highScore; + let randomWord; + for (let i = 0; i < wordSamples; i++) { + let newWord = wordset.randomWord(); + let newScore = score(newWord); + if (i == 0 || newScore > highScore) { + randomWord = newWord; + highScore = newScore; + } + } + return randomWord; +} + +==> ./monkeytype/src/js/test/tts.js <== +import Config from "./config"; +import * as Misc from "./misc"; + +let voice; + +export async function setLanguage(lang = Config.language) { + if (!voice) return; + let language = await Misc.getLanguage(lang); + let bcp = language.bcp47 ? language.bcp47 : "en-US"; + voice.lang = bcp; +} + +export async function init() { + voice = new SpeechSynthesisUtterance(); + setLanguage(); +} + +export function clear() { + voice = undefined; +} + +export function speak(text) { + if (!voice) init(); + voice.text = text; + window.speechSynthesis.cancel(); + window.speechSynthesis.speak(voice); +} + +==> ./monkeytype/src/js/test/test-logic.js <== +import * as TestUI from "./test-ui"; +import * as ManualRestart from "./manual-restart-tracker"; +import Config, * as UpdateConfig from "./config"; +import * as Misc from "./misc"; +import * as Notifications from "./notifications"; +import * as CustomText from "./custom-text"; +import * as TestStats from "./test-stats"; +import * as PractiseWords from "./practise-words"; +import * as ShiftTracker from "./shift-tracker"; +import * as Focus from "./focus"; +import * as Funbox from "./funbox"; +import * as Keymap from "./keymap"; +import * as ThemeController from "./theme-controller"; +import * as PaceCaret from "./pace-caret"; +import * as Caret from "./caret"; +import * as LiveWpm from "./live-wpm"; +import * as LiveAcc from "./live-acc"; +import * as LiveBurst from "./live-burst"; +import * as TimerProgress from "./timer-progress"; +import * as UI from "./ui"; +import * as QuoteSearchPopup from "./quote-search-popup"; +import * as QuoteSubmitPopup from "./quote-submit-popup"; +import * as PbCrown from "./pb-crown"; +import * as TestTimer from "./test-timer"; +import * as OutOfFocus from "./out-of-focus"; +import * as AccountButton from "./account-button"; +import * as DB from "./db"; +import * as Replay from "./replay.js"; +import axiosInstance from "./axios-instance"; +import * as MonkeyPower from "./monkey-power"; +import * as Poetry from "./poetry.js"; +import * as Wikipedia from "./wikipedia.js"; +import * as TodayTracker from "./today-tracker"; +import * as WeakSpot from "./weak-spot"; +import * as Wordset from "./wordset"; +import * as ChallengeContoller from "./challenge-controller"; +import * as RateQuotePopup from "./rate-quote-popup"; +import * as BritishEnglish from "./british-english"; +import * as LazyMode from "./lazy-mode"; +import * as Result from "./result"; + +const objecthash = require("object-hash"); + +export let glarsesMode = false; + +let failReason = ""; + +export function toggleGlarses() { + glarsesMode = true; + console.log( + "Glarses Mode On - test result will be hidden. You can check the stats in the console (here)" + ); + console.log("To disable Glarses Mode refresh the page."); +} + +export let notSignedInLastResult = null; + +export function clearNotSignedInResult() { + notSignedInLastResult = null; +} + +export function setNotSignedInUid(uid) { + notSignedInLastResult.uid = uid; + delete notSignedInLastResult.hash; + notSignedInLastResult.hash = objecthash(notSignedInLastResult); +} + +class Words { + constructor() { + this.list = []; + this.length = 0; + this.currentIndex = 0; + } + get(i, raw = false) { + if (i === undefined) { + return this.list; + } else { + if (raw) { + return this.list[i]?.replace(/[.?!":\-,]/g, "")?.toLowerCase(); + } else { + return this.list[i]; + } + } + } + getCurrent() { + return this.list[this.currentIndex]; + } + getLast() { + return this.list[this.list.length - 1]; + } + push(word) { + this.list.push(word); + this.length = this.list.length; + } + reset() { + this.list = []; + this.currentIndex = 0; + this.length = this.list.length; + } + resetCurrentIndex() { + this.currentIndex = 0; + } + decreaseCurrentIndex() { + this.currentIndex--; + } + increaseCurrentIndex() { + this.currentIndex++; + } + clean() { + for (let s of this.list) { + if (/ +/.test(s)) { + let id = this.list.indexOf(s); + let tempList = s.split(" "); + this.list.splice(id, 1); + for (let i = 0; i < tempList.length; i++) { + this.list.splice(id + i, 0, tempList[i]); + } + } + } + } +} + +class Input { + constructor() { + this.current = ""; + this.history = []; + this.length = 0; + } + + reset() { + this.current = ""; + this.history = []; + this.length = 0; + } + + resetHistory() { + this.history = []; + this.length = 0; + } + + setCurrent(val) { + this.current = val; + this.length = this.current.length; + } + + appendCurrent(val) { + this.current += val; + this.length = this.current.length; + } + + resetCurrent() { + this.current = ""; + } + + getCurrent() { + return this.current; + } + + pushHistory() { + this.history.push(this.current); + this.historyLength = this.history.length; + this.resetCurrent(); + } + + popHistory() { + return this.history.pop(); + } + + getHistory(i) { + if (i === undefined) { + return this.history; + } else { + return this.history[i]; + } + } + + getHistoryLast() { + return this.history[this.history.length - 1]; + } +} + +class Corrected { + constructor() { + this.current = ""; + this.history = []; + } + setCurrent(val) { + this.current = val; + } + + appendCurrent(val) { + this.current += val; + } + + resetCurrent() { + this.current = ""; + } + + resetHistory() { + this.history = []; + } + + reset() { + this.resetCurrent(); + this.resetHistory(); + } + + getHistory(i) { + return this.history[i]; + } + + popHistory() { + return this.history.pop(); + } + + pushHistory() { + this.history.push(this.current); + this.current = ""; + } +} + +export let active = false; +export let words = new Words(); +export let input = new Input(); +export let corrected = new Corrected(); +export let currentWordIndex = 0; +export let isRepeated = false; +export let isPaceRepeat = false; +export let lastTestWpm = 0; +export let hasTab = false; +export let randomQuote = null; +export let bailout = false; + +export function setActive(tf) { + active = tf; + if (!tf) MonkeyPower.reset(); +} + +export function setRepeated(tf) { + isRepeated = tf; +} + +export function setPaceRepeat(tf) { + isPaceRepeat = tf; +} + +export function setHasTab(tf) { + hasTab = tf; +} + +export function setBailout(tf) { + bailout = tf; +} + +export function setRandomQuote(rq) { + randomQuote = rq; +} + +let spanishSentenceTracker = ""; +export function punctuateWord(previousWord, currentWord, index, maxindex) { + let word = currentWord; + + let currentLanguage = Config.language.split("_")[0]; + + let lastChar = Misc.getLastChar(previousWord); + + if (Config.funbox === "58008") { + if (currentWord.length > 3) { + if (Math.random() < 0.75) { + let special = ["/", "*", "-", "+"][Math.floor(Math.random() * 4)]; + word = Misc.setCharAt(word, Math.floor(word.length / 2), special); + } + } + } else { + if ( + (index == 0 || lastChar == "." || lastChar == "?" || lastChar == "!") && + currentLanguage != "code" + ) { + //always capitalise the first word or if there was a dot unless using a code alphabet + + word = Misc.capitalizeFirstLetter(word); + + if (currentLanguage == "spanish" || currentLanguage == "catalan") { + let rand = Math.random(); + if (rand > 0.9) { + word = "¿" + word; + spanishSentenceTracker = "?"; + } else if (rand > 0.8) { + word = "¡" + word; + spanishSentenceTracker = "!"; + } + } + } else if ( + (Math.random() < 0.1 && + lastChar != "." && + lastChar != "," && + index != maxindex - 2) || + index == maxindex - 1 + ) { + if (currentLanguage == "spanish" || currentLanguage == "catalan") { + if (spanishSentenceTracker == "?" || spanishSentenceTracker == "!") { + word += spanishSentenceTracker; + spanishSentenceTracker = ""; + } + } else { + let rand = Math.random(); + if (rand <= 0.8) { + word += "."; + } else if (rand > 0.8 && rand < 0.9) { + if (currentLanguage == "french") { + word = "?"; + } else if ( + currentLanguage == "arabic" || + currentLanguage == "persian" || + currentLanguage == "urdu" + ) { + word += "؟"; + } else if (currentLanguage == "greek") { + word += ";"; + } else { + word += "?"; + } + } else { + if (currentLanguage == "french") { + word = "!"; + } else { + word += "!"; + } + } + } + } else if ( + Math.random() < 0.01 && + lastChar != "," && + lastChar != "." && + currentLanguage !== "russian" + ) { + word = `"${word}"`; + } else if ( + Math.random() < 0.011 && + lastChar != "," && + lastChar != "." && + currentLanguage !== "russian" && + currentLanguage !== "ukrainian" + ) { + word = `'${word}'`; + } else if (Math.random() < 0.012 && lastChar != "," && lastChar != ".") { + if (currentLanguage == "code") { + let r = Math.random(); + if (r < 0.25) { + word = `(${word})`; + } else if (r < 0.5) { + word = `{${word}}`; + } else if (r < 0.75) { + word = `[${word}]`; + } else { + word = `<${word}>`; + } + } else { + word = `(${word})`; + } + } else if ( + Math.random() < 0.013 && + lastChar != "," && + lastChar != "." && + lastChar != ";" && + lastChar != "؛" && + lastChar != ":" + ) { + if (currentLanguage == "french") { + word = ":"; + } else if (currentLanguage == "greek") { + word = "·"; + } else { + word += ":"; + } + } else if ( + Math.random() < 0.014 && + lastChar != "," && + lastChar != "." && + previousWord != "-" + ) { + word = "-"; + } else if ( + Math.random() < 0.015 && + lastChar != "," && + lastChar != "." && + lastChar != ";" && + lastChar != "؛" && + lastChar != ":" + ) { + if (currentLanguage == "french") { + word = ";"; + } else if (currentLanguage == "greek") { + word = "·"; + } else if (currentLanguage == "arabic") { + word += "؛"; + } else { + word += ";"; + } + } else if (Math.random() < 0.2 && lastChar != ",") { + if ( + currentLanguage == "arabic" || + currentLanguage == "urdu" || + currentLanguage == "persian" + ) { + word += "،"; + } else { + word += ","; + } + } else if (Math.random() < 0.25 && currentLanguage == "code") { + let specials = ["{", "}", "[", "]", "(", ")", ";", "=", "+", "%", "/"]; + + word = specials[Math.floor(Math.random() * 10)]; + } + } + return word; +} + +export function startTest() { + if (UI.pageTransition) { + return false; + } + if (!Config.dbConfigLoaded) { + UpdateConfig.setChangedBeforeDb(true); + } + try { + if (firebase.auth().currentUser != null) { + firebase.analytics().logEvent("testStarted"); + } else { + firebase.analytics().logEvent("testStartedNoLogin"); + } + } catch (e) { + console.log("Analytics unavailable"); + } + setActive(true); + Replay.startReplayRecording(); + Replay.replayGetWordsList(words.list); + TestStats.resetKeypressTimings(); + TimerProgress.restart(); + TimerProgress.show(); + $("#liveWpm").text("0"); + LiveWpm.show(); + LiveAcc.show(); + LiveBurst.show(); + TimerProgress.update(TestTimer.time); + TestTimer.clear(); + + if (Config.funbox === "memory") { + Funbox.resetMemoryTimer(); + $("#wordsWrapper").addClass("hidden"); + } + + try { + if (Config.paceCaret !== "off" || (Config.repeatedPace && isPaceRepeat)) + PaceCaret.start(); + } catch (e) {} + //use a recursive self-adjusting timer to avoid time drift + TestStats.setStart(performance.now()); + TestTimer.start(); + return true; +} + +export function restart( + withSameWordset = false, + nosave = false, + event, + practiseMissed = false +) { + if (TestUI.testRestarting || TestUI.resultCalculating) { + try { + event.preventDefault(); + } catch {} + return; + } + if (UI.getActivePage() == "pageTest" && !TestUI.resultVisible) { + if (!ManualRestart.get()) { + if (hasTab) { + try { + if (!event.shiftKey) return; + } catch {} + } + try { + if (Config.mode !== "zen") event.preventDefault(); + } catch {} + if ( + !Misc.canQuickRestart( + Config.mode, + Config.words, + Config.time, + CustomText + ) + ) { + let message = "Use your mouse to confirm."; + if (Config.quickTab) + message = "Press shift + tab or use your mouse to confirm."; + Notifications.add("Quick restart disabled. " + message, 0, 3); + return; + } + // }else{ + // return; + // } + } + } + if (active) { + TestStats.pushKeypressesToHistory(); + let testSeconds = TestStats.calculateTestSeconds(performance.now()); + let afkseconds = TestStats.calculateAfkSeconds(testSeconds); + // incompleteTestSeconds += ; + let tt = testSeconds - afkseconds; + if (tt < 0) tt = 0; + console.log( + `increasing incomplete time by ${tt}s (${testSeconds}s - ${afkseconds}s afk)` + ); + TestStats.incrementIncompleteSeconds(tt); + TestStats.incrementRestartCount(); + if (tt > 600) { + Notifications.add( + `Your time typing just increased by ${Misc.roundTo2( + tt / 60 + )} minutes. If you think this is incorrect please contact Miodec and dont refresh the website.`, + -1 + ); + } + // restartCount++; + } + + if (Config.mode == "zen") { + $("#words").empty(); + } + + if ( + PractiseWords.before.mode !== null && + !withSameWordset && + !practiseMissed + ) { + Notifications.add("Reverting to previous settings.", 0); + UpdateConfig.setPunctuation(PractiseWords.before.punctuation); + UpdateConfig.setNumbers(PractiseWords.before.numbers); + UpdateConfig.setMode(PractiseWords.before.mode); + PractiseWords.resetBefore(); + } + + let repeatWithPace = false; + if (TestUI.resultVisible && Config.repeatedPace && withSameWordset) { + repeatWithPace = true; + } + + ManualRestart.reset(); + TestTimer.clear(); + TestStats.restart(); + corrected.reset(); + ShiftTracker.reset(); + Caret.hide(); + setActive(false); + Replay.stopReplayRecording(); + LiveWpm.hide(); + LiveAcc.hide(); + LiveBurst.hide(); + TimerProgress.hide(); + Replay.pauseReplay(); + setBailout(false); + PaceCaret.reset(); + $("#showWordHistoryButton").removeClass("loaded"); + $("#restartTestButton").blur(); + Funbox.resetMemoryTimer(); + RateQuotePopup.clearQuoteStats(); + if (UI.getActivePage() == "pageTest" && window.scrollY > 0) + window.scrollTo({ top: 0, behavior: "smooth" }); + $("#wordsInput").val(" "); + + TestUI.reset(); + + $("#timerNumber").css("opacity", 0); + let el = null; + if (TestUI.resultVisible) { + //results are being displayed + el = $("#result"); + } else { + //words are being displayed + el = $("#typingTest"); + } + if (TestUI.resultVisible) { + if ( + Config.randomTheme !== "off" && + !UI.pageTransition && + !Config.customTheme + ) { + ThemeController.randomizeTheme(); + } + } + TestUI.setResultVisible(false); + UI.setPageTransition(true); + TestUI.setTestRestarting(true); + el.stop(true, true).animate( + { + opacity: 0, + }, + 125, + async () => { + if (UI.getActivePage() == "pageTest") Focus.set(false); + TestUI.focusWords(); + $("#monkey .fast").stop(true, true).css("opacity", 0); + $("#monkey").stop(true, true).css({ animationDuration: "0s" }); + $("#typingTest").css("opacity", 0).removeClass("hidden"); + $("#wordsInput").val(" "); + let shouldQuoteRepeat = false; + if ( + Config.mode === "quote" && + Config.repeatQuotes === "typing" && + failReason !== "" + ) { + shouldQuoteRepeat = true; + } + if (Config.funbox === "arrows") { + UpdateConfig.setPunctuation(false, true); + UpdateConfig.setNumbers(false, true); + } else if (Config.funbox === "58008") { + UpdateConfig.setNumbers(false, true); + } else if (Config.funbox === "specials") { + UpdateConfig.setPunctuation(false, true); + UpdateConfig.setNumbers(false, true); + } else if (Config.funbox === "ascii") { + UpdateConfig.setPunctuation(false, true); + UpdateConfig.setNumbers(false, true); + } + if (!withSameWordset && !shouldQuoteRepeat) { + setRepeated(false); + setPaceRepeat(repeatWithPace); + setHasTab(false); + await init(); + PaceCaret.init(nosave); + } else { + setRepeated(true); + setPaceRepeat(repeatWithPace); + setActive(false); + Replay.stopReplayRecording(); + words.resetCurrentIndex(); + input.reset(); + if (Config.funbox === "plus_one" || Config.funbox === "plus_two") { + Notifications.add( + "Sorry, this funbox won't work with repeated tests.", + 0 + ); + await Funbox.activate("none"); + } else { + await Funbox.activate(); + } + TestUI.showWords(); + PaceCaret.init(); + } + failReason = ""; + if (Config.mode === "quote") { + setRepeated(false); + } + if (Config.keymapMode !== "off") { + Keymap.show(); + } else { + Keymap.hide(); + } + document.querySelector("#miniTimerAndLiveWpm .wpm").innerHTML = "0"; + document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = "100%"; + document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = "0"; + document.querySelector("#liveWpm").innerHTML = "0"; + document.querySelector("#liveAcc").innerHTML = "100%"; + document.querySelector("#liveBurst").innerHTML = "0"; + + if (Config.funbox === "memory") { + Funbox.startMemoryTimer(); + if (Config.keymapMode === "next") { + UpdateConfig.setKeymapMode("react"); + } + } + + let mode2 = Misc.getMode2(); + let fbtext = ""; + if (Config.funbox !== "none") { + fbtext = " " + Config.funbox; + } + $(".pageTest #premidTestMode").text( + `${Config.mode} ${mode2} ${Config.language.replace(/_/g, " ")}${fbtext}` + ); + $(".pageTest #premidSecondsLeft").text(Config.time); + + if (Config.funbox === "layoutfluid") { + UpdateConfig.setLayout( + Config.customLayoutfluid + ? Config.customLayoutfluid.split("#")[0] + : "qwerty", + true + ); + UpdateConfig.setKeymapLayout( + Config.customLayoutfluid + ? Config.customLayoutfluid.split("#")[0] + : "qwerty", + true + ); + Keymap.highlightKey( + words + .getCurrent() + .substring(input.current.length, input.current.length + 1) + .toString() + .toUpperCase() + ); + } + + $("#result").addClass("hidden"); + $("#testModesNotice").removeClass("hidden").css({ + opacity: 1, + }); + // resetPaceCaret(); + $("#typingTest") + .css("opacity", 0) + .removeClass("hidden") + .stop(true, true) + .animate( + { + opacity: 1, + }, + 125, + () => { + TestUI.setTestRestarting(false); + // resetPaceCaret(); + PbCrown.hide(); + TestTimer.clear(); + if ($("#commandLineWrapper").hasClass("hidden")) + TestUI.focusWords(); + // ChartController.result.update(); + TestUI.updateModesNotice(); + UI.setPageTransition(false); + // console.log(TestStats.incompleteSeconds); + // console.log(TestStats.restartCount); + } + ); + } + ); +} + +async function getNextWord(wordset, language, wordsBound) { + let randomWord = wordset.randomWord(); + const previousWord = words.get(words.length - 1, true); + const previousWord2 = words.get(words.length - 2, true); + if (Config.mode === "quote") { + randomWord = randomQuote.textSplit[words.length]; + } else if ( + Config.mode == "custom" && + !CustomText.isWordRandom && + !CustomText.isTimeRandom + ) { + randomWord = CustomText.text[words.length]; + } else if ( + Config.mode == "custom" && + (CustomText.isWordRandom || CustomText.isTimeRandom) && + (wordset.length < 3 || PractiseWords.before.mode !== null) + ) { + randomWord = wordset.randomWord(); + } else { + let regenarationCount = 0; //infinite loop emergency stop button + while ( + regenarationCount < 100 && + (previousWord == randomWord || + previousWord2 == randomWord || + (!Config.punctuation && randomWord == "I")) + ) { + regenarationCount++; + randomWord = wordset.randomWord(); + } + } + + if (randomWord === undefined) { + randomWord = wordset.randomWord(); + } + + if (Config.lazyMode === true && !language.noLazyMode) { + randomWord = LazyMode.replaceAccents(randomWord, language.accents); + } + + randomWord = randomWord.replace(/ +/gm, " "); + randomWord = randomWord.replace(/^ | $/gm, ""); + + if (Config.funbox === "rAnDoMcAsE") { + let randomcaseword = ""; + for (let i = 0; i < randomWord.length; i++) { + if (i % 2 != 0) { + randomcaseword += randomWord[i].toUpperCase(); + } else { + randomcaseword += randomWord[i]; + } + } + randomWord = randomcaseword; + } else if (Config.funbox === "capitals") { + randomWord = Misc.capitalizeFirstLetter(randomWord); + } else if (Config.funbox === "gibberish") { + randomWord = Misc.getGibberish(); + } else if (Config.funbox === "arrows") { + randomWord = Misc.getArrows(); + } else if (Config.funbox === "58008") { + randomWord = Misc.getNumbers(7); + } else if (Config.funbox === "specials") { + randomWord = Misc.getSpecials(); + } else if (Config.funbox === "ascii") { + randomWord = Misc.getASCII(); + } else if (Config.funbox === "weakspot") { + randomWord = WeakSpot.getWord(wordset); + } + + if (Config.punctuation) { + randomWord = punctuateWord( + words.get(words.length - 1), + randomWord, + words.length, + wordsBound + ); + } + if (Config.numbers) { + if (Math.random() < 0.1) { + randomWord = Misc.getNumbers(4); + } + } + + if (Config.britishEnglish && /english/.test(Config.language)) { + randomWord = await BritishEnglish.replace(randomWord); + } + + return randomWord; +} + +export async function init() { + setActive(false); + Replay.stopReplayRecording(); + words.reset(); + TestUI.setCurrentWordElementIndex(0); + // accuracy = { + // correct: 0, + // incorrect: 0, + // }; + + input.resetHistory(); + input.resetCurrent(); + + let language = await Misc.getLanguage(Config.language); + if (language && language.name !== Config.language) { + UpdateConfig.setLanguage("english"); + } + + if (!language) { + UpdateConfig.setLanguage("english"); + language = await Misc.getLanguage(Config.language); + } + + if (Config.lazyMode === true && language.noLazyMode) { + Notifications.add("This language does not support lazy mode.", 0); + UpdateConfig.setLazyMode(false); + } + + let wordsBound = 100; + if (Config.showAllLines) { + if (Config.mode === "quote") { + wordsBound = 100; + } else if (Config.mode === "custom") { + if (CustomText.isWordRandom) { + wordsBound = CustomText.word; + } else if (CustomText.isTimeRandom) { + wordsBound = 100; + } else { + wordsBound = CustomText.text.length; + } + } else if (Config.mode != "time") { + wordsBound = Config.words; + } + } else { + if (Config.mode === "words" && Config.words < wordsBound) { + wordsBound = Config.words; + } + if ( + Config.mode == "custom" && + CustomText.isWordRandom && + CustomText.word < wordsBound + ) { + wordsBound = CustomText.word; + } + if (Config.mode == "custom" && CustomText.isTimeRandom) { + wordsBound = 100; + } + if ( + Config.mode == "custom" && + !CustomText.isWordRandom && + !CustomText.isTimeRandom && + CustomText.text.length < wordsBound + ) { + wordsBound = CustomText.text.length; + } + } + + if ( + (Config.mode === "custom" && + CustomText.isWordRandom && + CustomText.word == 0) || + (Config.mode === "custom" && + CustomText.isTimeRandom && + CustomText.time == 0) + ) { + wordsBound = 100; + } + + if (Config.mode === "words" && Config.words === 0) { + wordsBound = 100; + } + if (Config.funbox === "plus_one") { + wordsBound = 2; + if (Config.mode === "words" && Config.words < wordsBound) { + wordsBound = Config.words; + } + } + if (Config.funbox === "plus_two") { + wordsBound = 3; + if (Config.mode === "words" && Config.words < wordsBound) { + wordsBound = Config.words; + } + } + + if ( + Config.mode == "time" || + Config.mode == "words" || + Config.mode == "custom" + ) { + let wordList = language.words; + if (Config.mode == "custom") { + wordList = CustomText.text; + } + const wordset = Wordset.withWords(wordList); + + if ( + (Config.funbox == "wikipedia" || Config.funbox == "poetry") && + Config.mode != "custom" + ) { + let wordCount = 0; + + // If mode is words, get as many sections as you need until the wordCount is fullfilled + while ( + (Config.mode == "words" && Config.words >= wordCount) || + (Config.mode === "time" && wordCount < 100) + ) { + let section = + Config.funbox == "wikipedia" + ? await Wikipedia.getSection() + : await Poetry.getPoem(); + for (let word of section.words) { + if (wordCount >= Config.words && Config.mode == "words") { + wordCount++; + break; + } + wordCount++; + words.push(word); + } + } + } else { + for (let i = 0; i < wordsBound; i++) { + let randomWord = await getNextWord(wordset, language, wordsBound); + + if (/t/g.test(randomWord)) { + setHasTab(true); + } + + if (/ +/.test(randomWord)) { + let randomList = randomWord.split(" "); + let id = 0; + while (id < randomList.length) { + words.push(randomList[id]); + id++; + + if ( + words.length == wordsBound && + Config.mode == "custom" && + CustomText.isWordRandom + ) { + break; + } + } + if ( + Config.mode == "custom" && + !CustomText.isWordRandom && + !CustomText.isTimeRandom + ) { + // + } else { + i = words.length - 1; + } + } else { + words.push(randomWord); + } + } + } + } else if (Config.mode == "quote") { + // setLanguage(Config.language.replace(/_\d*k$/g, ""), true); + + let quotes = await Misc.getQuotes(Config.language.replace(/_\d*k$/g, "")); + + if (quotes.length === 0) { + TestUI.setTestRestarting(false); + Notifications.add( + `No ${Config.language.replace(/_\d*k$/g, "")} quotes found`, + 0 + ); + if (firebase.auth().currentUser) { + QuoteSubmitPopup.show(false); + } + UpdateConfig.setMode("words"); + restart(); + return; + } + + let rq; + if (Config.quoteLength != -2) { + let quoteLengths = Config.quoteLength; + let groupIndex; + if (quoteLengths.length > 1) { + groupIndex = + quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; + while (quotes.groups[groupIndex].length === 0) { + groupIndex = + quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; + } + } else { + groupIndex = quoteLengths[0]; + if (quotes.groups[groupIndex].length === 0) { + Notifications.add("No quotes found for selected quote length", 0); + TestUI.setTestRestarting(false); + return; + } + } + + rq = + quotes.groups[groupIndex][ + Math.floor(Math.random() * quotes.groups[groupIndex].length) + ]; + if (randomQuote != null && rq.id === randomQuote.id) { + rq = + quotes.groups[groupIndex][ + Math.floor(Math.random() * quotes.groups[groupIndex].length) + ]; + } + } else { + quotes.groups.forEach((group) => { + let filtered = group.filter( + (quote) => quote.id == QuoteSearchPopup.selectedId + ); + if (filtered.length > 0) { + rq = filtered[0]; + } + }); + if (rq == undefined) { + rq = quotes.groups[0][0]; + Notifications.add("Quote Id Does Not Exist", 0); + } + } + rq.text = rq.text.replace(/ +/gm, " "); + rq.text = rq.text.replace(/t/gm, "t"); + rq.text = rq.text.replace(/\\\\n/gm, "\n"); + rq.text = rq.text.replace(/t/gm, "t"); + rq.text = rq.text.replace(/\\n/gm, "\n"); + rq.text = rq.text.replace(/( *(\r\n|\r|\n) *)/g, "\n "); + rq.text = rq.text.replace(/…/g, "..."); + rq.text = rq.text.trim(); + rq.textSplit = rq.text.split(" "); + rq.language = Config.language.replace(/_\d*k$/g, ""); + + setRandomQuote(rq); + + let w = randomQuote.textSplit; + + wordsBound = Math.min(wordsBound, w.length); + + for (let i = 0; i < wordsBound; i++) { + if (/t/g.test(w[i])) { + setHasTab(true); + } + if ( + Config.britishEnglish && + Config.language.replace(/_\d*k$/g, "") === "english" + ) { + w[i] = await BritishEnglish.replace(w[i]); + } + + if (Config.lazyMode === true && !language.noLazyMode) { + w[i] = LazyMode.replaceAccents(w[i], language.accents); + } + + words.push(w[i]); + } + } + //handle right-to-left languages + if (language.leftToRight) { + TestUI.arrangeCharactersLeftToRight(); + } else { + TestUI.arrangeCharactersRightToLeft(); + } + if (language.ligatures) { + $("#words").addClass("withLigatures"); + $("#resultWordsHistory .words").addClass("withLigatures"); + $("#resultReplay .words").addClass("withLigatures"); + } else { + $("#words").removeClass("withLigatures"); + $("#resultWordsHistory .words").removeClass("withLigatures"); + $("#resultReplay .words").removeClass("withLigatures"); + } + // if (Config.mode == "zen") { + // // Creating an empty active word element for zen mode + // $("#words").append('
'); + // $("#words").css("height", "auto"); + // $("#wordsWrapper").css("height", "auto"); + // } else { + if (UI.getActivePage() == "pageTest") { + await Funbox.activate(); + } + TestUI.showWords(); + // } +} + +export function calculateWpmAndRaw() { + let chars = 0; + let correctWordChars = 0; + let spaces = 0; + //check input history + for (let i = 0; i < input.history.length; i++) { + let word = Config.mode == "zen" ? input.getHistory(i) : words.get(i); + if (input.getHistory(i) == word) { + //the word is correct + //+1 for space + correctWordChars += word.length; + if ( + i < input.history.length - 1 && + Misc.getLastChar(input.getHistory(i)) !== "\n" + ) { + spaces++; + } + } + chars += input.getHistory(i).length; + } + if (input.current !== "") { + let word = Config.mode == "zen" ? input.current : words.getCurrent(); + //check whats currently typed + let toAdd = { + correct: 0, + incorrect: 0, + missed: 0, + }; + for (let c = 0; c < word.length; c++) { + if (c < input.current.length) { + //on char that still has a word list pair + if (input.current[c] == word[c]) { + toAdd.correct++; + } else { + toAdd.incorrect++; + } + } else { + //on char that is extra + toAdd.missed++; + } + } + chars += toAdd.correct; + chars += toAdd.incorrect; + chars += toAdd.missed; + if (toAdd.incorrect == 0) { + //word is correct so far, add chars + correctWordChars += toAdd.correct; + } + } + if (Config.funbox === "nospace" || Config.funbox === "arrows") { + spaces = 0; + } + chars += input.current.length; + let testSeconds = TestStats.calculateTestSeconds(performance.now()); + let wpm = Math.round(((correctWordChars + spaces) * (60 / testSeconds)) / 5); + let raw = Math.round(((chars + spaces) * (60 / testSeconds)) / 5); + return { + wpm: wpm, + raw: raw, + }; +} + +export async function addWord() { + let bound = 100; + if (Config.funbox === "wikipedia" || Config.funbox == "poetry") { + if (Config.mode == "time" && words.length - words.currentIndex < 20) { + let section = + Config.funbox == "wikipedia" + ? await Wikipedia.getSection() + : await Poetry.getPoem(); + let wordCount = 0; + for (let word of section.words) { + if (wordCount >= Config.words && Config.mode == "words") { + break; + } + wordCount++; + words.push(word); + TestUI.addWord(word); + } + } else { + return; + } + } + + if (Config.funbox === "plus_one") bound = 1; + if (Config.funbox === "plus_two") bound = 2; + if ( + words.length - input.history.length > bound || + (Config.mode === "words" && + words.length >= Config.words && + Config.words > 0) || + (Config.mode === "custom" && + CustomText.isWordRandom && + words.length >= CustomText.word && + CustomText.word != 0) || + (Config.mode === "custom" && + !CustomText.isWordRandom && + !CustomText.isTimeRandom && + words.length >= CustomText.text.length) || + (Config.mode === "quote" && words.length >= randomQuote.textSplit.length) + ) + return; + const language = + Config.mode !== "custom" + ? await Misc.getCurrentLanguage() + : { + //borrow the direction of the current language + leftToRight: await Misc.getCurrentLanguage().leftToRight, + words: CustomText.text, + }; + const wordset = Wordset.withWords(language.words); + + let randomWord = await getNextWord(wordset, language, bound); + + let split = randomWord.split(" "); + if (split.length > 1) { + split.forEach((word) => { + words.push(word); + TestUI.addWord(word); + }); + } else { + words.push(randomWord); + TestUI.addWord(randomWord); + } +} + +var retrySaving = { + completedEvent: null, + canRetry: false, +}; + +export function retrySavingResult() { + if (!retrySaving.completedEvent) { + Notifications.add( + "Could not retry saving the result as the result no longer exists.", + 0, + -1 + ); + } + if (!retrySaving.canRetry) { + return; + } + + retrySaving.canRetry = false; + $("#retrySavingResultButton").addClass("hidden"); + + AccountButton.loading(true); + + Notifications.add("Retrying to save..."); + + var { completedEvent } = retrySaving; + + axiosInstance + .post("/results/add", { + result: completedEvent, + }) + .then((response) => { + AccountButton.loading(false); + Result.hideCrown(); + + if (response.status !== 200) { + Notifications.add("Result not saved. " + response.data.message, -1); + } else { + completedEvent._id = response.data.insertedId; + if (response.data.isPb) { + completedEvent.isPb = true; + } + + DB.saveLocalResult(completedEvent); + DB.updateLocalStats({ + time: + completedEvent.testDuration + + completedEvent.incompleteTestSeconds - + completedEvent.afkDuration, + started: TestStats.restartCount + 1, + }); + + try { + firebase.analytics().logEvent("testCompleted", completedEvent); + } catch (e) { + console.log("Analytics unavailable"); + } + + if (response.data.isPb) { + //new pb + Result.showCrown(); + Result.updateCrown(); + DB.saveLocalPB( + Config.mode, + completedEvent.mode2, + Config.punctuation, + Config.language, + Config.difficulty, + Config.lazyMode, + completedEvent.wpm, + completedEvent.acc, + completedEvent.rawWpm, + completedEvent.consistency + ); + } + } + + $("#retrySavingResultButton").addClass("hidden"); + Notifications.add("Result saved", 1); + }) + .catch((e) => { + AccountButton.loading(false); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to save result: " + msg, -1); + $("#retrySavingResultButton").removeClass("hidden"); + retrySaving.canRetry = true; + }); +} + +function buildCompletedEvent(difficultyFailed) { + //build completed event object + let completedEvent = { + wpm: undefined, + rawWpm: undefined, + charStats: undefined, + acc: undefined, + mode: Config.mode, + mode2: undefined, + quoteLength: -1, + punctuation: Config.punctuation, + numbers: Config.numbers, + lazyMode: Config.lazyMode, + timestamp: Date.now(), + language: Config.language, + restartCount: TestStats.restartCount, + incompleteTestSeconds: + TestStats.incompleteSeconds < 0 + ? 0 + : Misc.roundTo2(TestStats.incompleteSeconds), + difficulty: Config.difficulty, + blindMode: Config.blindMode, + tags: undefined, + keySpacing: TestStats.keypressTimings.spacing.array, + keyDuration: TestStats.keypressTimings.duration.array, + consistency: undefined, + keyConsistency: undefined, + funbox: Config.funbox, + bailedOut: bailout, + chartData: { + wpm: TestStats.wpmHistory, + raw: undefined, + err: undefined, + }, + customText: undefined, + testDuration: undefined, + afkDuration: undefined, + }; + + // stats + let stats = TestStats.calculateStats(); + if (stats.time % 1 != 0 && Config.mode !== "time") { + TestStats.setLastSecondNotRound(); + } + lastTestWpm = stats.wpm; + completedEvent.wpm = stats.wpm; + completedEvent.rawWpm = stats.wpmRaw; + completedEvent.charStats = [ + stats.correctChars + stats.correctSpaces, + stats.incorrectChars, + stats.extraChars, + stats.missedChars, + ]; + completedEvent.acc = stats.acc; + + // if the last second was not rounded, add another data point to the history + if (TestStats.lastSecondNotRound && !difficultyFailed) { + let wpmAndRaw = calculateWpmAndRaw(); + TestStats.pushToWpmHistory(wpmAndRaw.wpm); + TestStats.pushToRawHistory(wpmAndRaw.raw); + TestStats.pushKeypressesToHistory(); + } + + //consistency + let rawPerSecond = TestStats.keypressPerSecond.map((f) => + Math.round((f.count / 5) * 60) + ); + let stddev = Misc.stdDev(rawPerSecond); + let avg = Misc.mean(rawPerSecond); + let consistency = Misc.roundTo2(Misc.kogasa(stddev / avg)); + let keyconsistencyarray = TestStats.keypressTimings.spacing.array.slice(); + keyconsistencyarray = keyconsistencyarray.splice( + 0, + keyconsistencyarray.length - 1 + ); + let keyConsistency = Misc.roundTo2( + Misc.kogasa( + Misc.stdDev(keyconsistencyarray) / Misc.mean(keyconsistencyarray) + ) + ); + if (isNaN(consistency)) { + consistency = 0; + } + completedEvent.keyConsistency = keyConsistency; + completedEvent.consistency = consistency; + let smoothedraw = Misc.smooth(rawPerSecond, 1); + completedEvent.chartData.raw = smoothedraw; + completedEvent.chartData.unsmoothedRaw = rawPerSecond; + + //smoothed consistency + let stddev2 = Misc.stdDev(smoothedraw); + let avg2 = Misc.mean(smoothedraw); + let smoothConsistency = Misc.roundTo2(Misc.kogasa(stddev2 / avg2)); + completedEvent.smoothConsistency = smoothConsistency; + + //wpm consistency + let stddev3 = Misc.stdDev(completedEvent.chartData.wpm); + let avg3 = Misc.mean(completedEvent.chartData.wpm); + let wpmConsistency = Misc.roundTo2(Misc.kogasa(stddev3 / avg3)); + completedEvent.wpmConsistency = wpmConsistency; + + completedEvent.testDuration = parseFloat(stats.time); + completedEvent.afkDuration = TestStats.calculateAfkSeconds( + completedEvent.testDuration + ); + + completedEvent.chartData.err = []; + for (let i = 0; i < TestStats.keypressPerSecond.length; i++) { + completedEvent.chartData.err.push(TestStats.keypressPerSecond[i].errors); + } + + if (Config.mode === "quote") { + completedEvent.quoteLength = randomQuote.group; + completedEvent.lang = Config.language.replace(/_\d*k$/g, ""); + } + + completedEvent.mode2 = Misc.getMode2(); + + if (Config.mode === "custom") { + completedEvent.customText = {}; + completedEvent.customText.textLen = CustomText.text.length; + completedEvent.customText.isWordRandom = CustomText.isWordRandom; + completedEvent.customText.isTimeRandom = CustomText.isTimeRandom; + completedEvent.customText.word = + CustomText.word !== "" && !isNaN(CustomText.word) + ? CustomText.word + : null; + completedEvent.customText.time = + CustomText.time !== "" && !isNaN(CustomText.time) + ? CustomText.time + : null; + } else { + delete completedEvent.customText; + } + + //tags + let activeTagsIds = []; + try { + DB.getSnapshot().tags.forEach((tag) => { + if (tag.active === true) { + activeTagsIds.push(tag._id); + } + }); + } catch (e) {} + completedEvent.tags = activeTagsIds; + + if (completedEvent.mode != "custom") delete completedEvent.customText; + + return completedEvent; +} + +export async function finish(difficultyFailed = false) { + if (!active) return; + if (Config.mode == "zen" && input.current.length != 0) { + input.pushHistory(); + corrected.pushHistory(); + Replay.replayGetWordsList(input.history); + } + + TestStats.recordKeypressSpacing(); //this is needed in case there is afk time at the end - to make sure test duration makes sense + + TestUI.setResultCalculating(true); + TestUI.setResultVisible(true); + TestStats.setEnd(performance.now()); + setActive(false); + Replay.stopReplayRecording(); + Focus.set(false); + Caret.hide(); + LiveWpm.hide(); + PbCrown.hide(); + LiveAcc.hide(); + LiveBurst.hide(); + TimerProgress.hide(); + OutOfFocus.hide(); + TestTimer.clear(); + Funbox.activate("none", null); + + //need one more calculation for the last word if test auto ended + if (TestStats.burstHistory.length !== input.getHistory().length) { + let burst = TestStats.calculateBurst(); + TestStats.pushBurstToHistory(burst); + } + + //remove afk from zen + if (Config.mode == "zen" || bailout) { + TestStats.removeAfkData(); + } + + const completedEvent = buildCompletedEvent(difficultyFailed); + + //todo check if any fields are undefined + + ///////// completed event ready + + //afk check + let kps = TestStats.keypressPerSecond.slice(-5); + let afkDetected = kps.every((second) => second.afk); + if (bailout) afkDetected = false; + + let tooShort = false; + let dontSave = false; + //fail checks + if (difficultyFailed) { + Notifications.add(`Test failed - ${failReason}`, 0, 1); + dontSave = true; + } else if (afkDetected) { + Notifications.add("Test invalid - AFK detected", 0); + dontSave = true; + } else if (isRepeated) { + Notifications.add("Test invalid - repeated", 0); + dontSave = true; + } else if ( + (Config.mode === "time" && + completedEvent.mode2 < 15 && + completedEvent.mode2 > 0) || + (Config.mode === "time" && + completedEvent.mode2 == 0 && + completedEvent.testDuration < 15) || + (Config.mode === "words" && + completedEvent.mode2 < 10 && + completedEvent.mode2 > 0) || + (Config.mode === "words" && + completedEvent.mode2 == 0 && + completedEvent.testDuration < 15) || + (Config.mode === "custom" && + !CustomText.isWordRandom && + !CustomText.isTimeRandom && + CustomText.text.length < 10) || + (Config.mode === "custom" && + CustomText.isWordRandom && + !CustomText.isTimeRandom && + CustomText.word < 10) || + (Config.mode === "custom" && + !CustomText.isWordRandom && + CustomText.isTimeRandom && + CustomText.time < 15) || + (Config.mode === "zen" && completedEvent.testDuration < 15) + ) { + Notifications.add("Test invalid - too short", 0); + tooShort = true; + dontSave = true; + } else if (completedEvent.wpm < 0 || completedEvent.wpm > 350) { + Notifications.add("Test invalid - wpm", 0); + TestStats.setInvalid(); + dontSave = true; + } else if (completedEvent.acc < 75 || completedEvent.acc > 100) { + Notifications.add("Test invalid - accuracy", 0); + TestStats.setInvalid(); + dontSave = true; + } + + // test is valid + + if (!dontSave) { + TodayTracker.addSeconds( + completedEvent.testDuration + + (TestStats.incompleteSeconds < 0 + ? 0 + : Misc.roundTo2(TestStats.incompleteSeconds)) - + completedEvent.afkDuration + ); + Result.updateTodayTracker(); + } + + if (firebase.auth().currentUser == null) { + $(".pageTest #result #rateQuoteButton").addClass("hidden"); + try { + firebase.analytics().logEvent("testCompletedNoLogin", completedEvent); + } catch (e) { + console.log("Analytics unavailable"); + } + notSignedInLastResult = completedEvent; + dontSave = true; + } + + Result.update( + completedEvent, + difficultyFailed, + failReason, + afkDetected, + isRepeated, + tooShort, + randomQuote, + dontSave + ); + + delete completedEvent.chartData.unsmoothedRaw; + + if (completedEvent.testDuration > 122) { + completedEvent.chartData = "toolong"; + completedEvent.keySpacing = "toolong"; + completedEvent.keyDuration = "toolong"; + TestStats.setKeypressTimingsTooLong(); + } + + if (dontSave) { + try { + firebase.analytics().logEvent("testCompletedInvalid", completedEvent); + } catch (e) { + console.log("Analytics unavailable"); + } + return; + } + + // user is logged in + + if ( + Config.difficulty == "normal" || + ((Config.difficulty == "master" || Config.difficulty == "expert") && + !difficultyFailed) + ) { + TestStats.resetIncomplete(); + } + + completedEvent.uid = firebase.auth().currentUser.uid; + Result.updateRateQuote(randomQuote); + + Result.updateGraphPBLine(); + + AccountButton.loading(true); + completedEvent.challenge = ChallengeContoller.verify(completedEvent); + if (!completedEvent.challenge) delete completedEvent.challenge; + completedEvent.hash = objecthash(completedEvent); + axiosInstance + .post("/results/add", { + result: completedEvent, + }) + .then((response) => { + AccountButton.loading(false); + Result.hideCrown(); + + if (response.status !== 200) { + Notifications.add("Result not saved. " + response.data.message, -1); + } else { + completedEvent._id = response.data.insertedId; + if (response.data.isPb) { + completedEvent.isPb = true; + } + + DB.saveLocalResult(completedEvent); + DB.updateLocalStats({ + time: + completedEvent.testDuration + + completedEvent.incompleteTestSeconds - + completedEvent.afkDuration, + started: TestStats.restartCount + 1, + }); + + try { + firebase.analytics().logEvent("testCompleted", completedEvent); + } catch (e) { + console.log("Analytics unavailable"); + } + + if (response.data.isPb) { + //new pb + Result.showCrown(); + Result.updateCrown(); + DB.saveLocalPB( + Config.mode, + completedEvent.mode2, + Config.punctuation, + Config.language, + Config.difficulty, + Config.lazyMode, + completedEvent.wpm, + completedEvent.acc, + completedEvent.rawWpm, + completedEvent.consistency + ); + } + } + + $("#retrySavingResultButton").addClass("hidden"); + }) + .catch((e) => { + AccountButton.loading(false); + let msg = e?.response?.data?.message ?? e.message; + Notifications.add("Failed to save result: " + msg, -1); + $("#retrySavingResultButton").removeClass("hidden"); + + retrySaving.completedEvent = completedEvent; + retrySaving.canRetry = true; + }); +} + +export function fail(reason) { + failReason = reason; + // input.pushHistory(); + // corrected.pushHistory(); + TestStats.pushKeypressesToHistory(); + finish(true); + let testSeconds = TestStats.calculateTestSeconds(performance.now()); + let afkseconds = TestStats.calculateAfkSeconds(testSeconds); + let tt = testSeconds - afkseconds; + if (tt < 0) tt = 0; + TestStats.incrementIncompleteSeconds(tt); + TestStats.incrementRestartCount(); +} + +==> ./monkeytype/src/js/test/test-ui.js <== +import * as Notifications from "./notifications"; +import * as ThemeColors from "./theme-colors"; +import Config, * as UpdateConfig from "./config"; +import * as DB from "./db"; +import * as TestLogic from "./test-logic"; +import * as Funbox from "./funbox"; +import * as PaceCaret from "./pace-caret"; +import * as CustomText from "./custom-text"; +import * as Keymap from "./keymap"; +import * as Caret from "./caret"; +import * as CommandlineLists from "./commandline-lists"; +import * as Commandline from "./commandline"; +import * as OutOfFocus from "./out-of-focus"; +import * as ManualRestart from "./manual-restart-tracker"; +import * as PractiseWords from "./practise-words"; +import * as Replay from "./replay"; +import * as TestStats from "./test-stats"; +import * as Misc from "./misc"; +import * as TestUI from "./test-ui"; +import * as ChallengeController from "./challenge-controller"; +import * as RateQuotePopup from "./rate-quote-popup"; +import * as UI from "./ui"; +import * as TestTimer from "./test-timer"; + +export let currentWordElementIndex = 0; +export let resultVisible = false; +export let activeWordTop = 0; +export let testRestarting = false; +export let lineTransition = false; +export let currentTestLine = 0; +export let resultCalculating = false; + +export function setResultVisible(val) { + resultVisible = val; +} + +export function setCurrentWordElementIndex(val) { + currentWordElementIndex = val; +} + +export function setActiveWordTop(val) { + activeWordTop = val; +} + +export function setTestRestarting(val) { + testRestarting = val; +} + +export function setResultCalculating(val) { + resultCalculating = val; +} + +export function reset() { + currentTestLine = 0; + currentWordElementIndex = 0; +} + +export function focusWords() { + if (!$("#wordsWrapper").hasClass("hidden")) { + $("#wordsInput").focus(); + } +} + +export function updateActiveElement(backspace) { + let active = document.querySelector("#words .active"); + if (Config.mode == "zen" && backspace) { + active.remove(); + } else if (active !== null) { + if (Config.highlightMode == "word") { + active.querySelectorAll("letter").forEach((e) => { + e.classList.remove("correct"); + }); + } + active.classList.remove("active"); + } + try { + let activeWord = document.querySelectorAll("#words .word")[ + currentWordElementIndex + ]; + activeWord.classList.add("active"); + activeWord.classList.remove("error"); + activeWordTop = document.querySelector("#words .active").offsetTop; + if (Config.highlightMode == "word") { + activeWord.querySelectorAll("letter").forEach((e) => { + e.classList.add("correct"); + }); + } + } catch (e) {} +} + +function getWordHTML(word) { + let newlineafter = false; + let retval = `
`; + for (let c = 0; c < word.length; c++) { + if (Config.funbox === "arrows") { + if (word.charAt(c) === "↑") { + retval += ``; + } + if (word.charAt(c) === "↓") { + retval += ``; + } + if (word.charAt(c) === "←") { + retval += ``; + } + if (word.charAt(c) === "→") { + retval += ``; + } + } else if (word.charAt(c) === "t") { + retval += ``; + } else if (word.charAt(c) === "\n") { + newlineafter = true; + retval += ``; + } else { + retval += "" + word.charAt(c) + ""; + } + } + retval += "
"; + if (newlineafter) retval += "
"; + return retval; +} + +export function showWords() { + $("#words").empty(); + + let wordsHTML = ""; + if (Config.mode !== "zen") { + for (let i = 0; i < TestLogic.words.length; i++) { + wordsHTML += getWordHTML(TestLogic.words.get(i)); + } + } else { + wordsHTML = + '
word height
'; + } + + $("#words").html(wordsHTML); + + $("#wordsWrapper").removeClass("hidden"); + const wordHeight = $(document.querySelector(".word")).outerHeight(true); + const wordsHeight = $(document.querySelector("#words")).outerHeight(true); + console.log( + `Showing words. wordHeight: ${wordHeight}, wordsHeight: ${wordsHeight}` + ); + if ( + Config.showAllLines && + Config.mode != "time" && + !(CustomText.isWordRandom && CustomText.word == 0) && + !CustomText.isTimeRandom + ) { + $("#words").css("height", "auto"); + $("#wordsWrapper").css("height", "auto"); + let nh = wordHeight * 3; + + if (nh > wordsHeight) { + nh = wordsHeight; + } + $(".outOfFocusWarning").css("line-height", nh + "px"); + } else { + $("#words") + .css("height", wordHeight * 4 + "px") + .css("overflow", "hidden"); + $("#wordsWrapper") + .css("height", wordHeight * 3 + "px") + .css("overflow", "hidden"); + $(".outOfFocusWarning").css("line-height", wordHeight * 3 + "px"); + } + + if (Config.mode === "zen") { + $(document.querySelector(".word")).remove(); + } else { + if (Config.keymapMode === "next") { + Keymap.highlightKey( + TestLogic.words + .getCurrent() + .substring( + TestLogic.input.current.length, + TestLogic.input.current.length + 1 + ) + .toString() + .toUpperCase() + ); + } + } + + updateActiveElement(); + Funbox.toggleScript(TestLogic.words.getCurrent()); + + Caret.updatePosition(); +} + +export function addWord(word) { + $("#words").append(getWordHTML(word)); +} + +export function flipColors(tf) { + if (tf) { + $("#words").addClass("flipped"); + } else { + $("#words").removeClass("flipped"); + } +} + +export function colorful(tc) { + if (tc) { + $("#words").addClass("colorfulMode"); + } else { + $("#words").removeClass("colorfulMode"); + } +} + +export async function screenshot() { + let revealReplay = false; + function revertScreenshot() { + $("#notificationCenter").removeClass("hidden"); + $("#commandLineMobileButton").removeClass("hidden"); + $(".pageTest .ssWatermark").addClass("hidden"); + $(".pageTest .ssWatermark").text("monkeytype.com"); + $(".pageTest .buttons").removeClass("hidden"); + if (revealReplay) $("#resultReplay").removeClass("hidden"); + if (firebase.auth().currentUser == null) + $(".pageTest .loginTip").removeClass("hidden"); + } + + if (!$("#resultReplay").hasClass("hidden")) { + revealReplay = true; + Replay.pauseReplay(); + } + $("#resultReplay").addClass("hidden"); + $(".pageTest .ssWatermark").removeClass("hidden"); + $(".pageTest .ssWatermark").text( + moment(Date.now()).format("DD MMM YYYY HH:mm") + " | monkeytype.com " + ); + if (firebase.auth().currentUser != null) { + $(".pageTest .ssWatermark").text( + DB.getSnapshot().name + + " | " + + moment(Date.now()).format("DD MMM YYYY HH:mm") + + " | monkeytype.com " + ); + } + $(".pageTest .buttons").addClass("hidden"); + let src = $("#middle"); + var sourceX = src.position().left; /*X position from div#target*/ + var sourceY = src.position().top; /*Y position from div#target*/ + var sourceWidth = src.outerWidth( + true + ); /*clientWidth/offsetWidth from div#target*/ + var sourceHeight = src.outerHeight( + true + ); /*clientHeight/offsetHeight from div#target*/ + $("#notificationCenter").addClass("hidden"); + $("#commandLineMobileButton").addClass("hidden"); + $(".pageTest .loginTip").addClass("hidden"); + try { + let paddingX = 50; + let paddingY = 25; + html2canvas(document.body, { + backgroundColor: await ThemeColors.get("bg"), + width: sourceWidth + paddingX * 2, + height: sourceHeight + paddingY * 2, + x: sourceX - paddingX, + y: sourceY - paddingY, + }).then(function (canvas) { + canvas.toBlob(function (blob) { + try { + if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) { + open(URL.createObjectURL(blob)); + revertScreenshot(); + } else { + navigator.clipboard + .write([ + new ClipboardItem( + Object.defineProperty({}, blob.type, { + value: blob, + enumerable: true, + }) + ), + ]) + .then(() => { + Notifications.add("Copied to clipboard", 1, 2); + revertScreenshot(); + }); + } + } catch (e) { + Notifications.add( + "Error saving image to clipboard: " + e.message, + -1 + ); + revertScreenshot(); + } + }); + }); + } catch (e) { + Notifications.add("Error creating image: " + e.message, -1); + revertScreenshot(); + } + setTimeout(() => { + revertScreenshot(); + }, 3000); +} + +export function updateWordElement(showError = !Config.blindMode) { + let input = TestLogic.input.current; + let wordAtIndex; + let currentWord; + wordAtIndex = document.querySelector("#words .word.active"); + currentWord = TestLogic.words.getCurrent(); + let ret = ""; + + let newlineafter = false; + + if (Config.mode === "zen") { + for (let i = 0; i < TestLogic.input.current.length; i++) { + if (TestLogic.input.current[i] === "t") { + ret += ``; + } else if (TestLogic.input.current[i] === "\n") { + newlineafter = true; + ret += ``; + } else { + ret += `${TestLogic.input.current[i]}`; + } + } + } else { + let correctSoFar = false; + + // slice earlier if input has trailing compose characters + const inputWithoutComposeLength = Misc.trailingComposeChars.test(input) + ? input.search(Misc.trailingComposeChars) + : input.length; + if ( + input.search(Misc.trailingComposeChars) < currentWord.length && + currentWord.slice(0, inputWithoutComposeLength) === + input.slice(0, inputWithoutComposeLength) + ) { + correctSoFar = true; + } + + let wordHighlightClassString = correctSoFar ? "correct" : "incorrect"; + if (Config.blindMode) { + wordHighlightClassString = "correct"; + } + + for (let i = 0; i < input.length; i++) { + let charCorrect = currentWord[i] == input[i]; + + let correctClass = "correct"; + if (Config.highlightMode == "off") { + correctClass = ""; + } + + let currentLetter = currentWord[i]; + let tabChar = ""; + let nlChar = ""; + if (Config.funbox === "arrows") { + if (currentLetter === "↑") { + currentLetter = ``; + } + if (currentLetter === "↓") { + currentLetter = ``; + } + if (currentLetter === "←") { + currentLetter = ``; + } + if (currentLetter === "→") { + currentLetter = ``; + } + } else if (currentLetter === "t") { + tabChar = "tabChar"; + currentLetter = ``; + } else if (currentLetter === "\n") { + nlChar = "nlChar"; + currentLetter = ``; + } + + if ( + Misc.trailingComposeChars.test(input) && + i > input.search(Misc.trailingComposeChars) + ) + continue; + + if (charCorrect) { + ret += `${currentLetter}`; + } else if ( + currentLetter !== undefined && + Misc.trailingComposeChars.test(input) && + i === input.search(Misc.trailingComposeChars) + ) { + ret += `${currentLetter}`; + } else if (!showError) { + if (currentLetter !== undefined) { + ret += `${currentLetter}`; + } + } else if (currentLetter === undefined) { + if (!Config.hideExtraLetters) { + let letter = input[i]; + if (letter == " " || letter == "t" || letter == "\n") { + letter = "_"; + } + ret += `${letter}`; + } + } else { + ret += + `` + + currentLetter + + (Config.indicateTypos ? `${input[i]}` : "") + + ""; + } + } + + const inputWithSingleComposeLength = Misc.trailingComposeChars.test(input) + ? input.search(Misc.trailingComposeChars) + 1 + : input.length; + if (inputWithSingleComposeLength < currentWord.length) { + for (let i = inputWithSingleComposeLength; i < currentWord.length; i++) { + if (Config.funbox === "arrows") { + if (currentWord[i] === "↑") { + ret += ``; + } + if (currentWord[i] === "↓") { + ret += ``; + } + if (currentWord[i] === "←") { + ret += ``; + } + if (currentWord[i] === "→") { + ret += ``; + } + } else if (currentWord[i] === "t") { + ret += ``; + } else if (currentWord[i] === "\n") { + ret += ``; + } else { + ret += + `` + + currentWord[i] + + ""; + } + } + } + + if (Config.highlightMode === "letter" && Config.hideExtraLetters) { + if (input.length > currentWord.length && !Config.blindMode) { + $(wordAtIndex).addClass("error"); + } else if (input.length == currentWord.length) { + $(wordAtIndex).removeClass("error"); + } + } + } + wordAtIndex.innerHTML = ret; + if (newlineafter) $("#words").append("
"); +} + +export function lineJump(currentTop) { + //last word of the line + if (currentTestLine > 0) { + let hideBound = currentTop; + + let toHide = []; + let wordElements = $("#words .word"); + for (let i = 0; i < currentWordElementIndex; i++) { + if ($(wordElements[i]).hasClass("hidden")) continue; + let forWordTop = Math.floor(wordElements[i].offsetTop); + if (forWordTop < hideBound - 10) { + toHide.push($($("#words .word")[i])); + } + } + const wordHeight = $(document.querySelector(".word")).outerHeight(true); + if (Config.smoothLineScroll && toHide.length > 0) { + lineTransition = true; + $("#words").prepend( + `
` + ); + $("#words .smoothScroller").animate( + { + height: 0, + }, + TestTimer.slowTimer ? 0 : 125, + () => { + $("#words .smoothScroller").remove(); + } + ); + $("#paceCaret").animate( + { + top: document.querySelector("#paceCaret").offsetTop - wordHeight, + }, + TestTimer.slowTimer ? 0 : 125 + ); + $("#words").animate( + { + marginTop: `-${wordHeight}px`, + }, + TestTimer.slowTimer ? 0 : 125, + () => { + activeWordTop = document.querySelector("#words .active").offsetTop; + + currentWordElementIndex -= toHide.length; + lineTransition = false; + toHide.forEach((el) => el.remove()); + $("#words").css("marginTop", "0"); + } + ); + } else { + toHide.forEach((el) => el.remove()); + currentWordElementIndex -= toHide.length; + $("#paceCaret").css({ + top: document.querySelector("#paceCaret").offsetTop - wordHeight, + }); + } + } + currentTestLine++; +} + +export function updateModesNotice() { + let anim = false; + if ($(".pageTest #testModesNotice").text() === "") anim = true; + + $(".pageTest #testModesNotice").empty(); + + if (TestLogic.isRepeated && Config.mode !== "quote") { + $(".pageTest #testModesNotice").append( + `
repeated
` + ); + } + + if (TestLogic.hasTab) { + $(".pageTest #testModesNotice").append( + `
shift + tab to restart
` + ); + } + + if (ChallengeController.active) { + $(".pageTest #testModesNotice").append( + `
${ChallengeController.active.display}
` + ); + } + + if (Config.mode === "zen") { + $(".pageTest #testModesNotice").append( + `
shift + enter to finish zen
` + ); + } + + // /^[0-9a-zA-Z_.-]+$/.test(name); + + if ( + (/_\d+k$/g.test(Config.language) || + /code_/g.test(Config.language) || + Config.language == "english_commonly_misspelled") && + Config.mode !== "quote" + ) { + $(".pageTest #testModesNotice").append( + `
${Config.language.replace( + /_/g, + " " + )}
` + ); + } + + if (Config.difficulty === "expert") { + $(".pageTest #testModesNotice").append( + `
expert
` + ); + } else if (Config.difficulty === "master") { + $(".pageTest #testModesNotice").append( + `
master
` + ); + } + + if (Config.blindMode) { + $(".pageTest #testModesNotice").append( + `
blind
` + ); + } + + if (Config.lazyMode) { + $(".pageTest #testModesNotice").append( + `
lazy
` + ); + } + + if ( + Config.paceCaret !== "off" || + (Config.repeatedPace && TestLogic.isPaceRepeat) + ) { + let speed = ""; + try { + speed = ` (${Math.round(PaceCaret.settings.wpm)} wpm)`; + } catch {} + $(".pageTest #testModesNotice").append( + `
${ + Config.paceCaret === "average" + ? "average" + : Config.paceCaret === "pb" + ? "pb" + : "custom" + } pace${speed}
` + ); + } + + if (Config.minWpm !== "off") { + $(".pageTest #testModesNotice").append( + `
min ${Config.minWpmCustomSpeed} wpm
` + ); + } + + if (Config.minAcc !== "off") { + $(".pageTest #testModesNotice").append( + `
min ${Config.minAccCustom}% acc
` + ); + } + + if (Config.minBurst !== "off") { + $(".pageTest #testModesNotice").append( + `
min ${ + Config.minBurstCustomSpeed + } burst ${Config.minBurst === "flex" ? "(flex)" : ""}
` + ); + } + + if (Config.funbox !== "none") { + $(".pageTest #testModesNotice").append( + `
${Config.funbox.replace( + /_/g, + " " + )}
` + ); + } + + if (Config.confidenceMode === "on") { + $(".pageTest #testModesNotice").append( + `
confidence
` + ); + } + if (Config.confidenceMode === "max") { + $(".pageTest #testModesNotice").append( + `
max confidence
` + ); + } + + if (Config.stopOnError != "off") { + $(".pageTest #testModesNotice").append( + `
stop on ${Config.stopOnError}
` + ); + } + + if (Config.layout !== "default") { + $(".pageTest #testModesNotice").append( + `
emulating ${Config.layout.replace( + /_/g, + " " + )}
` + ); + } + + if (Config.oppositeShiftMode !== "off") { + $(".pageTest #testModesNotice").append( + `
opposite shift${ + Config.oppositeShiftMode === "keymap" ? " (keymap)" : "" + }
` + ); + } + + let tagsString = ""; + try { + DB.getSnapshot().tags.forEach((tag) => { + if (tag.active === true) { + tagsString += tag.name + ", "; + } + }); + + if (tagsString !== "") { + $(".pageTest #testModesNotice").append( + `
${tagsString.substring( + 0, + tagsString.length - 2 + )}
` + ); + } + } catch {} + + if (anim) { + $(".pageTest #testModesNotice") + .css("transition", "none") + .css("opacity", 0) + .animate( + { + opacity: 1, + }, + 125, + () => { + $(".pageTest #testModesNotice").css("transition", ".125s"); + } + ); + } +} + +export function arrangeCharactersRightToLeft() { + $("#words").addClass("rightToLeftTest"); + $("#resultWordsHistory .words").addClass("rightToLeftTest"); + $("#resultReplay .words").addClass("rightToLeftTest"); +} + +export function arrangeCharactersLeftToRight() { + $("#words").removeClass("rightToLeftTest"); + $("#resultWordsHistory .words").removeClass("rightToLeftTest"); + $("#resultReplay .words").removeClass("rightToLeftTest"); +} + +async function loadWordsHistory() { + $("#resultWordsHistory .words").empty(); + let wordsHTML = ""; + for (let i = 0; i < TestLogic.input.history.length + 2; i++) { + let input = TestLogic.input.getHistory(i); + let word = TestLogic.words.get(i); + let wordEl = ""; + try { + if (input === "") throw new Error("empty input word"); + if ( + TestLogic.corrected.getHistory(i) !== undefined && + TestLogic.corrected.getHistory(i) !== "" + ) { + wordEl = `
`; + } else { + wordEl = `
`; + } + if (i === TestLogic.input.history.length - 1) { + //last word + let wordstats = { + correct: 0, + incorrect: 0, + missed: 0, + }; + let length = Config.mode == "zen" ? input.length : word.length; + for (let c = 0; c < length; c++) { + if (c < input.length) { + //on char that still has a word list pair + if (Config.mode == "zen" || input[c] == word[c]) { + wordstats.correct++; + } else { + wordstats.incorrect++; + } + } else { + //on char that is extra + wordstats.missed++; + } + } + if (wordstats.incorrect !== 0 || Config.mode !== "time") { + if (Config.mode != "zen" && input !== word) { + wordEl = `
`; + } + } + } else { + if (Config.mode != "zen" && input !== word) { + wordEl = `
`; + } + } + + let loop; + if (Config.mode == "zen" || input.length > word.length) { + //input is longer - extra characters possible (loop over input) + loop = input.length; + } else { + //input is shorter or equal (loop over word list) + loop = word.length; + } + + for (let c = 0; c < loop; c++) { + let correctedChar; + try { + correctedChar = TestLogic.corrected.getHistory(i)[c]; + } catch (e) { + correctedChar = undefined; + } + let extraCorrected = ""; + if ( + c + 1 === loop && + TestLogic.corrected.getHistory(i) !== undefined && + TestLogic.corrected.getHistory(i).length > input.length + ) { + extraCorrected = "extraCorrected"; + } + if (Config.mode == "zen" || word[c] !== undefined) { + if (Config.mode == "zen" || input[c] === word[c]) { + if (correctedChar === input[c] || correctedChar === undefined) { + wordEl += `${input[c]}`; + } else { + wordEl += + `` + + input[c] + + ""; + } + } else { + if (input[c] === TestLogic.input.current) { + wordEl += + `` + + word[c] + + ""; + } else if (input[c] === undefined) { + wordEl += "" + word[c] + ""; + } else { + wordEl += + `` + + word[c] + + ""; + } + } + } else { + wordEl += '' + input[c] + ""; + } + } + wordEl += "
"; + } catch (e) { + try { + wordEl = "
"; + for (let c = 0; c < word.length; c++) { + wordEl += "" + word[c] + ""; + } + wordEl += "
"; + } catch {} + } + wordsHTML += wordEl; + } + $("#resultWordsHistory .words").html(wordsHTML); + $("#showWordHistoryButton").addClass("loaded"); + return true; +} + +export function toggleResultWords() { + if (resultVisible) { + if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) { + //show + + if (!$("#showWordHistoryButton").hasClass("loaded")) { + $("#words").html( + `
` + ); + loadWordsHistory().then(() => { + if (Config.burstHeatmap) { + TestUI.applyBurstHeatmap(); + } + $("#resultWordsHistory") + .removeClass("hidden") + .css("display", "none") + .slideDown(250, () => { + if (Config.burstHeatmap) { + TestUI.applyBurstHeatmap(); + } + }); + }); + } else { + if (Config.burstHeatmap) { + TestUI.applyBurstHeatmap(); + } + $("#resultWordsHistory") + .removeClass("hidden") + .css("display", "none") + .slideDown(250); + } + } else { + //hide + + $("#resultWordsHistory").slideUp(250, () => { + $("#resultWordsHistory").addClass("hidden"); + }); + } + } +} + +export function applyBurstHeatmap() { + if (Config.burstHeatmap) { + $("#resultWordsHistory .heatmapLegend").removeClass("hidden"); + + let burstlist = [...TestStats.burstHistory]; + + burstlist = burstlist.filter((x) => x !== Infinity); + burstlist = burstlist.filter((x) => x < 350); + + if ( + TestLogic.input.getHistory(TestLogic.input.getHistory().length - 1) + .length !== TestLogic.words.getCurrent()?.length + ) { + burstlist = burstlist.splice(0, burstlist.length - 1); + } + + let median = Misc.median(burstlist); + let adatm = []; + burstlist.forEach((burst) => { + adatm.push(Math.abs(median - burst)); + }); + let step = Misc.mean(adatm); + let steps = [ + { + val: 0, + class: "heatmap-0", + }, + { + val: median - step * 1.5, + class: "heatmap-1", + }, + { + val: median - step * 0.5, + class: "heatmap-2", + }, + { + val: median + step * 0.5, + class: "heatmap-3", + }, + { + val: median + step * 1.5, + class: "heatmap-4", + }, + ]; + $("#resultWordsHistory .words .word").each((index, word) => { + let wordBurstVal = parseInt($(word).attr("burst")); + let cls = ""; + steps.forEach((step) => { + if (wordBurstVal > step.val) cls = step.class; + }); + $(word).addClass(cls); + }); + } else { + $("#resultWordsHistory .heatmapLegend").addClass("hidden"); + $("#resultWordsHistory .words .word").removeClass("heatmap-0"); + $("#resultWordsHistory .words .word").removeClass("heatmap-1"); + $("#resultWordsHistory .words .word").removeClass("heatmap-2"); + $("#resultWordsHistory .words .word").removeClass("heatmap-3"); + $("#resultWordsHistory .words .word").removeClass("heatmap-4"); + } +} + +export function highlightBadWord(index, showError) { + if (!showError) return; + $($("#words .word")[index]).addClass("error"); +} + +$(document.body).on("click", "#saveScreenshotButton", () => { + screenshot(); +}); + +$(document).on("click", "#testModesNotice .text-button.restart", (event) => { + TestLogic.restart(); +}); + +$(document).on("click", "#testModesNotice .text-button.blind", (event) => { + UpdateConfig.toggleBlindMode(); +}); + +$(".pageTest #copyWordsListButton").click(async (event) => { + try { + let words; + if (Config.mode == "zen") { + words = TestLogic.input.history.join(" "); + } else { + words = TestLogic.words + .get() + .slice(0, TestLogic.input.history.length) + .join(" "); + } + await navigator.clipboard.writeText(words); + Notifications.add("Copied to clipboard", 0, 2); + } catch (e) { + Notifications.add("Could not copy to clipboard: " + e, -1); + } +}); + +$(".pageTest #rateQuoteButton").click(async (event) => { + RateQuotePopup.show(TestLogic.randomQuote); +}); + +$(".pageTest #toggleBurstHeatmap").click(async (event) => { + UpdateConfig.setBurstHeatmap(!Config.burstHeatmap); +}); + +$(".pageTest .loginTip .link").click(async (event) => { + UI.changePage("login"); +}); + +$(document).on("mouseleave", "#resultWordsHistory .words .word", (e) => { + $(".wordInputAfter").remove(); +}); + +$("#wpmChart").on("mouseleave", (e) => { + $(".wordInputAfter").remove(); +}); + +$(document).on("mouseenter", "#resultWordsHistory .words .word", (e) => { + if (resultVisible) { + let input = $(e.currentTarget).attr("input"); + let burst = $(e.currentTarget).attr("burst"); + if (input != undefined) + $(e.currentTarget).append( + `
+
+ ${input + .replace(/t/g, "_") + .replace(/\n/g, "_") + .replace(//g, ">")} +
+
+ ${Math.round(Config.alwaysShowCPM ? burst * 5 : burst)}${ + Config.alwaysShowCPM ? "cpm" : "wpm" + } +
+
` + ); + } +}); + +$(document).on("click", "#testModesNotice .text-button", (event) => { + // console.log("CommandlineLists."+$(event.currentTarget).attr("commands")); + let commands = CommandlineLists.getList( + $(event.currentTarget).attr("commands") + ); + let func = $(event.currentTarget).attr("function"); + if (commands !== undefined) { + if ($(event.currentTarget).attr("commands") === "commandsTags") { + CommandlineLists.updateTagCommands(); + } + CommandlineLists.pushCurrent(commands); + Commandline.show(); + } else if (func != undefined) { + eval(func); + } +}); + +$("#wordsInput").on("focus", () => { + if (!resultVisible && Config.showOutOfFocusWarning) { + OutOfFocus.hide(); + } + Caret.show(TestLogic.input.current); +}); + +$("#wordsInput").on("focusout", () => { + if (!resultVisible && Config.showOutOfFocusWarning) { + OutOfFocus.show(); + } + Caret.hide(); +}); + +$(document).on("keypress", "#restartTestButton", (event) => { + if (event.key == "Enter") { + ManualRestart.reset(); + if ( + TestLogic.active && + Config.repeatQuotes === "typing" && + Config.mode === "quote" + ) { + TestLogic.restart(true); + } else { + TestLogic.restart(); + } + } +}); + +$(document.body).on("click", "#restartTestButton", () => { + ManualRestart.set(); + if (resultCalculating) return; + if ( + TestLogic.active && + Config.repeatQuotes === "typing" && + Config.mode === "quote" + ) { + TestLogic.restart(true); + } else { + TestLogic.restart(); + } +}); + +$(document.body).on( + "click", + "#retrySavingResultButton", + TestLogic.retrySavingResult +); + +$(document).on("keypress", "#practiseWordsButton", (event) => { + if (event.keyCode == 13) { + PractiseWords.showPopup(true); + } +}); + +$(document.body).on("click", "#practiseWordsButton", () => { + // PractiseWords.init(); + PractiseWords.showPopup(); +}); + +$(document).on("keypress", "#nextTestButton", (event) => { + if (event.keyCode == 13) { + TestLogic.restart(); + } +}); + +$(document.body).on("click", "#nextTestButton", () => { + ManualRestart.set(); + TestLogic.restart(); +}); + +$(document).on("keypress", "#showWordHistoryButton", (event) => { + if (event.keyCode == 13) { + toggleResultWords(); + } +}); + +$(document.body).on("click", "#showWordHistoryButton", () => { + toggleResultWords(); +}); + +$(document.body).on("click", "#restartTestButtonWithSameWordset", () => { + if (Config.mode == "zen") { + Notifications.add("Repeat test disabled in zen mode"); + return; + } + ManualRestart.set(); + TestLogic.restart(true); +}); + +$(document).on("keypress", "#restartTestButtonWithSameWordset", (event) => { + if (Config.mode == "zen") { + Notifications.add("Repeat test disabled in zen mode"); + return; + } + if (event.keyCode == 13) { + TestLogic.restart(true); + } +}); + +$("#wordsWrapper").on("click", () => { + focusWords(); +}); + +==> ./monkeytype/src/js/test/lazy-mode.js <== +let accents = [ + ["áàâäåãąą́āą̄ă", "a"], + ["éèêëẽęę́ēę̄ėě", "e"], + ["íìîïĩįį́īį̄", "i"], + ["óòôöøõóōǫǫ́ǭő", "o"], + ["úùûüŭũúūůű", "u"], + ["ńň", "n"], + ["çĉčć", "c"], + ["ř", "r"], + ["ď", "d"], + ["ťț", "t"], + ["æ", "ae"], + ["œ", "oe"], + ["ẅ", "w"], + ["ĝğg̃", "g"], + ["ĥ", "h"], + ["ĵ", "j"], + ["ń", "n"], + ["ŝśšș", "s"], + ["żźž", "z"], + ["ÿỹýÿŷ", "y"], + ["ł", "l"], + ["أإآ", "ا"], + ["َ", ""], + ["ُ", ""], + ["ِ", ""], + ["ْ", ""], + ["ً", ""], + ["ٌ", ""], + ["ٍ", ""], + ["ّ", ""], +]; + +export function replaceAccents(word, accentsOverride) { + let newWord = word; + if (!accents && !accentsOverride) return newWord; + let regex; + let list = accentsOverride || accents; + for (let i = 0; i < list.length; i++) { + regex = new RegExp(`[${list[i][0]}]`, "gi"); + newWord = newWord.replace(regex, list[i][1]); + } + return newWord; +} + +==> ./monkeytype/src/js/test/british-english.js <== +import { capitalizeFirstLetter } from "./misc"; + +let list = null; + +export async function getList() { + if (list == null) { + return $.getJSON("languages/britishenglish.json", function (data) { + list = data; + return list; + }); + } else { + return list; + } +} + +export async function replace(word) { + let list = await getList(); + let replacement = list.find((a) => + word.match(RegExp(`^([\\W]*${a[0]}[\\W]*)$`, "gi")) + ); + return replacement + ? word.replace( + RegExp(`^(?:([\\W]*)(${replacement[0]})([\\W]*))$`, "gi"), + (_, $1, $2, $3) => + $1 + + ($2.charAt(0) === $2.charAt(0).toUpperCase() + ? $2 === $2.toUpperCase() + ? replacement[1].toUpperCase() + : capitalizeFirstLetter(replacement[1]) + : replacement[1]) + + $3 + ) + : word; +} + +==> ./monkeytype/src/js/test/custom-text.js <== +export let text = "The quick brown fox jumps over the lazy dog".split(" "); +export let isWordRandom = false; +export let isTimeRandom = false; +export let word = ""; +export let time = ""; +export let delimiter = " "; + +export function setText(txt) { + text = txt; +} + +export function setIsWordRandom(val) { + isWordRandom = val; +} + +export function setIsTimeRandom(val) { + isTimeRandom = val; +} + +export function setTime(val) { + time = val; +} + +export function setWord(val) { + word = val; +} + +export function setDelimiter(val) { + delimiter = val; +} + +==> ./monkeytype/src/js/test/live-burst.js <== +import Config from "./config"; +import * as TestLogic from "./test-logic"; + +export function update(burst) { + let number = burst; + if (Config.blindMode) { + number = 0; + } + document.querySelector("#miniTimerAndLiveWpm .burst").innerHTML = number; + document.querySelector("#liveBurst").innerHTML = number; +} + +export function show() { + if (!Config.showLiveBurst) return; + if (!TestLogic.active) return; + if (Config.timerStyle === "mini") { + if (!$("#miniTimerAndLiveWpm .burst").hasClass("hidden")) return; + $("#miniTimerAndLiveWpm .burst") + .removeClass("hidden") + .css("opacity", 0) + .animate( + { + opacity: Config.timerOpacity, + }, + 125 + ); + } else { + if (!$("#liveBurst").hasClass("hidden")) return; + $("#liveBurst").removeClass("hidden").css("opacity", 0).animate( + { + opacity: Config.timerOpacity, + }, + 125 + ); + } +} + +export function hide() { + $("#liveBurst").animate( + { + opacity: Config.timerOpacity, + }, + 125, + () => { + $("#liveBurst").addClass("hidden"); + } + ); + $("#miniTimerAndLiveWpm .burst").animate( + { + opacity: Config.timerOpacity, + }, + 125, + () => { + $("#miniTimerAndLiveWpm .burst").addClass("hidden"); + } + ); +} + +==> ./monkeytype/src/js/test/live-wpm.js <== +import Config from "./config"; +import * as TestLogic from "./test-logic"; + +let liveWpmElement = document.querySelector("#liveWpm"); +let miniLiveWpmElement = document.querySelector("#miniTimerAndLiveWpm .wpm"); + +export function update(wpm, raw) { + // if (!TestLogic.active || !Config.showLiveWpm) { + // hideLiveWpm(); + // } else { + // showLiveWpm(); + // } + let number = wpm; + if (Config.blindMode) { + number = raw; + } + if (Config.alwaysShowCPM) { + number = Math.round(number * 5); + } + miniLiveWpmElement.innerHTML = number; + liveWpmElement.innerHTML = number; +} + +export function show() { + if (!Config.showLiveWpm) return; + if (!TestLogic.active) return; + if (Config.timerStyle === "mini") { + // $("#miniTimerAndLiveWpm .wpm").css("opacity", Config.timerOpacity); + if (!$("#miniTimerAndLiveWpm .wpm").hasClass("hidden")) return; + $("#miniTimerAndLiveWpm .wpm") + .removeClass("hidden") + .css("opacity", 0) + .animate( + { + opacity: Config.timerOpacity, + }, + 125 + ); + } else { + // $("#liveWpm").css("opacity", Config.timerOpacity); + if (!$("#liveWpm").hasClass("hidden")) return; + $("#liveWpm").removeClass("hidden").css("opacity", 0).animate( + { + opacity: Config.timerOpacity, + }, + 125 + ); + } +} + +export function hide() { + $("#liveWpm").animate( + { + opacity: Config.timerOpacity, + }, + 125, + () => { + $("#liveWpm").addClass("hidden"); + } + ); + $("#miniTimerAndLiveWpm .wpm").animate( + { + opacity: Config.timerOpacity, + }, + 125, + () => { + $("#miniTimerAndLiveWpm .wpm").addClass("hidden"); + } + ); +} + +==> ./monkeytype/src/js/test/focus.js <== +import * as Caret from "./caret"; +import * as UI from "./ui"; + +let state = false; + +export function set(foc, withCursor = false) { + if (foc && !state) { + state = true; + Caret.stopAnimation(); + $("#top").addClass("focus"); + $("#bottom").addClass("focus"); + if (!withCursor) $("body").css("cursor", "none"); + $("#middle").addClass("focus"); + } else if (!foc && state) { + state = false; + Caret.startAnimation(); + $("#top").removeClass("focus"); + $("#bottom").removeClass("focus"); + $("body").css("cursor", "default"); + $("#middle").removeClass("focus"); + } +} + +$(document).mousemove(function (event) { + if (!state) return; + if (UI.getActivePage() == "pageLoading") return; + if (UI.getActivePage() == "pageAccount" && state == true) return; + if ( + $("#top").hasClass("focus") && + (event.originalEvent.movementX > 0 || event.originalEvent.movementY > 0) + ) { + set(false); + } +}); + +==> ./monkeytype/src/js/test/today-tracker.js <== +import * as Misc from "./misc"; +import * as DB from "./db"; + +let seconds = 0; +let addedAllToday = false; +let dayToday = null; + +export function addSeconds(s) { + if (addedAllToday) { + let nowDate = new Date(); + nowDate = nowDate.getDate(); + if (nowDate > dayToday) { + seconds = s; + return; + } + } + seconds += s; +} + +export function getString() { + let secString = Misc.secondsToString(Math.round(seconds), true, true); + return secString + (addedAllToday === true ? " today" : " session"); +} + +export async function addAllFromToday() { + let todayDate = new Date(); + todayDate.setSeconds(0); + todayDate.setMinutes(0); + todayDate.setHours(0); + todayDate.setMilliseconds(0); + dayToday = todayDate.getDate(); + todayDate = todayDate.getTime(); + + seconds = 0; + + let results = await DB.getSnapshot().results; + + results.forEach((result) => { + let resultDate = new Date(result.timestamp); + resultDate.setSeconds(0); + resultDate.setMinutes(0); + resultDate.setHours(0); + resultDate.setMilliseconds(0); + resultDate = resultDate.getTime(); + + if (resultDate >= todayDate) { + seconds += + result.testDuration + result.incompleteTestSeconds - result.afkDuration; + } + }); + + addedAllToday = true; +} + +==> ./monkeytype/src/js/test/wikipedia.js <== +import * as Loader from "./loader"; +import Config from "./config"; +import * as Misc from "./misc"; + +export class Section { + constructor(title, author, words) { + this.title = title; + this.author = author; + this.words = words; + } +} + +export async function getTLD(languageGroup) { + // language group to tld + switch (languageGroup.name) { + case "english": + return "en"; + + case "spanish": + return "es"; + + case "french": + return "fr"; + + case "german": + return "de"; + + case "portuguese": + return "pt"; + + case "italian": + return "it"; + + case "dutch": + return "nl"; + + default: + return "en"; + } +} + +export async function getSection() { + // console.log("Getting section"); + Loader.show(); + + // get TLD for wikipedia according to language group + let urlTLD = "en"; + let currentLanguageGroup = await Misc.findCurrentGroup(Config.language); + urlTLD = await getTLD(currentLanguageGroup); + + const randomPostURL = `https://${urlTLD}.wikipedia.org/api/rest_v1/page/random/summary`; + var sectionObj = {}; + var randomPostReq = await fetch(randomPostURL); + var pageid = 0; + + if (randomPostReq.status == 200) { + let postObj = await randomPostReq.json(); + sectionObj.title = postObj.title; + sectionObj.author = postObj.author; + pageid = postObj.pageid; + } + + return new Promise((res, rej) => { + if (randomPostReq.status != 200) { + Loader.hide(); + rej(randomPostReq.status); + } + + const sectionURL = `https://${urlTLD}.wikipedia.org/w/api.php?action=query&format=json&pageids=${pageid}&prop=extracts&exintro=true&origin=*`; + + var sectionReq = new XMLHttpRequest(); + sectionReq.onload = () => { + if (sectionReq.readyState == 4) { + if (sectionReq.status == 200) { + let sectionText = JSON.parse(sectionReq.responseText).query.pages[ + pageid.toString() + ].extract; + let words = []; + + // Remove double whitespaces and finally trailing whitespaces. + sectionText = sectionText.replace(/<\/p>

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

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