`;
- }
- 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 += ``;
- } else {
- wordEl +=
- `";
- }
- } else {
- if (input[c] === TestLogic.input.current) {
- wordEl +=
- `";
- } else if (input[c] === undefined) {
- wordEl += "" + word[c] + " ";
- } else {
- wordEl +=
- `";
- }
- }
- } else {
- wordEl += '";
- }
- }
- 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(
- `
`
- );
- }
-});
-
-$(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://monkeytype.com/)
+
+
+
+
+
+
+
+
+# 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :(
+
+ Looks like JavaScript is disabled. Please enable JavaScript in order
+ to use this website.
+
+
+
+
+
+
+ :(
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leaderboards
+
Next update in: --:--
+
+
+
+
+
+
+
+
+
+ #
+ name
+
+ wpm
+
+ accuracy
+
+
+ raw
+
+ consistency
+
+ test
+ date
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ name
+
+ wpm
+
+ accuracy
+
+
+ raw
+
+ consistency
+
+ test
+ date
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
monkey see
+ monkeytype
+
+
+
+
+
+
+
+ sign out
+
+
+
+
+
+
+
+
+ Caps Lock
+
+
Time left to memorise all words: 0s
+
+
+
+
+
+
+ Click or press any key to focus
+
+
+
+
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+ 4
+
+
+ 5
+
+
+ 6
+
+
+
+ 7
+
+
+ 8
+
+
+ 9
+
+
+ 0
+
+
+ -
+
+
+ =
+
+
+
+
+
+ q
+
+
+ w
+
+
+ e
+
+
+ r
+
+
+ t
+
+
+
+ y
+
+
+ u
+
+
+ i
+
+
+ o
+
+
+ p
+
+
+ [
+
+
+ ]
+
+
+ \
+
+
+
+
+
+ a
+
+
+ s
+
+
+ d
+
+
+
+ g
+
+
+
+ h
+
+
+
+ k
+
+
+ l
+
+
+ ;
+
+
+ '
+
+
+
+
+
+ z
+
+
+ x
+
+
+ c
+
+
+ v
+
+
+ b
+
+
+ n
+
+
+
+
+ m
+
+
+ ,
+
+
+ .
+
+
+ /
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
consistency
+
+ 2 -
+
+
+
+
+
+
+
+ source
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ Sign in
+ to save your result
+
+
+
+
+ input history
+
+
+
+
+
+
+
+
+
+
+
+
+ watch replay
+
+
+
+
0s
+
+
+
+
+
+ Retry saving result
+
+
+
+
monkeytype.com
+
+
+
+
+
+
+
+ Created with love by Miodec.
+
Supported
+ and
+
expanded
+ by many awesome people. Launched on 15th of May, 2020.
+
+
+
about
+
+
+ Monkeytype is a minimalistic typing test, featuring many test
+ modes, an account system to save your typing speed history and
+ user configurable features like themes, a smooth caret and more.
+
+
+
+
word set
+
+ By default, this website uses the most common 200 words in the
+ English language to generate its tests. You can change to an
+ expanded set (1000 most common words) in the options, or change
+ the language entirely.
+
+
+
+
keybinds
+
+ You can use
+ tab
+ and
+ enter
+ (or just
+ tab
+ if you have quick tab mode enabled) to restart the typing test.
+ Open the command line by pressing
+ ctrl/cmd
+ +
+ shift
+ +
+ p
+ or
+ esc
+ - there you can access all the functionality you need without
+ touching your mouse
+
+
+
+
stats
+
+ wpm - total amount of characters in the correctly typed words
+ (including spaces), divided by 5 and normalised to 60 seconds.
+
+
+ raw wpm - calculated just like wpm, but also includes incorrect
+ words.
+
+
acc - percentage of correctly pressed keys.
+
+ char - correct characters / incorrect characters. Calculated
+ after the test has ended.
+
+
+ consistency - based on the variance of your raw wpm. Closer to
+ 100% is better. Calculated using the coefficient of variation of
+ raw wpm and mapped onto a scale from 0 to 100.
+
+
+
+
+
results screen
+
+ After completing a test you will be able to see your wpm, raw
+ wpm, accuracy, character stats, test length, leaderboards info
+ and test info. (you can hover over some values to get floating
+ point numbers). You can also see a graph of your wpm and raw
+ over the duration of the test. Remember that the wpm line is a
+ global average, while the raw wpm line is a local, momentary
+ value. (meaning if you stop, the value is 0)
+
+
+
+
bug report or feature request
+
+ If you encounter a bug, or have a feature request - join the
+ Discord server, send me an email, a direct message on Twitter or
+ create an issue on GitHub.
+
+
+
+
support
+
+ Thanks to everyone who has supported this project. It would not
+ be possible without you and your continued support.
+
+
+
+
+
contact
+
+ If you encounter a bug, have a feature request or just want to
+ say hi - here are the different ways you can contact me
+ directly.
+
+
+
+
+
credits
+
+
+ Montydrei
+
+ for the name suggestion
+
+
+
+ Everyone
+
+ who provided valuable feedback on the original reddit post for
+ the prototype of this website
+
+
+ Supporters
+ who helped financially by donating, enabling optional ads or
+ buying merch
+
+
+
+ Contributors
+
+ on GitHub that have helped with implementing various features,
+ adding themes and more
+
+
+
+
+
+
+
+
+
+
+
+ tip: You can also change all these settings quickly using the
+ command line (
+ ctrl/cmd
+ +
+ shift
+ +
+ p
+ or
+ esc
+ )
+
+
+
+
+
+ account
+
+
+
+
+
discord integration
+
+ When you connect your monkeytype account to your Discord
+ account, you will be automatically assigned a new role every
+ time you achieve a new personal best in a 60 second test. If
+ you pair your accounts before joining the Discord server the
+ bot
+ will not
+ give you a role.
+
+
+
+
+
+ Your accounts are paired!
+
+
+
+
+
+
+
+
+
presets
+
+ Create settings presets that can be applied with one click.
+ Remember to edit your preset if you make any changes - they
+ don't save on their own.
+
+
+
+
+
+
+
+ behavior
+
+
+
+
+
test difficulty
+
+ Normal is the classic type test experience. Expert fails the
+ test if you submit (press space) an incorrect word. Master
+ fails if you press a single incorrect key (meaning you have to
+ achieve 100% accuracy).
+
+
+
+
+
quick tab mode
+
+ Press
+ tab
+ to quickly restart the test, or to quickly jump to the test
+ page. This function disables tab navigation on the website.
+
+
+
+
+
+
repeat quotes
+
+ This setting changes the restarting behavior when typing in
+ quote mode. Changing it to 'typing' will repeat the quote if
+ you restart while typing.
+
+
+
+
+
+
+
blind mode
+
+ No errors or incorrect words are highlighted. Helps you to
+ focus on raw speed. If enabled, quick end is recommended.
+
+
+
+
+
+
always show words history
+
+ This option will automatically show the words history at the
+ end of the test. Can cause slight lag with a lot of words.
+
+
+
+
+
single list command line
+
+ When enabled, it will show the command line with all commands
+ in a single list instead of submenu arrangements. Selecting
+ 'manual' will expose all commands only after typing
+ >
+ .
+
+
+
+
+
min wpm
+
+ Automatically fails a test if your WPM falls below a
+ threshold.
+
+
+
+
+
min accuracy
+
+ Automatically fails a test if your accuracy falls below a
+ threshold.
+
+
+
+
+
min burst
+
+ Automatically fails a test if your raw for a single word falls
+ below this threshold. Selecting 'flex' allows for this
+ threshold to automatically decrease for longer words.
+
+
+
+
+
british english
+
+ When enabled, the website will use the British spelling
+ instead of American. Note that this might not replace all
+ words correctly. If you find any issues, please let us know.
+
+
+
+
+
+
+
funbox
+
+ These are special modes that change the website in some
+ special way (by altering the word generation, behavior of the
+ website or the looks). Give each one of them a try!
+
+
+
+
+
+
custom layoutfluid
+
+ Select which layouts you want the layoutfluid funbox to cycle
+ through.
+
+
+
+
+
+
+ input
+
+
+
+
+
+
+
+ sound
+
+
+
+
+
sound volume
+
Change the volume of the sound effects.
+
+
+
+
play sound on click
+
+ Plays a short sound when you press a key.
+
+
+
+
+
play sound on error
+
+ Plays a short sound if you press an incorrect key or press
+ space too early.
+
+
+
+
+
+
+
+ caret
+
+
+
+
+
smooth caret
+
+ The caret will move smoothly between letters and words.
+
+
+
+
+
caret style
+
+ Change the style of the caret during the test.
+
+
+
+
+
pace caret
+
+ Displays a second caret that moves at constant speed. The
+ 'average' option averages the speed of last 10 results.
+
+
+
+
+
repeated pace
+
+ When repeating a test, a pace caret will automatically be
+ enabled for one test with the speed of your previous test. It
+ does not override the pace caret if it's already enabled.
+
+
+
+
+
pace caret style
+
+ Change the style of the pace caret during the test.
+
+
+
+
+
+
+
+ appearance
+
+
+
+
+
timer/progress style
+
+ Change the style of the timer/progress during a timed test.
+
+
+
+
+
timer/progress color
+
+ Change the color of the timer/progress number/bar and live wpm
+ number.
+
+
+
+
+
timer/progress opacity
+
+ Change the opacity of the timer/progress number/bar and live
+ wpm number.
+
+
+
+
+
highlight mode
+
+ Change what is highlighted during the test.
+
+
+
+
+
+
show all lines
+
+ When enabled, the website will show all lines for word, custom
+ and quote mode tests - otherwise the lines will be limited to
+ 3, and will automatically scroll. Using this could cause the
+ timer text and live wpm to not be visible.
+
+
+
+
+
always show decimal places
+
+ Always shows decimal places for values on the result page,
+ without the need to hover over the stats.
+
+
+
+
+
always show cpm
+
+ Always shows characters per minute calculation instead of the
+ default words per minute calculation.
+
+
+
+
+
start graphs at zero
+
+ Force graph axis to always start at zero, no matter what the
+ data is. Turning this off may exaggerate the value changes.
+
+
+
+
+
font size
+
Change the font size of the test words.
+
+
+
+
+
page width
+
Control the width of the content.
+
+
+
+
keymap
+
+ Displays your current layout while taking a test. React shows
+ what you pressed and Next shows what you need to press next.
+
+
+
+
+
+
keymap legend style
+
+
+
+
+
+
+
+
+
+ theme
+
+
+
+
+
flip test colors
+
+ By default, typed text is brighter than the future text. When
+ enabled, the colors will be flipped and the future text will
+ be brighter than the already typed text.
+
+
+
+
+
colorful mode
+
+ When enabled, the test words will use the main color, instead
+ of the text color, making the website more colorful.
+
+
+
+
+
custom background
+
+ Set an image url to be a custom background image. Cover fits
+ the image to cover the screen. Contain fits the image to be
+ fully visible. Max fits the image corner to corner.
+
+
+
+
+
custom background filter
+
+ Apply various effects to the custom background.
+
+
+
+
+
randomize theme
+
+ After completing a test, the theme will be set to a random
+ one. The random themes are not saved to your config. If set to
+ 'fav' only favourite themes will be randomized. If set to
+ 'light' or 'dark', only presets with light or dark background
+ colors will be randomized, respectively.
+
+
+
+
+
theme
+
+
+ preset
+
+
+ custom
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hide elements
+
+
+
+
+
live wpm
+
+ Displays a live WPM speed during the test. Updates once every
+ second.
+
+
+
+
+
live accuracy
+
Displays live accuracy during the test.
+
+
+
+
live burst
+
+ Displays live burst during the test of the last word you
+ typed.
+
+
+
+
+
timer/progress
+
+ Displays a live timer for timed tests and progress for
+ words/custom tests.
+
+
+
+
+
key tips
+
+ Shows the keybind tips at the bottom of the page.
+
+
+
+
+
out of focus warning
+
+ Shows an out of focus reminder after 1 second of being 'out of
+ focus' (not being able to type).
+
+
+
+
+
caps lock warning
+
Displays a warning when caps lock is on.
+
+
+
+
+
+
+ danger zone
+
+
+
+
+
import/export settings
+
Import or export the settings as JSON.
+
+
+
+
enable ads
+
+ If you wish to support me without directly donating you can
+ enable ads that will be visible at the bottom of the screen.
+ Sellout mode also shows ads on both sides of the screen.
+
+
+ (changes will take effect after a refresh).
+
+
+
+
+
reset settings
+
+ Resets settings to the default (but doesn't touch your tags).
+ Warning: you can't undo this action!
+
+
+
+
+
reset personal bests
+
+ Resets all your personal bests (but doesn't delete any tests
+ from your history). Warning: you can't undo this action!
+
+
+
+
+
update account name
+
+ Change the name of your account. You can only do this once
+ every 30 days.
+
+
+
+
+
password authentication settings
+
+ Add password authentication, update your password or email.
+
+
+
+
+
google authentication settings
+
Add or remove Google authentication.
+
+
+
+
+
delete account
+
+ Deletes your account and all data connected to it.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
login
+
Forgot password?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Account created on -
+
+
personal bests
+
+
+
+
+
+ time
+
+ wpm
+
+ accuracy
+
+
+ raw
+
+ consistency
+
+ date
+
+
+
+
+ 15
+ -
+ -
+ -
+
+
+ 30
+ -
+ -
+ -
+
+
+ 60
+ -
+ -
+ -
+
+
+ 120
+ -
+ -
+ -
+
+
+
+
show all
+
+
+
+
+
+ words
+
+ wpm
+
+ accuracy
+
+
+ raw
+
+ consistency
+
+ date
+
+
+
+
+ 10
+ -
+ -
+ -
+
+
+ 25
+ -
+ -
+ -
+
+
+ 50
+ -
+ -
+ -
+
+
+ 100
+ -
+ -
+ -
+
+
+
+
show all
+
+
+
+
+
+
+
+
+
+ No data found. Check your filters.
+
+
+
+
+
+
+
+ tests completed
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+ average wpm
+
+ (last 10 tests)
+
+
-
+
+
+
+
highest raw wpm
+
-
+
+
+
+
+
+ average raw wpm
+
+ (last 10 tests)
+
+
-
+
+
+
+
+
+ avg accuracy
+
+ (last 10 tests)
+
+
-
+
+
+
+
+
+ avg consistency
+
+ (last 10 tests)
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ chars
+
+ mode
+
+ info
+ tags
+
+
+
+
+
+
load more
+
+
+
+
+
+
+
+
+
+ tab
+ and
+ enter
+ - Restart Test
+
+ ctrl/cmd
+ +
+ shift
+ +
+ p
+ or
+ esc
+ - Command Line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+==> ./monkeytype/static/privacy-policy.html <==
+
+
+
+
+
+
Privacy Policy | Monkeytype
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
monkey see
+ monkeytype
+
Privacy Policy
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
monkey see
+ monkeytype
+
Email Handler
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+==> ./monkeytype/static/terms-of-service.html <==
+
+
+
+
+
+
Terms of Service | Monkeytype
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
monkey see
+ monkeytype
+
Terms of Service
+
+
+
+
+
+
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:
+
+
+ is unlawful or promotes unlawful activities;
+ is or contains sexually obscene content;
+ is libelous, defamatory, or fraudulent;
+ is discriminatory or abusive toward any individual or group;
+
+ 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;
+
+
+ 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
+
+
+ contains or installs any active malware or exploits/uses our
+ platform for exploit delivery (such as part of a command or control
+ system); or infringes on any proprietary right of any party,
+ including patent, trademark, trade secret, copyright, right of
+ publicity, or other rights.
+
+
+
+
While using the Services, you agree that you will not:
+
+
+ harass, abuse, threaten, or incite violence towards any individual
+ or group, including other Users and Monkeytype contributors;
+
+
+ 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;
+
+
+ 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;
+
+ access the Services in ways that exceed your authorization;
+
+ 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;
+
+
+ violate the privacy of any third party, such as by posting another
+ person's personal information without their consent;
+
+
+ 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;
+
+
+ facilitate or encourage any violations of this Agreement or
+ interfere with the operation, appearance, security, or functionality
+ of the Services; or
+
+ use the Services in any manner that is harmful to minors.
+
+
+ Without limiting the foregoing, you will not transmit or post any
+ content anywhere on the Services that violates any laws. Monkeytype
+ absolutely does not tolerate engaging in activity that significantly
+ harms our Users. We will resolve disputes in favor of protecting our
+ Users as a whole.
+
+
Privacy Policy
+ If you use our Services, you must abide by our Privacy Policy. You
+ acknowledge that you have read our
+
+ Privacy Policy
+
+ and understand that it sets forth how we collect, use, and store your
+ information. If you do not agree with our Privacy Statement, then you
+ must stop using the Services immediately. Any person, entity, or service
+ collecting data from the Services must comply with our Privacy
+ Statement. Misuse of any User's Personal Information is prohibited. If
+ you collect any Personal Information from a User, you agree that you
+ will only use the Personal Information you gather for the purpose for
+ which the User has authorized it. You agree that you will reasonably
+ secure any Personal Information you have gathered from the Services, and
+ you will respond promptly to complaints, removal requests, and 'do not
+ contact' requests from us or Users.
+
+
Limitations on Automated Use
+ You shouldn't use bots or access our Services in malicious or
+ un-permitted ways. While accessing or using the Services, you may not:
+
+ use bots, hacks, or cheats while using our site;
+ create manual requests to Monkeytype servers;
+
+ tamper with or use non-public areas of the Services, or the computer
+ or delivery systems of Monkeytype and/or its service providers;
+
+
+ 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;
+
+
+ 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;
+
+
+ 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;
+
+
+ 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
+
+
+ interfere with, or disrupt or attempt to interfere with or disrupt,
+ the access of any User, host, or network, including, without
+ limitation, by sending a virus to, spamming, or overloading the
+ Services, or by scripted use of the Services in such a manner as to
+ interfere with or create an undue burden on the Services.
+
+
+
Links
+ Monkeytype is not responsible for the contents of any linked sites. The
+ use of any linked website is at the user's own risk.
+
+
Changes
+ Monkeytype may revise these Terms of Service for its Website at any time
+ without prior notice. By using this Website, you are agreeing to be
+ bound by the current version of these Terms of Service.
+
Disclaimer
+
+ EXCLUDING THE EXPLICITLY STATED WARRANTIES WITHIN THESE TERMS, WE ONLY
+ OFFER OUR SERVICES ON AN 'AS-IS' BASIS. YOUR ACCESS TO AND USE OF THE
+ SERVICES OR ANY CONTENT IS AT YOUR OWN RISK. YOU UNDERSTAND AND AGREE
+ THAT THE SERVICES AND CONTENT ARE PROVIDED TO YOU ON AN 'AS IS,' 'WITH
+ ALL FAULTS,' AND 'AS AVAILABLE' BASIS. WITHOUT LIMITING THE FOREGOING,
+ TO THE FULL EXTENT PERMITTED BY LAW, MONKEYTYPE DISCLAIMS ALL
+ WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
+ NON-INFRINGEMENT. TO THE EXTENT SUCH DISCLAIMER CONFLICTS WITH
+ APPLICABLE LAW, THE SCOPE AND DURATION OF ANY APPLICABLE WARRANTY WILL
+ BE THE MINIMUM PERMITTED UNDER SUCH LAW. MONKEYTYPE MAKES NO
+ REPRESENTATIONS, WARRANTIES, OR GUARANTEES AS TO THE RELIABILITY,
+ TIMELINESS, QUALITY, SUITABILITY, AVAILABILITY, ACCURACY, OR
+ COMPLETENESS OF ANY KIND WITH RESPECT TO THE SERVICES, INCLUDING ANY
+ REPRESENTATION OR WARRANTY THAT THE USE OF THE SERVICES WILL (A) BE
+ TIMELY, UNINTERRUPTED, OR ERROR-FREE, OR OPERATE IN COMBINATION WITH
+ ANY OTHER HARDWARE, SOFTWARE, SYSTEM, OR DATA, (B) MEET YOUR
+ REQUIREMENTS OR EXPECTATIONS, (C) BE FREE FROM ERRORS OR THAT DEFECTS
+ WILL BE CORRECTED, OR (D) BE FREE OF VIRUSES OR OTHER HARMFUL
+ COMPONENTS. MONKEYTYPE ALSO MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND WITH RESPECT TO CONTENT; USER CONTENT IS PROVIDED BY AND IS
+ SOLELY THE RESPONSIBILITY OF THE RESPECTIVE USER PROVIDING THAT
+ CONTENT. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED
+ FROM MONKEYTYPE OR THROUGH THE SERVICES, WILL CREATE ANY WARRANTY NOT
+ EXPRESSLY MADE HEREIN. MONKEYTYPE DOES NOT WARRANT, ENDORSE,
+ GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY USER CONTENT ON THE
+ SERVICES OR ANY HYPERLINKED WEBSITE OR THIRD-PARTY SERVICE, AND
+ MONKEYTYPE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR
+ TRANSACTIONS BETWEEN YOU AND THIRD PARTIES. IF APPLICABLE LAW DOES NOT
+ ALLOW THE EXCLUSION OF SOME OR ALL OF THE ABOVE IMPLIED OR STATUTORY
+ WARRANTIES TO APPLY TO YOU, THE ABOVE EXCLUSIONS WILL APPLY TO YOU TO
+ THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW.
+
+
Contact
+
+ If you have any questions about Monkeytype’s privacy policy, the data
+ we hold on you, or you would like to exercise one of your data
+ protection rights, please do not hesitate to contact us.
+
+
+ Email:
+
+ jack@monkeytype.com
+
+
+ Discord:
+
+ Miodec#1512
+
+
+
+ Terms based on
+ Glitch terms
+
+
+
+
+
+
+
+==> ./monkeytype/static/robots.txt <==
+User-agent: *
+Disallow:
+
+==> ./monkeytype/static/security-policy.html <==
+
+
+
+
+
+
Security Policy | Monkeytype
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
monkey see
+ monkeytype
+
Security Policy
+
+
+
+
+
+ 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(
+ `
`
+ );
+ }
+ });
+ } 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(
+ `
`
+ );
+ }
+ });
+ 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 += ``;
+ } else {
+ wordEl +=
+ `";
+ }
+ } else {
+ if (input[c] === TestLogic.input.current) {
+ wordEl +=
+ `";
+ } else if (input[c] === undefined) {
+ wordEl += "" + word[c] + " ";
+ } else {
+ wordEl +=
+ `";
+ }
+ }
+ } else {
+ wordEl += '";
+ }
+ }
+ 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(
+ `
`
+ );
+ }
+});
+
+$(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