From 8264351e00cbd2e44f8b01b300744b5b20653296 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Jun 2026 13:46:27 +0200 Subject: [PATCH 01/12] feat(account): add challenges (@fehmer) --- .../__integration__/dal/user.spec.ts | 13 +- .../__tests__/api/controllers/user.spec.ts | 21 +- backend/package.json | 1 + backend/src/api/controllers/user.ts | 36 +- backend/src/constants/auto-roles.ts | 43 +- backend/src/dal/user.ts | 7 +- backend/src/utils/discord.ts | 63 +- backend/src/utils/test.json | 46 ++ frontend/package.json | 1 + frontend/src/ts/commandline/lists.ts | 23 +- .../ts/commandline/lists/load-challenge.ts | 36 +- .../ts/controllers/challenge-controller.ts | 44 +- frontend/src/ts/controllers/url-handler.tsx | 6 +- frontend/src/ts/pages/account-settings.ts | 21 +- frontend/src/ts/utils/json-data.ts | 12 +- frontend/static/challenges/_list.json | 720 ------------------ packages/challenges/.oxlintrc.json | 7 + packages/challenges/__test__/tsconfig.json | 7 + packages/challenges/package.json | 34 + packages/challenges/src/index.ts | 564 ++++++++++++++ packages/challenges/tsconfig.json | 13 + packages/challenges/tsup.config.js | 3 + packages/challenges/vitest.config.ts | 11 + packages/contracts/src/users.ts | 10 + packages/schemas/src/challenges.ts | 76 +- packages/schemas/src/users.ts | 11 + pnpm-lock.yaml | 103 ++- 27 files changed, 1025 insertions(+), 907 deletions(-) create mode 100644 backend/src/utils/test.json delete mode 100644 frontend/static/challenges/_list.json create mode 100644 packages/challenges/.oxlintrc.json create mode 100644 packages/challenges/__test__/tsconfig.json create mode 100644 packages/challenges/package.json create mode 100644 packages/challenges/src/index.ts create mode 100644 packages/challenges/tsconfig.json create mode 100644 packages/challenges/tsup.config.js create mode 100644 packages/challenges/vitest.config.ts diff --git a/backend/__tests__/__integration__/dal/user.spec.ts b/backend/__tests__/__integration__/dal/user.spec.ts index 5f44cffe1d41..1126650fefef 100644 --- a/backend/__tests__/__integration__/dal/user.spec.ts +++ b/backend/__tests__/__integration__/dal/user.spec.ts @@ -1271,7 +1271,7 @@ describe("UserDal", () => { describe("linkDiscord", () => { it("throws for nonexisting user", async () => { await expect(async () => - UserDAL.linkDiscord("unknown", "", ""), + UserDAL.linkDiscord("unknown", "", "", {}), ).rejects.toThrow("User not found\nStack: link discord"); }); it("should update", async () => { @@ -1279,14 +1279,18 @@ describe("UserDal", () => { const { uid } = await UserTestData.createUser({ discordId: "discordId", discordAvatar: "discordAvatar", + challenges: { + "100hours": {}, + }, }); //when - await UserDAL.linkDiscord(uid, "newId", "newAvatar"); + await UserDAL.linkDiscord(uid, "newId", "newAvatar", { "250hours": {} }); //then const read = await UserDAL.getUser(uid, "read"); expect(read.discordId).toEqual("newId"); expect(read.discordAvatar).toEqual("newAvatar"); + expect(read.challenges).toEqual({ "250hours": {} }); }); it("should update without avatar", async () => { //given @@ -1315,6 +1319,10 @@ describe("UserDal", () => { const { uid } = await UserTestData.createUser({ discordId: "discordId", discordAvatar: "discordAvatar", + challenges: { + "100hours": {}, + "250hours": { addedAt: Date.now() }, + }, }); //when @@ -1324,6 +1332,7 @@ describe("UserDal", () => { const read = await UserDAL.getUser(uid, "read"); expect(read.discordId).toBeUndefined(); expect(read.discordAvatar).toBeUndefined(); + expect(read.challenges).toBeUndefined(); }); }); describe("updateInbox", () => { diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index 867f050cbfa2..8c197717de1b 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -37,6 +37,7 @@ import * as WeeklyXpLeaderboard from "../../../src/services/weekly-xp-leaderboar import * as ConnectionsDal from "../../../src/dal/connections"; import { pb } from "../../__testData__/users"; import Test from "supertest/lib/test"; +import { Challenges } from "@monkeytype/challenges"; const { mockApp, uid, mockAuth } = setup(); const configuration = Configuration.getCachedConfiguration(); @@ -1552,7 +1553,7 @@ describe("user controller test", () => { it("should get oauth link", async () => { //WHEN const { body } = await mockApp - .get("/users/discord/oauth") + .get("/users/discord/oauth?includeRoles=true") .set("Authorization", `Bearer ${uid}`) .expect(200); @@ -1561,7 +1562,9 @@ describe("user controller test", () => { message: "Discord oauth link generated", data: { url }, }); - expect(getOauthLinkMock).toHaveBeenCalledWith(uid); + expect(getOauthLinkMock).toHaveBeenCalledWith(uid, { + includeRoles: true, + }); }); it("should fail if feature is not enabled", async () => { //GIVEN @@ -1587,6 +1590,7 @@ describe("user controller test", () => { "iStateValidForUser", ); const getDiscordUserMock = vi.spyOn(DiscordUtils, "getDiscordUser"); + const getDiscordRoleIdsMock = vi.spyOn(DiscordUtils, "getDiscordRoleIds"); const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains"); const userLinkDiscordMock = vi.spyOn(UserDal, "linkDiscord"); const georgeLinkDiscordMock = vi.spyOn(GeorgeQueue, "linkDiscord"); @@ -1599,6 +1603,9 @@ describe("user controller test", () => { id: "discordUserId", avatar: "discordUserAvatar", }); + getDiscordRoleIdsMock.mockResolvedValue([ + Challenges["100hours"].discordRoleId as string, + ]); isDiscordIdAvailableMock.mockResolvedValue(true); blocklistContainsMock.mockResolvedValue(false); userLinkDiscordMock.mockResolvedValue(); @@ -1610,6 +1617,7 @@ describe("user controller test", () => { isStateValidForUserMock, isDiscordIdAvailableMock, getDiscordUserMock, + getDiscordRoleIdsMock, blocklistContainsMock, userLinkDiscordMock, georgeLinkDiscordMock, @@ -1629,6 +1637,7 @@ describe("user controller test", () => { tokenType: "tokenType", accessToken: "accessToken", state: "statestatestatestate", + scope: ["scopeOne", "scopeTwo"], }) .expect(200); @@ -1653,6 +1662,11 @@ describe("user controller test", () => { "tokenType", "accessToken", ); + expect(getDiscordRoleIdsMock).toHaveBeenCalledWith( + "tokenType", + "accessToken", + ["scopeOne", "scopeTwo"], + ); expect(isDiscordIdAvailableMock).toHaveBeenCalledWith("discordUserId"); expect(blocklistContainsMock).toHaveBeenCalledWith({ discordId: "discordUserId", @@ -1661,6 +1675,9 @@ describe("user controller test", () => { uid, "discordUserId", "discordUserAvatar", + { + "100hours": {}, + }, ); expect(georgeLinkDiscordMock).toHaveBeenCalledWith( "discordUserId", diff --git a/backend/package.json b/backend/package.json index 5cd74ebacf64..c063510eaaaf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@date-fns/utc": "1.2.0", + "@monkeytype/challenges": "workspace:*", "@monkeytype/contracts": "workspace:*", "@monkeytype/funbox": "workspace:*", "@monkeytype/schemas": "workspace:*", diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index b89a7874515f..3dcb7b417393 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -39,6 +39,7 @@ import { CountByYearAndDay, TestActivity, UserProfileDetails, + UserChallenges, } from "@monkeytype/schemas/users"; import { addImportantLog, addLog, deleteUserLogs } from "../../dal/logs"; import { sendForgotPasswordEmail as authSendForgotPasswordEmail } from "../../utils/auth"; @@ -59,6 +60,7 @@ import { ForgotPasswordEmailRequest, GetCurrentTestActivityResponse, GetCustomThemesResponse, + GetDiscordOauthLinkQuery, GetDiscordOauthLinkResponse, GetFavoriteQuotesResponse, GetFriendsResponse, @@ -94,6 +96,18 @@ import { tryCatch } from "@monkeytype/util/trycatch"; import * as ConnectionsDal from "../../dal/connections"; import { PersonalBest } from "@monkeytype/schemas/shared"; +import { Challenges } from "@monkeytype/challenges"; +import { ChallengeName } from "@monkeytype/schemas/challenges"; + +const challengeNameByRoleId: Record = Object.fromEntries( + Object.entries(Challenges) + .filter(([_, challenge]) => challenge.discordRoleId !== undefined) + .map(([name, challenge]) => [ + challenge.discordRoleId as string, + name as ChallengeName, + ]), +); + async function verifyCaptcha(captcha: string): Promise { const { data: verified, error } = await tryCatch(verify(captcha)); if (error) { @@ -629,12 +643,13 @@ export async function getUser(req: MonkeyRequest): Promise { } export async function getOauthLink( - req: MonkeyRequest, + req: MonkeyRequest, ): Promise { const { uid } = req.ctx.decodedToken; + const { includeRoles } = req.query; //build the url - const url = await DiscordUtils.getOauthLink(uid); + const url = await DiscordUtils.getOauthLink(uid, { includeRoles }); //return return new MonkeyResponse("Discord oauth link generated", { @@ -646,7 +661,7 @@ export async function linkDiscord( req: MonkeyRequest, ): Promise { const { uid } = req.ctx.decodedToken; - const { tokenType, accessToken, state } = req.body; + const { tokenType, accessToken, state, scope } = req.body; if (!(await DiscordUtils.iStateValidForUser(state, uid))) { throw new MonkeyError(403, "Invalid user token"); @@ -692,7 +707,20 @@ export async function linkDiscord( throw new MonkeyError(409, "The Discord account is blocked"); } - await UserDAL.linkDiscord(uid, discordId, discordAvatar); + let roles = await DiscordUtils.getDiscordRoleIds( + tokenType, + accessToken, + scope, + ); + + const challenges: UserChallenges = Object.fromEntries( + roles + .map((roleId) => challengeNameByRoleId[roleId]) + .filter((it) => it !== undefined) + .map((it) => [it, {}]), + ); + + await UserDAL.linkDiscord(uid, discordId, discordAvatar, challenges); await GeorgeQueue.linkDiscord(discordId, uid, userInfo.lbOptOut ?? false); void addImportantLog("user_discord_link", `linked to ${discordId}`, uid); diff --git a/backend/src/constants/auto-roles.ts b/backend/src/constants/auto-roles.ts index dd80dc29a7e0..2f719abf441c 100644 --- a/backend/src/constants/auto-roles.ts +++ b/backend/src/constants/auto-roles.ts @@ -1,38 +1,5 @@ -export default [ - "oneHourWarrior", - "doubleDown", - "tripleTrouble", - "quad", - "trueSimp", - "bigramSalad", - "simp", - "antidiseWhat", - "whatsThisWebsiteCalledAgain", - "developd", - "slowAndSteady", - "speedSpacer", - "iveGotThePower", - "accuracyExpert", - "accuracyMaster", - "accuracyGod", - "jolly", - "gottaCatchEmAll", - "rapGod", - "navySeal", - "rollercoaster", - "oneHourMirror", - "chooChoo", - "earfquake", - "simonSez", - "accountant", - "hidden", - "iCanSeeTheFuture", - "whatAreWordsAtThisPoint", - "specials", - "aeiou", - "asciiWarrior", - "iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS", - "oneNauseousMonkey", - "69", - "englishMaster", -]; +import { Challenges } from "@monkeytype/challenges"; + +export default Object.entries(Challenges) + .filter(([_, challenge]) => challenge.autoRole) + .map(([name]) => name); diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index ada92f0ee764..a862ad26e50c 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -26,6 +26,7 @@ import { User, CountByYearAndDay, Friend, + UserChallenges, } from "@monkeytype/schemas/users"; import { Mode, @@ -613,11 +614,15 @@ export async function linkDiscord( uid: string, discordId: string, discordAvatar?: string, + challenges?: UserChallenges, ): Promise { const updates: Partial = { discordId }; if (discordAvatar !== undefined && discordAvatar !== null) { updates.discordAvatar = discordAvatar; } + if (challenges !== undefined) { + updates.challenges = challenges; + } await updateUser({ uid }, { $set: updates }, { stack: "link discord" }); } @@ -625,7 +630,7 @@ export async function linkDiscord( export async function unlinkDiscord(uid: string): Promise { await updateUser( { uid }, - { $unset: { discordId: "", discordAvatar: "" } }, + { $unset: { discordId: "", discordAvatar: "", challenges: "" } }, { stack: "unlink discord" }, ); } diff --git a/backend/src/utils/discord.ts b/backend/src/utils/discord.ts index e290c02d5f75..b2533e2db581 100644 --- a/backend/src/utils/discord.ts +++ b/backend/src/utils/discord.ts @@ -6,16 +6,27 @@ import { z } from "zod"; import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; const BASE_URL = "https://discord.com/api"; +const CLIENT_ID = "798272335035498557"; +const SERVER_ID = "713194177403420752"; +const READ_ROLE_SCOPE = "guilds.members.read"; -const DiscordIdAndAvatarSchema = z.object({ - id: z.string(), - avatar: z - .string() - .optional() - .or(z.null().transform(() => undefined)), -}); +const DiscordIdAndAvatarSchema = z + .object({ + id: z.string(), + avatar: z + .string() + .optional() + .or(z.null().transform(() => undefined)), + }) + .strip(); type DiscordIdAndAvatar = z.infer; +const DiscordGuildMemberSchema = z + .object({ + roles: z.array(z.string()), + }) + .strip(); + export async function getDiscordUser( tokenType: string, accessToken: string, @@ -34,21 +45,51 @@ export async function getDiscordUser( return parsed; } -export async function getOauthLink(uid: string): Promise { +export async function getDiscordRoleIds( + tokenType: string, + accessToken: string, + scope?: string[], +): Promise { + if (!scope?.includes(READ_ROLE_SCOPE)) return []; + + const response = await fetch( + `${BASE_URL}/users/@me/guilds/${SERVER_ID}/member`, + { + headers: { + authorization: `${tokenType} ${accessToken}`, + }, + }, + ); + + const parsed = parseJsonWithSchema( + await response.text(), + DiscordGuildMemberSchema, + ); + + return parsed.roles; +} + +export async function getOauthLink( + uid: string, + options: { includeRoles?: boolean }, +): Promise { const connection = RedisClient.getConnection(); if (!connection) { throw new MonkeyError(500, "Redis connection not found"); } const token = randomBytes(10).toString("hex"); + const scope = ["identify"]; + + if (options.includeRoles) scope.push(READ_ROLE_SCOPE); - //add the token uid pair to reids + //add the token uid pair to redis await connection.setex(`discordoauth:${uid}`, 60, token); - return `${BASE_URL}/oauth2/authorize?client_id=798272335035498557&redirect_uri=${ + return `${BASE_URL}/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${ isDevEnvironment() ? `http%3A%2F%2Flocalhost%3A3000%2Fverify` : `https%3A%2F%2Fmonkeytype.com%2Fverify` - }&response_type=token&scope=identify&state=${token}`; + }&response_type=token&scope=${scope.join("+")}&state=${token}`; } export async function iStateValidForUser( diff --git a/backend/src/utils/test.json b/backend/src/utils/test.json new file mode 100644 index 000000000000..20c5c2f48997 --- /dev/null +++ b/backend/src/utils/test.json @@ -0,0 +1,46 @@ +{ + "avatar": null, + "banner": null, + "communication_disabled_until": null, + "flags": 0, + "joined_at": "2026-06-23T07:34:29.080000+00:00", + "nick": null, + "pending": false, + "premium_since": null, + "roles": ["730095916547309641", "873307090242908242"], + "unusual_dm_activity_until": null, + "collectibles": null, + "display_name_styles": null, + "user": { + "id": "1220521103592652822", + "username": "cute.terror", + "avatar": "b1599e23cdca04d352d5af2a7aab3929", + "discriminator": "0", + "public_flags": 0, + "flags": 0, + "banner": null, + "accent_color": 14588849, + "global_name": "Cosmiqu\u00e9", + "avatar_decoration_data": null, + "collectibles": null, + "display_name_styles": null, + "banner_color": "#de9bb1", + "clan": { + "identity_guild_id": "262268073363505164", + "identity_enabled": true, + "tag": "\u14da\u160f\u15e2", + "badge": "439c5b7117f3f3e8e574b4766d1b7e73" + }, + "primary_guild": { + "identity_guild_id": "262268073363505164", + "identity_enabled": true, + "tag": "\u14da\u160f\u15e2", + "badge": "439c5b7117f3f3e8e574b4766d1b7e73" + } + }, + "mute": false, + "deaf": false, + "bio": "", + "permissions": 104189505, + "permissions_new": "1091684154068545" +} diff --git a/frontend/package.json b/frontend/package.json index 24a7fc9a68f7..5a33b1face8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "dependencies": { "@date-fns/utc": "1.2.0", "@leonabcd123/modern-caps-lock": "3.1.3", + "@monkeytype/challenges": "workspace:*", "@monkeytype/contracts": "workspace:*", "@monkeytype/funbox": "workspace:*", "@monkeytype/schemas": "workspace:*", diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 6780991f828f..ac9a39c45a32 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -12,14 +12,10 @@ import CustomThemesListCommands from "./lists/custom-themes-list"; import PresetsCommands from "./lists/presets"; import FunboxCommands from "./lists/funbox"; import ThemesCommands from "./lists/themes"; -import LoadChallengeCommands, { - update as updateLoadChallengeCommands, -} from "./lists/load-challenge"; +import LoadChallengeCommands from "./lists/load-challenge"; import { Config } from "../config/store"; import { setConfig } from "../config/setters"; -import * as getErrorMessage from "../utils/error"; -import * as JSONData from "../utils/json-data"; import { randomizeTheme } from "../controllers/theme-controller"; import { showModal } from "../states/modals"; import { @@ -41,20 +37,6 @@ import { import { applyConfigFromJson } from "../config/lifecycle"; import { lastEventLog } from "../test/test-state"; -const challengesPromise = JSONData.getChallengeList(); -challengesPromise - .then((challenges) => { - updateLoadChallengeCommands(challenges); - }) - .catch((e: unknown) => { - console.error( - getErrorMessage.createErrorMessage( - e, - "Failed to update challenges commands", - ), - ); - }); - const adsCommands = buildCommands("ads"); export const commands: CommandsSubgroup = { @@ -406,8 +388,6 @@ export function doesListExist(listName: string): boolean { export async function getList( listName: CommandlineListKey | ConfigKey, ): Promise { - await Promise.allSettled([challengesPromise]); - const subGroup = subgroupByConfigKey[listName]; if (subGroup !== undefined) { return subGroup; @@ -451,7 +431,6 @@ export function getTopOfStack(): CommandsSubgroup { let singleList: CommandsSubgroup | undefined; export async function getSingleSubgroup(): Promise { - await Promise.allSettled([challengesPromise]); const singleCommands: Command[] = []; for (const command of commands.list) { const ret = buildSingleListCommands(command); diff --git a/frontend/src/ts/commandline/lists/load-challenge.ts b/frontend/src/ts/commandline/lists/load-challenge.ts index c49f1511a298..1d657a32647d 100644 --- a/frontend/src/ts/commandline/lists/load-challenge.ts +++ b/frontend/src/ts/commandline/lists/load-challenge.ts @@ -1,13 +1,26 @@ -import { navigate } from "../../controllers/route-controller"; +import { Challenges } from "@monkeytype/challenges"; import * as ChallengeController from "../../controllers/challenge-controller"; +import { navigate } from "../../controllers/route-controller"; import * as TestLogic from "../../test/test-logic"; import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; import { Command, CommandsSubgroup } from "../types"; -import { Challenge } from "@monkeytype/schemas/challenges"; +import { typedEntries } from "../../utils/misc"; const subgroup: CommandsSubgroup = { title: "Load challenge...", - list: [], + list: typedEntries(Challenges) + .filter(([_, challenge]) => challenge.type !== "hidden") + .map(([name, challenge]) => ({ + id: `loadChallenge${capitalizeFirstLetterOfEachWord(name)}`, + display: challenge.display, + exec: async (): Promise => { + await navigate("/"); + await ChallengeController.setup(name); + TestLogic.restart({ + nosave: true, + }); + }, + })), }; const commands: Command[] = [ @@ -19,21 +32,4 @@ const commands: Command[] = [ }, ]; -function update(challenges: Challenge[]): void { - challenges.forEach((challenge) => { - subgroup.list.push({ - id: `loadChallenge${capitalizeFirstLetterOfEachWord(challenge.name)}`, - display: challenge.display, - exec: async (): Promise => { - await navigate("/"); - await ChallengeController.setup(challenge.name); - TestLogic.restart({ - nosave: true, - }); - }, - }); - }); -} - export default commands; -export { update }; diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 0252e8d5e7b2..4b08fdc25606 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -1,33 +1,32 @@ -import * as Misc from "../utils/misc"; -import * as JSONData from "../utils/json-data"; import { - showNoticeNotification, showErrorNotification, + showNoticeNotification, showSuccessNotification, } from "../states/notifications"; import * as CustomText from "../test/custom-text"; import * as Funbox from "../test/funbox/funbox"; +import * as Misc from "../utils/misc"; -import { Config } from "../config/store"; import { setConfig } from "../config/setters"; +import { Config } from "../config/store"; import { configEvent } from "../events/config"; import * as TestState from "../test/test-state"; -import { showLoaderBar, hideLoaderBar } from "../states/loader-bar"; -import { CustomTextLimitMode, CustomTextMode } from "@monkeytype/schemas/util"; +import { Challenges } from "@monkeytype/challenges"; +import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; import { Config as ConfigType, Difficulty, - ThemeName, FunboxName, + ThemeName, } from "@monkeytype/schemas/configs"; -import { Mode } from "@monkeytype/schemas/shared"; import { CompletedEvent } from "@monkeytype/schemas/results"; +import { Mode } from "@monkeytype/schemas/shared"; +import { CustomTextLimitMode, CustomTextMode } from "@monkeytype/schemas/util"; +import { hideLoaderBar, showLoaderBar } from "../states/loader-bar"; +import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; import { areUnsortedArraysEqual } from "../utils/arrays"; -import { tryCatch } from "@monkeytype/util/trycatch"; -import { Challenge } from "@monkeytype/schemas/challenges"; import { qs } from "../utils/dom"; -import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; let challengeLoading = false; @@ -207,24 +206,13 @@ export function verify(result: CompletedEvent): string | null { } } -export async function setup(challengeName: string): Promise { +export async function setup(challengeName: ChallengeName): Promise { challengeLoading = true; setConfig("funbox", []); - const { data: list, error } = await tryCatch(JSONData.getChallengeList()); - if (error) { - showErrorNotification("Failed to setup challenge", { error }); - setTimeout(() => { - qs("header .config")?.show(); - qs(".page.pageTest")?.show(); - }, 250); - return false; - } + const challenge = Challenges[challengeName]; - const challenge = list.find( - (c) => c.name.toLowerCase() === challengeName.toLowerCase(), - ); let notitext; try { if (challenge === undefined) { @@ -245,7 +233,7 @@ export async function setup(challengeName: string): Promise { setConfig("difficulty", "normal", { nosave: true, }); - if (challenge.name === "englishMaster") { + if (challengeName === "englishMaster") { setConfig("language", "english_10k", { nosave: true, }); @@ -347,6 +335,7 @@ export async function setup(challengeName: string): Promise { throw new Error("Can't load challenge with current config"); } } else if (challenge.type === "other") { + /* TODO: missing challenge if (challenge.name === "semimak") { // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout? setConfig("mode", "time", { @@ -373,7 +362,8 @@ export async function setup(challengeName: string): Promise { setConfig("keymapMode", "static", { nosave: true, }); - } else if (challenge.name === "wingdings") { + } else */ + if (challengeName === "wingdings") { // Ten Words of Pain: 10-word Master mode test using the Wingdings custom font, no keymap setConfig("mode", "words", { nosave: true, @@ -401,7 +391,7 @@ export async function setup(challengeName: string): Promise { } else { showSuccessNotification(`Challenge loaded. ${notitext}`); } - setLoadedChallenge(challenge); + setLoadedChallenge({ name: challengeName, ...challenge }); challengeLoading = false; return true; } catch (e) { diff --git a/frontend/src/ts/controllers/url-handler.tsx b/frontend/src/ts/controllers/url-handler.tsx index 83ec39952c7f..9abe54e075ef 100644 --- a/frontend/src/ts/controllers/url-handler.tsx +++ b/frontend/src/ts/controllers/url-handler.tsx @@ -1,3 +1,4 @@ +import { ChallengeName } from "@monkeytype/schemas/challenges"; import { CustomBackgroundFilter, CustomBackgroundFilterSchema, @@ -45,10 +46,11 @@ export async function linkDiscord(hashOverride: string): Promise { const accessToken = fragment.get("access_token") as string; const tokenType = fragment.get("token_type") as string; const state = fragment.get("state") as string; + const scope = fragment.get("scope"); showLoaderBar(); const response = await Ape.users.linkDiscord({ - body: { tokenType, accessToken, state }, + body: { tokenType, accessToken, state, scope: scope?.split(" ") }, }); hideLoaderBar(); @@ -317,7 +319,7 @@ export async function loadChallengeFromUrl( ).toLowerCase(); if (getValue === "") return; - ChallengeController.setup(getValue) + ChallengeController.setup(getValue as ChallengeName) .then((result) => { if (result) { restartTest({ diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts index 6452541bc1f4..28183253306c 100644 --- a/frontend/src/ts/pages/account-settings.ts +++ b/frontend/src/ts/pages/account-settings.ts @@ -181,15 +181,18 @@ qsa( ".page.pageAccountSettings .section.discordIntegration .getLinkAndGoToOauth", )?.on("click", () => { showLoaderBar(); - void Ape.users.getDiscordOAuth().then((response) => { - if (response.status === 200) { - window.open(response.body.data.url, "_self"); - } else { - showErrorNotification( - `Failed to get OAuth from discord: ${response.body.message}`, - ); - } - }); + + void Ape.users + .getDiscordOAuth({ query: { includeRoles: true } }) + .then((response) => { + if (response.status === 200) { + window.open(response.body.data.url, "_self"); + } else { + showErrorNotification( + `Failed to get OAuth from discord: ${response.body.message}`, + ); + } + }); }); qs(".page.pageAccountSettings #setStreakHourOffset")?.on("click", () => { diff --git a/frontend/src/ts/utils/json-data.ts b/frontend/src/ts/utils/json-data.ts index 7b773eb19a20..0d19df8f0639 100644 --- a/frontend/src/ts/utils/json-data.ts +++ b/frontend/src/ts/utils/json-data.ts @@ -1,9 +1,8 @@ import { Language, LanguageObject } from "@monkeytype/schemas/languages"; -import { Challenge } from "@monkeytype/schemas/challenges"; import { LayoutObject } from "@monkeytype/schemas/layouts"; -import { toHex } from "./strings"; import { languageHashes } from "virtual:language-hashes"; import { isDevEnvironment } from "./env"; +import { toHex } from "./strings"; //pin implementation const fetch = window.fetch; @@ -154,15 +153,6 @@ export class Section { export type FunboxWordOrder = "normal" | "reverse"; -/** - * Fetches the list of challenges from the server. - * @returns A promise that resolves to the list of challenges. - */ -export async function getChallengeList(): Promise { - const data = await cachedFetchJson("/challenges/_list.json"); - return data; -} - /** * Fetches the list of supporters from the server. * @returns A promise that resolves to the list of supporters. diff --git a/frontend/static/challenges/_list.json b/frontend/static/challenges/_list.json deleted file mode 100644 index a94b95aa6858..000000000000 --- a/frontend/static/challenges/_list.json +++ /dev/null @@ -1,720 +0,0 @@ -[ - { - "name": "oneHourWarrior", - "display": "One Hour Warrior", - "autoRole": true, - "type": "customTime", - "parameters": [3600], - "requirements": { - "time": { - "min": 3600 - } - } - }, - { - "name": "doubleDown", - "display": "Double Down", - "autoRole": true, - "type": "customTime", - "parameters": [7200], - "requirements": { - "time": { - "min": 7200 - } - } - }, - { - "name": "tripleTrouble", - "display": "Triple Trouble", - "autoRole": true, - "type": "customTime", - "parameters": [10800], - "requirements": { - "time": { - "min": 10800 - } - } - }, - { - "name": "quad", - "display": "Quaaaaad", - "autoRole": true, - "type": "customTime", - "parameters": [14400], - "requirements": { - "time": { - "min": 14400 - } - } - }, - { - "name": "8Ball", - "display": "8 Ball", - "type": "customTime", - "parameters": [28800], - "requirements": { - "time": { - "min": 28800 - } - } - }, - { - "name": "theBig12", - "display": "The Big 12", - "type": "customTime", - "parameters": [43200], - "requirements": { - "time": { - "min": 43200 - } - } - }, - { - "name": "1Day", - "display": "1 Day", - "type": "customTime", - "parameters": [86400], - "requirements": { - "time": { - "min": 86400 - } - } - }, - { - "name": "trueSimp", - "display": "True Simp", - "autoRole": true, - "type": "customText", - "parameters": ["miodec", "repeat", 10000, "word", false] - }, - { - "name": "bigramSalad", - "display": "Bigram Salad", - "autoRole": true, - "type": "customText", - "parameters": [ - "to of in it is as at be we he so on an or do if up by my go", - "random", - 100, - "word", - false - ], - "requirements": { - "wpm": { - "min": 100 - } - } - }, - { - "name": "simp", - "display": "Simp", - "autoRole": true, - "type": "customText", - "parameters": ["miodec", "repeat", 1000, "word", false] - }, - { - "name": "antidiseWhat", - "display": "Antidise-what?", - "autoRole": true, - "type": "customText", - "parameters": ["antidisestablishmentarianism", "repeat", 1, "word", false], - "requirements": { - "wpm": { - "min": 200 - } - } - }, - { - "name": "whatsThisWebsiteCalledAgain", - "display": "What's this website called again?", - "autoRole": true, - "type": "customText", - "parameters": ["monkeytype", "repeat", 1000, "word", false] - }, - { - "name": "developd", - "display": "Develop'd", - "autoRole": true, - "type": "customText", - "parameters": ["develop", "repeat", 1000, "word", false] - }, - { - "name": "slowAndSteady", - "display": "Slow and Steady", - "autoRole": true, - "type": "customTime", - "parameters": [300], - "requirements": { - "wpm": { - "exact": 60 - }, - "config": { - "liveSpeedStyle": "off", - "paceCaret": "off" - } - } - }, - { - "name": "speedSpacer", - "display": "Speed Spacer", - "autoRole": true, - "type": "customText", - "parameters": [ - "a b c d e f g h i j k l m n o p q r s t u v w x y z", - "random", - 100, - "word", - false - ], - "requirements": { - "wpm": { - "min": 100 - } - } - }, - { - "name": "iveGotThePower", - "display": "I've got the POWER", - "autoRole": true, - "type": "customText", - "parameters": ["power", "repeat", 10, "word", false], - "requirements": { - "wpm": { - "min": 400 - } - } - }, - { - "name": "accuracyExpert", - "display": "Accuracy Expert", - "autoRole": true, - "type": "accuracy", - "parameters": [], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - }, - "time": { - "min": 600 - } - } - }, - { - "name": "accuracyMaster", - "display": "Accuracy Master", - "autoRole": true, - "type": "accuracy", - "parameters": [], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - }, - "time": { - "min": 1200 - } - } - }, - { - "name": "accuracyGod", - "display": "Accuracy God", - "autoRole": true, - "type": "accuracy", - "parameters": [], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - }, - "time": { - "min": 1800 - } - } - }, - { - "name": "inAGalaxyFarFarAway", - "display": "In a galaxy far far away", - "type": "script", - "parameters": ["episode4.txt", null, ["space_balls"]], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "beepBoop", - "display": "Beep Boop", - "type": "script", - "parameters": ["beepboop.txt", null, ["nospace"]], - "message": "Mininum 45 WPM and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 45 - }, - "acc": { - "min": 100 - }, - "funbox": { - "exact": ["nospace"] - } - } - }, - { - "name": "whosYourDaddy", - "display": "Who's your daddy?", - "type": "script", - "parameters": ["episode5.txt", null, ["space_balls"]], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "itsATrap", - "display": "It's a trap!", - "type": "script", - "parameters": ["episode6.txt", null, ["space_balls"]], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "jolly", - "display": "Jolly", - "autoRole": true, - "type": "script", - "parameters": ["jolly.txt", null, null], - "message": "Minimum 70wpm required.", - "requirements": { - "wpm": { - "min": 70 - } - } - }, - { - "name": "gottaCatchEmAll", - "display": "Gotta catch 'em all", - "autoRole": true, - "type": "script", - "parameters": ["pokemon.txt", null, null] - }, - { - "name": "rapGod", - "display": "Rap God", - "autoRole": true, - "type": "script", - "parameters": ["rapgod.txt", null, null], - "message": "Minimum 85wpm and 90% accuracy required.", - "requirements": { - "wpm": { - "min": 85 - }, - "acc": { - "min": 90 - }, - "afk": { - "max": 5 - } - } - }, - { - "name": "navySeal", - "display": "Navy Seal", - "autoRole": true, - "type": "script", - "parameters": ["navyseal.txt", null, null], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - } - } - }, - { - "name": "littleChef", - "display": "Little Chef", - "type": "script", - "parameters": ["littlechef.txt", null, null] - }, - { - "name": "crosstalk", - "display": "(CROSSTALK)", - "type": "script", - "parameters": ["crosstalk.txt", null, null] - }, - { - "name": "bees", - "display": "Bees!", - "type": "script", - "parameters": ["bees.txt", null, null] - }, - { - "name": "getOffMySwamp", - "display": "Get off my swamp", - "type": "script", - "parameters": ["shrek.txt", null, null] - }, - { - "name": "lookAtMeIAmTheDeveloperNow", - "display": "Look at me. I am the developer now.", - "autoRole": true, - "type": "script", - "parameters": ["sourcecode.txt", null, null] - }, - { - "name": "beLikeWater", - "display": "Be like water", - "type": "funbox", - "parameters": [["layoutfluid"], "time", 60], - "message": "Remember: You need to achieve at least 50 wpm in each layout." - }, - { - "name": "rollercoaster", - "display": "Rollercoaster", - "autoRole": true, - "type": "funbox", - "parameters": [["round_round_baby"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["round_round_baby"] - } - } - }, - { - "name": "oneHourMirror", - "display": "ɿoɿɿim ɿυoʜ ɘno", - "autoRole": true, - "type": "funbox", - "parameters": [["mirror"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["mirror"] - } - } - }, - { - "name": "chooChoo", - "display": "Choo choo", - "autoRole": true, - "type": "funbox", - "parameters": [["choo_choo"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["choo_choo"] - } - } - }, - { - "name": "mnemonist", - "display": "Mnemonist", - "type": "funbox", - "parameters": [["memory"], "words", 25, "master"], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "earfquake", - "display": "Earfquake", - "autoRole": true, - "type": "funbox", - "parameters": [["earthquake"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["earthquake"] - } - } - }, - { - "name": "simonSez", - "display": "Simon Sez", - "autoRole": true, - "type": "funbox", - "parameters": [["simon_says"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["simon_says"] - } - } - }, - { - "name": "accountant", - "display": "Accountant", - "autoRole": true, - "type": "funbox", - "parameters": [["58008"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["58008"] - } - } - }, - { - "name": "hidden", - "display": "Hidden", - "autoRole": true, - "type": "funbox", - "parameters": [["read_ahead"], "time", 60], - "requirements": { - "wpm": { - "min": 100 - }, - "time": { - "min": 60 - }, - "funbox": { - "exact": ["read_ahead"] - }, - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "iCanSeeTheFuture", - "display": "I can see the future", - "autoRole": true, - "type": "funbox", - "parameters": [["read_ahead_hard"], "time", 60], - "requirements": { - "wpm": { - "min": 100 - }, - "time": { - "min": 60 - }, - "funbox": { - "exact": ["read_ahead_hard"] - }, - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "whatAreWordsAtThisPoint", - "display": "What are words at this point?", - "autoRole": true, - "type": "funbox", - "parameters": [["gibberish"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["gibberish"] - } - } - }, - { - "name": "specials", - "display": "Specials", - "autoRole": true, - "type": "funbox", - "parameters": [["specials"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["specials"] - } - } - }, - { - "name": "aeiou", - "display": "Aeiou.", - "autoRole": true, - "type": "funbox", - "parameters": [["tts"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["tts"] - } - } - }, - { - "name": "asciiWarrior", - "display": "ASCII warrior", - "autoRole": true, - "type": "funbox", - "parameters": [["ascii"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["ascii"] - } - } - }, - { - "name": "iKINdaLikEHoWinEFFICIeNtQwErtYIs.", - "display": "i KINda LikE HoW inEFFICIeNt QwErtY Is.", - "autoRole": true, - "type": "funbox", - "parameters": [["sPoNgEcAsE"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["sPoNgEcAsE"] - } - } - }, - { - "name": "oneNauseousMonkey", - "display": "One Nauseous Monkey", - "autoRole": true, - "type": "funbox", - "parameters": [["nausea"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["nausea"] - } - } - }, - { - "name": "thumbWarrior", - "display": "Thumb warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "mouseWarrior", - "display": "Mouse warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "mobileWarrior", - "display": "Mobile warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "69", - "display": "6969696969", - "autoRole": true, - "type": "customTime", - "parameters": [69], - "message": "You need to achieve 69 wpm, 69 raw, 69% accuracy and 69% consistency.", - "requirements": { - "wpm": { - "exact": 69 - }, - "raw": { - "exact": 69 - }, - "acc": { - "exact": 69 - }, - "con": { - "exact": 69 - } - } - }, - { - "name": "upsideDown", - "display": "Upside down", - "type": "customTime", - "parameters": [60] - }, - { - "name": "oneArmedBandit", - "display": "One armed bandit", - "type": "customWords", - "parameters": [10000] - }, - { - "name": "englishMaster", - "display": "English master", - "autoRole": true, - "type": "customTime", - "parameters": [3600], - "requirements": { - "time": { - "min": 3600 - }, - "config": { - "language": "english_10k", - "punctuation": true, - "numbers": true - } - } - }, - { - "name": "feetWarrior", - "display": "Feet warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "wingdings", - "display": "Ten Words of Pain", - "type": "other", - "parameters": [], - "message": "Complete a 10-word Master mode test using the Wingdings custom font. No keymap allowed. Minimum 60 WPM and 100% accuracy required.", - "requirements": { - "acc": { - "exact": 100 - } - } - } -] diff --git a/packages/challenges/.oxlintrc.json b/packages/challenges/.oxlintrc.json new file mode 100644 index 000000000000..f6a8e7c07d0a --- /dev/null +++ b/packages/challenges/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "ignorePatterns": ["node_modules", "dist", ".turbo"], + "extends": [ + "../oxlint-config/index.jsonc" + // "@monkeytype/oxlint-config" + ] +} diff --git a/packages/challenges/__test__/tsconfig.json b/packages/challenges/__test__/tsconfig.json new file mode 100644 index 000000000000..bc5ae47e535d --- /dev/null +++ b/packages/challenges/__test__/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@monkeytype/typescript-config/base.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"] +} diff --git a/packages/challenges/package.json b/packages/challenges/package.json new file mode 100644 index 000000000000..c7b095370a92 --- /dev/null +++ b/packages/challenges/package.json @@ -0,0 +1,34 @@ +{ + "name": "@monkeytype/challenges", + "private": true, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "dev": "tsup-node --watch", + "build": "npm run madge && tsup-node", + "test": "vitest run", + "madge": " madge --circular --extensions ts ./src", + "ts-check": "tsc --noEmit", + "lint": "oxlint . --type-aware --type-check", + "lint-fast": "oxlint ." + }, + "dependencies": { + "@monkeytype/schemas": "workspace:*" + }, + "devDependencies": { + "@monkeytype/tsup-config": "workspace:*", + "@monkeytype/typescript-config": "workspace:*", + "@types/node": "24.9.1", + "madge": "8.0.0", + "oxlint": "1.68.0", + "oxlint-tsgolint": "0.23.0", + "tsup": "8.4.0", + "typescript": "6.0.2", + "vitest": "4.1.0" + } +} diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts new file mode 100644 index 000000000000..f25729675503 --- /dev/null +++ b/packages/challenges/src/index.ts @@ -0,0 +1,564 @@ +import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; + +export const Challenges: Record> = { + "100hours": { + display: "100 hours of typing", + autoRole: false, + type: "hidden", + discordRoleId: "761766710704603166", + parameters: [], + }, + "250hours": { + display: "250 hours of typing", + autoRole: false, + type: "hidden", + discordRoleId: "799825381733433344", + parameters: [], + }, + "500hours": { + display: "500 hours of typing", + autoRole: false, + type: "hidden", + discordRoleId: "951861792622125106", + parameters: [], + }, + "69": { + display: "6969696969", + autoRole: true, + type: "customTime", + parameters: [69], + message: + "You need to achieve 69 wpm, 69 raw, 69% accuracy and 69% consistency.", + requirements: { + wpm: { exact: 69 }, + raw: { exact: 69 }, + acc: { exact: 69 }, + con: { exact: 69 }, + }, + discordRoleId: "749505965174292511", + }, + oneHourWarrior: { + display: "One Hour Warrior", + autoRole: true, + type: "customTime", + parameters: [3600], + requirements: { + time: { min: 3600 }, + }, + discordRoleId: "728371749737201855", + }, + doubleDown: { + display: "Double Down", + autoRole: true, + type: "customTime", + parameters: [7200], + requirements: { + time: { min: 7200 }, + }, + discordRoleId: "732008008514535544", + }, + tripleTrouble: { + display: "Triple Trouble", + autoRole: true, + type: "customTime", + parameters: [10800], + requirements: { + time: { min: 10800 }, + }, + discordRoleId: "732008047618293762", + }, + quad: { + display: "Quaaaaad", + autoRole: true, + type: "customTime", + parameters: [14400], + requirements: { + time: { min: 14400 }, + }, + discordRoleId: "736215666352455801", + }, + "8Ball": { + display: "8 Ball", + type: "customTime", + parameters: [28800], + requirements: { + time: { min: 28800 }, + }, + }, + theBig12: { + display: "The Big 12", + type: "customTime", + parameters: [43200], + requirements: { + time: { min: 43200 }, + }, + }, + "1Day": { + display: "1 Day", + type: "customTime", + parameters: [86400], + requirements: { + time: { min: 86400 }, + }, + }, + trueSimp: { + display: "True Simp", + autoRole: true, + type: "customText", + parameters: ["miodec", "repeat", 10000, "word", false], + discordRoleId: "744328648211038359", + }, + bigramSalad: { + display: "Bigram Salad", + autoRole: true, + type: "customText", + parameters: [ + "to of in it is as at be we he so on an or do if up by my go", + "random", + 100, + "word", + false, + ], + requirements: { + wpm: { min: 100 }, + }, + discordRoleId: "818535054145093652", + }, + simp: { + display: "Simp", + autoRole: true, + type: "customText", + parameters: ["miodec", "repeat", 1000, "word", false], + discordRoleId: "743854992699687023", + }, + antidiseWhat: { + display: "Antidise-what?", + autoRole: true, + type: "customText", + parameters: ["antidisestablishmentarianism", "repeat", 1, "word", false], + requirements: { + wpm: { min: 200 }, + }, + discordRoleId: "782006507360616449", + }, + whatsThisWebsiteCalledAgain: { + display: "What's this website called again?", + autoRole: true, + type: "customText", + parameters: ["monkeytype", "repeat", 1000, "word", false], + discordRoleId: "739276161603076116", + }, + developd: { + display: "Develop'd", + autoRole: true, + type: "customText", + parameters: ["develop", "repeat", 1000, "word", false], + discordRoleId: "735964917877964932", + }, + slowAndSteady: { + display: "Slow and Steady", + autoRole: true, + type: "customTime", + parameters: [300], + requirements: { + wpm: { exact: 60 }, + config: { liveSpeedStyle: "off", paceCaret: "off" }, + }, + discordRoleId: "782005061935956008", + }, + speedSpacer: { + display: "Speed Spacer", + autoRole: true, + type: "customText", + parameters: [ + "a b c d e f g h i j k l m n o p q r s t u v w x y z", + "random", + 100, + "word", + false, + ], + requirements: { + wpm: { min: 100 }, + }, + discordRoleId: "755244049446731856", + }, + iveGotThePower: { + display: "I've got the POWER", + autoRole: true, + type: "customText", + parameters: ["power", "repeat", 10, "word", false], + requirements: { + wpm: { min: 400 }, + }, + discordRoleId: "764879734873915402", + }, + accuracyExpert: { + display: "Accuracy Expert", + autoRole: true, + type: "accuracy", + parameters: [], + message: "Minimum 60wpm and 100% accuracy required.", + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 600 }, + }, + discordRoleId: "751168451263070259", + }, + accuracyMaster: { + display: "Accuracy Master", + autoRole: true, + type: "accuracy", + parameters: [], + message: "Minimum 60wpm and 100% accuracy required.", + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 1200 }, + }, + discordRoleId: "751168567432708239", + }, + accuracyGod: { + display: "Accuracy God", + autoRole: true, + type: "accuracy", + parameters: [], + message: "Minimum 60wpm and 100% accuracy required.", + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 1800 }, + }, + discordRoleId: "751168657626890361", + }, + inAGalaxyFarFarAway: { + display: "In a galaxy far far away", + type: "script", + parameters: ["episode4.txt", null, ["space_balls"]], + requirements: { + config: { tapeMode: "off" }, + }, + }, + beepBoop: { + display: "Beep Boop", + type: "script", + parameters: ["beepboop.txt", null, ["nospace"]], + message: "Mininum 45 WPM and 100% accuracy required.", + requirements: { + wpm: { min: 45 }, + acc: { min: 100 }, + funbox: { exact: ["nospace"] }, + }, + }, + whosYourDaddy: { + display: "Who's your daddy?", + type: "script", + parameters: ["episode5.txt", null, ["space_balls"]], + requirements: { + config: { tapeMode: "off" }, + }, + }, + itsATrap: { + display: "It's a trap!", + type: "script", + parameters: ["episode6.txt", null, ["space_balls"]], + requirements: { + config: { tapeMode: "off" }, + }, + }, + jolly: { + display: "Jolly", + autoRole: true, + type: "script", + parameters: ["jolly.txt", null, null], + message: "Minimum 70wpm required.", + requirements: { + wpm: { min: 70 }, + }, + discordRoleId: "768497412548329563", + }, + gottaCatchEmAll: { + display: "Gotta catch 'em all", + autoRole: true, + type: "script", + parameters: ["pokemon.txt", null, null], + discordRoleId: "767069340599975998", + }, + rapGod: { + display: "Rap God", + autoRole: true, + type: "script", + parameters: ["rapgod.txt", null, null], + message: "Minimum 85wpm and 90% accuracy required.", + requirements: { + wpm: { min: 85 }, + acc: { min: 90 }, + afk: { max: 5 }, + }, + discordRoleId: "743844891045396603", + }, + navySeal: { + display: "Navy Seal", + autoRole: true, + type: "script", + parameters: ["navyseal.txt", null, null], + message: "Minimum 60wpm and 100% accuracy required.", + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + }, + discordRoleId: "762345535969165342", + }, + littleChef: { + display: "Little Chef", + type: "script", + parameters: ["littlechef.txt", null, null], + }, + crosstalk: { + display: "(CROSSTALK)", + type: "script", + parameters: ["crosstalk.txt", null, null], + }, + bees: { + display: "Bees!", + type: "script", + parameters: ["bees.txt", null, null], + }, + getOffMySwamp: { + display: "Get off my swamp", + type: "script", + parameters: ["shrek.txt", null, null], + }, + lookAtMeIAmTheDeveloperNow: { + display: "Look at me. I am the developer now.", + autoRole: true, + type: "script", + parameters: ["sourcecode.txt", null, null], + }, + beLikeWater: { + display: "Be like water", + type: "funbox", + parameters: [["layoutfluid"], "time", 60], + message: "Remember: You need to achieve at least 50 wpm in each layout.", + }, + rollercoaster: { + display: "Rollercoaster", + autoRole: true, + type: "funbox", + parameters: [["round_round_baby"], "time", 3600], + requirements: { + time: { min: 3600 }, + funbox: { exact: ["round_round_baby"] }, + }, + discordRoleId: "736032495526740001", + }, + oneHourMirror: { + display: "ɿoɿɿim ɿυoʜ ɘno", + autoRole: true, + type: "funbox", + parameters: [["mirror"], "time", 3600], + requirements: { + time: { min: 3600 }, + funbox: { exact: ["mirror"] }, + }, + discordRoleId: "737385182998429757", + }, + chooChoo: { + display: "Choo choo", + autoRole: true, + type: "funbox", + parameters: [["choo_choo"], "time", 3600], + requirements: { + time: { min: 3600 }, + funbox: { exact: ["choo_choo"] }, + }, + discordRoleId: "739306439574683710", + }, + mnemonist: { + display: "Mnemonist", + type: "funbox", + parameters: [["memory"], "words", 25, "master"], + requirements: { + config: { tapeMode: "off" }, + }, + }, + earfquake: { + display: "Earfquake", + autoRole: true, + type: "funbox", + parameters: [["earthquake"], "time", 3600], + requirements: { + time: { min: 3600 }, + funbox: { exact: ["earthquake"] }, + }, + discordRoleId: "740730587429601291", + }, + simonSez: { + display: "Simon Sez", + autoRole: true, + type: "funbox", + parameters: [["simon_says"], "time", 3600], + requirements: { + time: { min: 3600 }, + funbox: { exact: ["simon_says"] }, + }, + discordRoleId: "742128871825997914", + }, + accountant: { + display: "Accountant", + autoRole: true, + type: "funbox", + parameters: [["58008"], "time", 3600], + requirements: { + time: { min: 3600 }, + funbox: { exact: ["58008"] }, + }, + discordRoleId: "743962178821816391", + }, + hidden: { + display: "Hidden", + autoRole: true, + type: "funbox", + parameters: [["read_ahead"], "time", 60], + requirements: { + wpm: { min: 100 }, + time: { min: 60 }, + funbox: { exact: ["read_ahead"] }, + config: { tapeMode: "off" }, + }, + discordRoleId: "782006137742557194", + }, + iCanSeeTheFuture: { + display: "I can see the future", + autoRole: true, + type: "funbox", + parameters: [["read_ahead_hard"], "time", 60], + requirements: { + wpm: { min: 100 }, + time: { min: 60 }, + funbox: { exact: ["read_ahead_hard"] }, + config: { tapeMode: "off" }, + }, + discordRoleId: "814877508008411226", + }, + whatAreWordsAtThisPoint: { + display: "What are words at this point?", + autoRole: true, + type: "funbox", + parameters: [["gibberish"], "time", 3600], + requirements: { + time: { min: 60 }, + funbox: { exact: ["gibberish"] }, + }, + discordRoleId: "744209241396740176", + }, + specials: { + display: "Specials", + autoRole: true, + type: "funbox", + parameters: [["specials"], "time", 3600], + requirements: { + time: { min: 60 }, + funbox: { exact: ["specials"] }, + }, + discordRoleId: "744209452714033162", + }, + aeiou: { + display: "Aeiou.", + autoRole: true, + type: "funbox", + parameters: [["tts"], "time", 3600], + requirements: { + time: { min: 60 }, + funbox: { exact: ["tts"] }, + }, + discordRoleId: "744318102766092362", + }, + asciiWarrior: { + display: "ASCII warrior", + autoRole: true, + type: "funbox", + parameters: [["ascii"], "time", 3600], + requirements: { + time: { min: 60 }, + funbox: { exact: ["ascii"] }, + }, + discordRoleId: "746142791326760980", + }, + iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS: { + display: "i KINda LikE HoW inEFFICIeNt QwErtY Is.", + autoRole: true, + type: "funbox", + parameters: [["sPoNgEcAsE"], "time", 3600], + requirements: { + time: { min: 60 }, + funbox: { exact: ["sPoNgEcAsE"] }, + }, + }, + oneNauseousMonkey: { + display: "One Nauseous Monkey", + autoRole: true, + type: "funbox", + parameters: [["nausea"], "time", 3600], + requirements: { + time: { min: 60 }, + funbox: { exact: ["nausea"] }, + }, + discordRoleId: "760930262740631633", + }, + thumbWarrior: { + display: "Thumb warrior", + type: "customTime", + parameters: [3600], + }, + mouseWarrior: { + display: "Mouse warrior", + type: "customTime", + parameters: [3600], + }, + mobileWarrior: { + display: "Mobile warrior", + type: "customTime", + parameters: [3600], + }, + upsideDown: { + display: "Upside down", + type: "customTime", + parameters: [60], + }, + oneArmedBandit: { + display: "One armed bandit", + type: "customWords", + parameters: [10000], + }, + englishMaster: { + display: "English master", + autoRole: true, + type: "customTime", + parameters: [3600], + requirements: { + time: { min: 3600 }, + config: { language: "english_10k", punctuation: true, numbers: true }, + }, + discordRoleId: "751166528824672396", + }, + feetWarrior: { + display: "Feet warrior", + type: "customTime", + parameters: [3600], + }, + wingdings: { + display: "Ten Words of Pain", + type: "other", + parameters: [], + message: + "Complete a 10-word Master mode test using the Wingdings custom font. No keymap allowed. Minimum 60 WPM and 100% accuracy required.", + requirements: { + acc: { exact: 100 }, + }, + }, +}; diff --git a/packages/challenges/tsconfig.json b/packages/challenges/tsconfig.json new file mode 100644 index 000000000000..19dc35bfb3ce --- /dev/null +++ b/packages/challenges/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@monkeytype/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "moduleResolution": "Bundler", + "module": "ES6", + "target": "ES2015", + "lib": ["es2019", "dom"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/challenges/tsup.config.js b/packages/challenges/tsup.config.js new file mode 100644 index 000000000000..28181ee3ec44 --- /dev/null +++ b/packages/challenges/tsup.config.js @@ -0,0 +1,3 @@ +import { extendConfig } from "@monkeytype/tsup-config"; + +export default extendConfig(() => ({ entry: ["src/index.ts"] })); diff --git a/packages/challenges/vitest.config.ts b/packages/challenges/vitest.config.ts new file mode 100644 index 000000000000..481ab6a143b8 --- /dev/null +++ b/packages/challenges/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + passWithNoTests: true, + coverage: { + include: ["**/*.ts"], + }, + }, +}); diff --git a/packages/contracts/src/users.ts b/packages/contracts/src/users.ts index 7a01febb6648..62090ea3aabd 100644 --- a/packages/contracts/src/users.ts +++ b/packages/contracts/src/users.ts @@ -177,6 +177,14 @@ export const EditCustomThemeRequstSchema = z.object({ }); export type EditCustomThemeRequst = z.infer; +export const GetDiscordOauthLinkQuerySchema = z.object({ + includeRoles: z.boolean().optional(), +}); + +export type GetDiscordOauthLinkQuery = z.infer< + typeof GetDiscordOauthLinkQuerySchema +>; + export const GetDiscordOauthLinkResponseSchema = responseWithData( z.object({ url: z.string().url(), @@ -190,6 +198,7 @@ export const LinkDiscordRequestSchema = z.object({ tokenType: z.string(), accessToken: z.string(), state: z.string().length(20), + scope: z.array(z.string()).optional(), }); export type LinkDiscordRequest = z.infer; @@ -663,6 +672,7 @@ export const usersContract = c.router( description: "Start OAuth authentication with discord", method: "GET", path: "/discord/oauth", + query: GetDiscordOauthLinkQuerySchema.strict(), responses: { 200: GetDiscordOauthLinkResponseSchema, }, diff --git a/packages/schemas/src/challenges.ts b/packages/schemas/src/challenges.ts index 8f4611ed74f1..4973caeeb44a 100644 --- a/packages/schemas/src/challenges.ts +++ b/packages/schemas/src/challenges.ts @@ -5,11 +5,84 @@ const MinRequiredNumber = z.object({ min: z.number() }).strict(); const MaxRequiredNumber = z.object({ max: z.number() }).strict(); const ExactRequiredNumber = z.object({ exact: z.number() }).strict(); +import { customEnumErrorHandler } from "./util"; + +export const ChallengeNameSchema = z.enum( + [ + "oneHourWarrior", + "doubleDown", + "tripleTrouble", + "quad", + "8Ball", + "theBig12", + "1Day", + "trueSimp", + "bigramSalad", + "simp", + "antidiseWhat", + "whatsThisWebsiteCalledAgain", + "developd", + "slowAndSteady", + "speedSpacer", + "iveGotThePower", + "accuracyExpert", + "accuracyMaster", + "accuracyGod", + "inAGalaxyFarFarAway", + "beepBoop", + "whosYourDaddy", + "itsATrap", + "jolly", + "gottaCatchEmAll", + "rapGod", + "navySeal", + "littleChef", + "crosstalk", + "bees", + "getOffMySwamp", + "lookAtMeIAmTheDeveloperNow", + "beLikeWater", + "rollercoaster", + "oneHourMirror", + "chooChoo", + "mnemonist", + "earfquake", + "simonSez", + "accountant", + "hidden", + "iCanSeeTheFuture", + "whatAreWordsAtThisPoint", + "specials", + "aeiou", + "asciiWarrior", + "oneNauseousMonkey", + "thumbWarrior", + "mouseWarrior", + "mobileWarrior", + "69", + "upsideDown", + "oneArmedBandit", + "englishMaster", + "feetWarrior", + "wingdings", + "iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS", + "100hours", + "250hours", + "500hours", + ], + { + errorMap: customEnumErrorHandler("Must be a known challenge name"), + }, +); + +export type ChallengeName = z.infer; + export const ChallengeSchema = z .object({ - name: z.string(), + name: ChallengeNameSchema, display: z.string(), autoRole: z.boolean().optional(), + discordRoleId: z.string().optional(), //TODO fix type: z.enum([ "customTime", "customWords", @@ -18,6 +91,7 @@ export const ChallengeSchema = z "accuracy", "funbox", "other", + "hidden", ]), message: z.string().optional(), parameters: z.array( diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index ab0e6c312115..2fac4fc24ba0 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -14,6 +14,7 @@ import { import { CustomThemeColorsSchema, FunboxNameSchema } from "./configs"; import { doesNotContainDisallowedWords } from "./validation/validation"; import { ConnectionSchema } from "./connections"; +import { ChallengeNameSchema } from "./challenges"; export const ResultFilterPresetNameSchema = slug().max(16); @@ -249,6 +250,14 @@ export const UserNameSchema = doesNotContainDisallowedWords( UserNameWithoutFilterSchema, ); +export const UserChallengesSchema = z.record( + ChallengeNameSchema, + z.object({ + addedAt: z.number().int().nonnegative().optional(), + }), +); +export type UserChallenges = z.infer; + export const UserSchema = z.object({ name: UserNameSchema, email: UserEmailSchema, @@ -284,6 +293,7 @@ export const UserSchema = z.object({ quoteMod: QuoteModSchema.optional(), resultFilterPresets: z.array(ResultFiltersSchema).optional(), testActivity: TestActivitySchema.optional(), + challenges: UserChallengesSchema.optional(), }); export type User = z.infer; @@ -312,6 +322,7 @@ export const UserProfileSchema = UserSchema.pick({ inventory: true, allTimeLbs: true, testActivity: true, + challenges: true, }) .extend({ typingStats: TypingStatsSchema, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e970454ffba..76b19160ded2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@date-fns/utc': specifier: 1.2.0 version: 1.2.0 + '@monkeytype/challenges': + specifier: workspace:* + version: link:../packages/challenges '@monkeytype/contracts': specifier: workspace:* version: link:../packages/contracts @@ -288,6 +291,9 @@ importers: '@leonabcd123/modern-caps-lock': specifier: 3.1.3 version: 3.1.3 + '@monkeytype/challenges': + specifier: workspace:* + version: link:../packages/challenges '@monkeytype/contracts': specifier: workspace:* version: link:../packages/contracts @@ -642,6 +648,40 @@ importers: specifier: ^4.1.0 version: 4.1.0(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) + packages/challenges: + dependencies: + '@monkeytype/schemas': + specifier: workspace:* + version: link:../schemas + devDependencies: + '@monkeytype/tsup-config': + specifier: workspace:* + version: link:../tsup-config + '@monkeytype/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/node': + specifier: 24.9.1 + version: 24.9.1 + madge: + specifier: 8.0.0 + version: 8.0.0(typescript@6.0.2) + oxlint: + specifier: 1.68.0 + version: 1.68.0(oxlint-tsgolint@0.23.0) + oxlint-tsgolint: + specifier: 0.23.0 + version: 0.23.0 + tsup: + specifier: 8.4.0 + version: 8.4.0(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vitest: + specifier: 4.1.0 + version: 4.1.0(@types/node@24.9.1)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.25.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) + packages/contracts: dependencies: '@monkeytype/schemas': @@ -680,7 +720,7 @@ importers: version: 6.0.2 vitest: specifier: 4.1.0 - version: 4.1.0(@types/node@24.9.1)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.25.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.0(@types/node@24.9.1)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) packages/funbox: dependencies: @@ -10521,9 +10561,6 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -10914,10 +10951,6 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - tinyexec@1.1.2: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} @@ -22774,8 +22807,6 @@ snapshots: statuses@2.0.2: {} - std-env@4.0.0: {} - std-env@4.1.0: {} stemmer@2.0.1: {} @@ -23344,8 +23375,6 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.0.2: {} - tinyexec@1.1.2: {} tinyglobby@0.2.13: @@ -23962,12 +23991,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: @@ -23992,12 +24021,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -24022,12 +24051,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.25.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -24051,12 +24080,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.70.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -24080,12 +24109,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: From 41bdd82b92b32b4702ca53ca14bf1488e305ddff Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Jun 2026 15:43:56 +0200 Subject: [PATCH 02/12] wip --- .../__tests__/api/controllers/user.spec.ts | 20 ++++++++++++++++--- backend/src/api/controllers/user.ts | 9 +++++++++ .../ts/components/modals/EditProfileModal.tsx | 17 +++++++++++++++- packages/challenges/package.json | 3 ++- .../challenges/scripts/challenge-roles.ts | 16 +++++++++++++++ packages/schemas/src/users.ts | 1 + 6 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 packages/challenges/scripts/challenge-roles.ts diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index 8c197717de1b..ec588441435b 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -2979,6 +2979,9 @@ describe("user controller test", () => { testActivity: { "2024": fillYearWithDay(94), }, + challenges: { + "100hours": {}, + }, }; beforeEach(async () => { @@ -3050,12 +3053,15 @@ describe("user controller test", () => { expect(getUserByNameMock).toHaveBeenCalledWith("bob", "get user profile"); expect(getUserMock).not.toHaveBeenCalled(); }); - it("should get testActivity if enabled", async () => { + it("should get testActivity/challenges if enabled", async () => { //GIVEN vi.useFakeTimers().setSystemTime(1712102400000); getUserByNameMock.mockResolvedValue({ ...foundUser, - profileDetails: { showActivityOnPublicProfile: true }, + profileDetails: { + showActivityOnPublicProfile: true, + showChallengesOnPublicProfile: true, + }, } as any); const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry; leaderboardGetRankMock.mockResolvedValue(rank); @@ -3071,13 +3077,18 @@ describe("user controller test", () => { testsByDays: expect.arrayContaining([]), }), ); + + expect(body.data.challenges).toEqual({ "100hours": {} }); }); it("should not get testActivity if disabled", async () => { //GIVEN vi.useFakeTimers().setSystemTime(1712102400000); getUserByNameMock.mockResolvedValue({ ...foundUser, - profileDetails: { showActivityOnPublicProfile: false }, + profileDetails: { + showActivityOnPublicProfile: false, + showChallengesOnPublicProfile: false, + }, } as any); const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry; leaderboardGetRankMock.mockResolvedValue(rank); @@ -3088,6 +3099,7 @@ describe("user controller test", () => { //THEN expect(body.data.testActivity).toBeUndefined(); + expect(body.data.challenges).toBeUndefined(); }); it("should get base profile for banned user", async () => { @@ -3205,6 +3217,7 @@ describe("user controller test", () => { website: "https://monkeytype.com", }, showActivityOnPublicProfile: false, + showChallengesOnPublicProfile: false, }; //WHEN @@ -3233,6 +3246,7 @@ describe("user controller test", () => { website: "https://monkeytype.com", }, showActivityOnPublicProfile: false, + showChallengesOnPublicProfile: false, }, { badges: [{ id: 4 }, { id: 2, selected: true }, { id: 3 }], diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 3dcb7b417393..1a9ecefaab07 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -1034,6 +1034,13 @@ export async function getProfile( } else { delete profileData.testActivity; } + + if (user.profileDetails?.showChallengesOnPublicProfile) { + profileData.challenges = user.challenges; + } else { + delete profileData.challenges; + } + return new MonkeyResponse("Profile retrieved", profileData); } @@ -1047,6 +1054,7 @@ export async function updateProfile( socialProfiles, selectedBadgeId, showActivityOnPublicProfile, + showChallengesOnPublicProfile, } = req.body; const user = await UserDAL.getPartialUser(uid, "update user profile", [ @@ -1076,6 +1084,7 @@ export async function updateProfile( ]), ), showActivityOnPublicProfile, + showChallengesOnPublicProfile, }; await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory); diff --git a/frontend/src/ts/components/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx index 088f8e5ce2de..756517926a3f 100644 --- a/frontend/src/ts/components/modals/EditProfileModal.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -40,7 +40,9 @@ export function EditProfile() { twitter: snapshot.details?.socialProfiles?.twitter ?? "", website: snapshot.details?.socialProfiles?.website ?? "", showActivityOnPublicProfile: - snapshot.details?.showActivityOnPublicProfile ?? true, + snapshot.details?.showActivityOnPublicProfile, + showChallengesOnPublicProfile: + snapshot.details?.showChallengesOnPublicProfile, badgeId: badges.find((b) => b.selected)?.id ?? -1, }, onSubmit: async ({ value }) => { @@ -53,6 +55,7 @@ export function EditProfile() { website: value.website || undefined, }, showActivityOnPublicProfile: value.showActivityOnPublicProfile, + showChallengesOnPublicProfile: value.showActivityOnPublicProfile, }; const response = await Ape.users.updateProfile({ @@ -259,6 +262,18 @@ export function EditProfile() { +
+ + + {(field) => ( + + )} + +
+ save diff --git a/packages/challenges/package.json b/packages/challenges/package.json index c7b095370a92..a3ffc505242d 100644 --- a/packages/challenges/package.json +++ b/packages/challenges/package.json @@ -15,7 +15,8 @@ "madge": " madge --circular --extensions ts ./src", "ts-check": "tsc --noEmit", "lint": "oxlint . --type-aware --type-check", - "lint-fast": "oxlint ." + "lint-fast": "oxlint .", + "george-mapping": "tsx ./scripts/challenge-roles" }, "dependencies": { "@monkeytype/schemas": "workspace:*" diff --git a/packages/challenges/scripts/challenge-roles.ts b/packages/challenges/scripts/challenge-roles.ts new file mode 100644 index 000000000000..f369ce926dc8 --- /dev/null +++ b/packages/challenges/scripts/challenge-roles.ts @@ -0,0 +1,16 @@ +import { Challenges } from "../src/index"; + +const known: Record = Object.fromEntries( + Object.entries(Challenges) + .filter(([, def]) => def.discordRoleId !== undefined) + .map(([name, def]) => [name, def.discordRoleId] as [string, string]), +); + +const missing = Object.entries(Challenges) + .filter(([, def]) => def.discordRoleId === undefined) + .map(([name]) => name); + +console.log("roleid mapping"); +console.log(JSON.stringify(known, null, 2)); +console.log("missing challenges"); +console.log(JSON.stringify(missing, null, 2)); diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index 2fac4fc24ba0..fec0bb676a2e 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -118,6 +118,7 @@ export const UserProfileDetailsSchema = z .strict() .optional(), showActivityOnPublicProfile: z.boolean().optional(), + showChallengesOnPublicProfile: z.boolean().optional(), }) .strict(); export type UserProfileDetails = z.infer; From 5065845ef285f303218e0e37982d62c20f915db5 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Jun 2026 17:26:13 +0200 Subject: [PATCH 03/12] add missing roleIds --- packages/challenges/src/index.ts | 22 ++++++++++++++++++++++ packages/schemas/src/challenges.ts | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index f25729675503..55f01c425a08 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -84,6 +84,7 @@ export const Challenges: Record> = { requirements: { time: { min: 28800 }, }, + discordRoleId: "736528159956271126", }, theBig12: { display: "The Big 12", @@ -92,6 +93,7 @@ export const Challenges: Record> = { requirements: { time: { min: 43200 }, }, + discordRoleId: "740532256388546581", }, "1Day": { display: "1 Day", @@ -100,6 +102,7 @@ export const Challenges: Record> = { requirements: { time: { min: 86400 }, }, + discordRoleId: "751801958511149057", }, trueSimp: { display: "True Simp", @@ -241,6 +244,7 @@ export const Challenges: Record> = { requirements: { config: { tapeMode: "off" }, }, + discordRoleId: "740004324301602907", }, beepBoop: { display: "Beep Boop", @@ -252,6 +256,7 @@ export const Challenges: Record> = { acc: { min: 100 }, funbox: { exact: ["nospace"] }, }, + discordRoleId: "813076265145729024", }, whosYourDaddy: { display: "Who's your daddy?", @@ -260,6 +265,7 @@ export const Challenges: Record> = { requirements: { config: { tapeMode: "off" }, }, + discordRoleId: "742171915405361204", }, itsATrap: { display: "It's a trap!", @@ -268,6 +274,7 @@ export const Challenges: Record> = { requirements: { config: { tapeMode: "off" }, }, + discordRoleId: "744325174668820550", }, jolly: { display: "Jolly", @@ -317,33 +324,39 @@ export const Challenges: Record> = { display: "Little Chef", type: "script", parameters: ["littlechef.txt", null, null], + discordRoleId: "763544714028122153", }, crosstalk: { display: "(CROSSTALK)", type: "script", parameters: ["crosstalk.txt", null, null], + discordRoleId: "761276009664217129", }, bees: { display: "Bees!", type: "script", parameters: ["bees.txt", null, null], + discordRoleId: "739636003182084307", }, getOffMySwamp: { display: "Get off my swamp", type: "script", parameters: ["shrek.txt", null, null], + discordRoleId: "757346966987342026", }, lookAtMeIAmTheDeveloperNow: { display: "Look at me. I am the developer now.", autoRole: true, type: "script", parameters: ["sourcecode.txt", null, null], + discordRoleId: "937358772635074600", }, beLikeWater: { display: "Be like water", type: "funbox", parameters: [["layoutfluid"], "time", 60], message: "Remember: You need to achieve at least 50 wpm in each layout.", + discordRoleId: "740568679485276201", }, rollercoaster: { display: "Rollercoaster", @@ -385,6 +398,7 @@ export const Challenges: Record> = { requirements: { config: { tapeMode: "off" }, }, + discordRoleId: "782005606852067328", }, earfquake: { display: "Earfquake", @@ -498,6 +512,7 @@ export const Challenges: Record> = { time: { min: 60 }, funbox: { exact: ["sPoNgEcAsE"] }, }, + discordRoleId: "760999194525171724", }, oneNauseousMonkey: { display: "One Nauseous Monkey", @@ -514,26 +529,31 @@ export const Challenges: Record> = { display: "Thumb warrior", type: "customTime", parameters: [3600], + discordRoleId: "761794585109200906", }, mouseWarrior: { display: "Mouse warrior", type: "customTime", parameters: [3600], + discordRoleId: "744580294442614790", }, mobileWarrior: { display: "Mobile warrior", type: "customTime", parameters: [3600], + discordRoleId: "744723801526370407", }, upsideDown: { display: "Upside down", type: "customTime", parameters: [60], + discordRoleId: "782725716114014237", }, oneArmedBandit: { display: "One armed bandit", type: "customWords", parameters: [10000], + discordRoleId: "765919192557682708", }, englishMaster: { display: "English master", @@ -550,6 +570,7 @@ export const Challenges: Record> = { display: "Feet warrior", type: "customTime", parameters: [3600], + discordRoleId: "751953592860147822", }, wingdings: { display: "Ten Words of Pain", @@ -560,5 +581,6 @@ export const Challenges: Record> = { requirements: { acc: { exact: 100 }, }, + discordRoleId: "863192575984140338", }, }; diff --git a/packages/schemas/src/challenges.ts b/packages/schemas/src/challenges.ts index 4973caeeb44a..a1023ea22bb3 100644 --- a/packages/schemas/src/challenges.ts +++ b/packages/schemas/src/challenges.ts @@ -82,7 +82,7 @@ export const ChallengeSchema = z name: ChallengeNameSchema, display: z.string(), autoRole: z.boolean().optional(), - discordRoleId: z.string().optional(), //TODO fix + discordRoleId: z.string(), type: z.enum([ "customTime", "customWords", From 9ee4c982c63593eb5cbaed0e3fa6c374b2cf6df6 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Jun 2026 17:39:54 +0200 Subject: [PATCH 04/12] missing roleIds --- backend/__tests__/api/controllers/user.spec.ts | 2 +- backend/src/api/controllers/user.ts | 2 +- frontend/src/ts/auth.tsx | 2 +- .../src/ts/commandline/commandline-metadata.ts | 2 +- .../src/ts/commandline/lists/load-challenge.ts | 2 +- .../src/ts/components/common/AsyncContent.tsx | 2 +- frontend/src/ts/components/modals/SimpleModal.tsx | 2 +- frontend/src/ts/components/pages/account/utils.ts | 3 ++- frontend/src/ts/config/lifecycle.ts | 3 ++- frontend/src/ts/config/setters.ts | 3 ++- frontend/src/ts/config/utils.ts | 2 +- .../src/ts/controllers/challenge-controller.ts | 8 +++----- frontend/src/ts/controllers/chart-controller.ts | 3 ++- frontend/src/ts/utils/misc.ts | 12 ------------ packages/challenges/scripts/challenge-roles.ts | 12 +++--------- packages/util/src/objects.ts | 15 +++++++++++++++ 16 files changed, 37 insertions(+), 38 deletions(-) create mode 100644 packages/util/src/objects.ts diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index ec588441435b..eeee13c1006d 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -1604,7 +1604,7 @@ describe("user controller test", () => { avatar: "discordUserAvatar", }); getDiscordRoleIdsMock.mockResolvedValue([ - Challenges["100hours"].discordRoleId as string, + Challenges["100hours"].discordRoleId, ]); isDiscordIdAvailableMock.mockResolvedValue(true); blocklistContainsMock.mockResolvedValue(false); diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 1a9ecefaab07..717cd00f9115 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -103,7 +103,7 @@ const challengeNameByRoleId: Record = Object.fromEntries( Object.entries(Challenges) .filter(([_, challenge]) => challenge.discordRoleId !== undefined) .map(([name, challenge]) => [ - challenge.discordRoleId as string, + challenge.discordRoleId, name as ChallengeName, ]), ); diff --git a/frontend/src/ts/auth.tsx b/frontend/src/ts/auth.tsx index 52b7231bd230..f73f7a0c6480 100644 --- a/frontend/src/ts/auth.tsx +++ b/frontend/src/ts/auth.tsx @@ -1,4 +1,5 @@ import { PasswordSchema } from "@monkeytype/schemas/users"; +import { typedKeys } from "@monkeytype/util/objects"; import { tryCatch } from "@monkeytype/util/trycatch"; import { FirebaseError } from "firebase/app"; import { @@ -41,7 +42,6 @@ import { } from "./states/notifications"; import { isDevEnvironment } from "./utils/env"; import { createErrorMessage } from "./utils/error"; -import { typedKeys } from "./utils/misc"; import { SnapshotInitError } from "./utils/snapshot-init-error"; import { OneOf } from "./utils/types"; diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts index 71b01937d653..5d479fd2309a 100644 --- a/frontend/src/ts/commandline/commandline-metadata.ts +++ b/frontend/src/ts/commandline/commandline-metadata.ts @@ -13,8 +13,8 @@ import { getActivePage, isAuthenticated } from "../states/core"; import { Fonts } from "../constants/fonts"; import { KnownFontName } from "@monkeytype/schemas/fonts"; import * as UI from "../ui"; -import { typedKeys } from "../utils/misc"; import { Validation } from "../types/validation"; +import { typedKeys } from "@monkeytype/util/objects"; //TODO: remove display property and instead use optionsMetadata from configMetadata // eventually this file should be fully merged into config metadata, probably under the 'commandline' property diff --git a/frontend/src/ts/commandline/lists/load-challenge.ts b/frontend/src/ts/commandline/lists/load-challenge.ts index 1d657a32647d..3deea21072ae 100644 --- a/frontend/src/ts/commandline/lists/load-challenge.ts +++ b/frontend/src/ts/commandline/lists/load-challenge.ts @@ -4,7 +4,7 @@ import { navigate } from "../../controllers/route-controller"; import * as TestLogic from "../../test/test-logic"; import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; import { Command, CommandsSubgroup } from "../types"; -import { typedEntries } from "../../utils/misc"; +import { typedEntries } from "@monkeytype/util/objects"; const subgroup: CommandsSubgroup = { title: "Load challenge...", diff --git a/frontend/src/ts/components/common/AsyncContent.tsx b/frontend/src/ts/components/common/AsyncContent.tsx index 1309377dc4ca..d2b5a4d019f9 100644 --- a/frontend/src/ts/components/common/AsyncContent.tsx +++ b/frontend/src/ts/components/common/AsyncContent.tsx @@ -1,3 +1,4 @@ +import { typedKeys } from "@monkeytype/util/objects"; import { UseQueryResult } from "@tanstack/solid-query"; import { Accessor, @@ -12,7 +13,6 @@ import { import { showErrorNotification } from "../../states/notifications"; import { createErrorMessage } from "../../utils/error"; -import { typedKeys } from "../../utils/misc"; import { LoadingCircle } from "./LoadingCircle"; type AsyncEntry = { diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index d07efad88d59..c7a0337944ae 100644 --- a/frontend/src/ts/components/modals/SimpleModal.tsx +++ b/frontend/src/ts/components/modals/SimpleModal.tsx @@ -1,3 +1,4 @@ +import { typedEntries } from "@monkeytype/util/objects"; import { AnyFieldApi, createForm } from "@tanstack/solid-form"; import { Accessor, @@ -25,7 +26,6 @@ import { SimpleModalInput, } from "../../states/simple-modal"; import { cn } from "../../utils/cn"; -import { typedEntries } from "../../utils/misc"; import { getZodType, unwrapSchema } from "../../utils/zod"; import { AnimatedModal } from "../common/AnimatedModal"; import { Checkbox } from "../ui/form/Checkbox"; diff --git a/frontend/src/ts/components/pages/account/utils.ts b/frontend/src/ts/components/pages/account/utils.ts index 096bfe99631f..e251099295c0 100644 --- a/frontend/src/ts/components/pages/account/utils.ts +++ b/frontend/src/ts/components/pages/account/utils.ts @@ -1,7 +1,8 @@ import { ResultFilters, ResultFiltersSchema } from "@monkeytype/schemas/users"; -import { typedKeys } from "../../../utils/misc"; + import defaultResultFilters from "../../../constants/default-result-filters"; import { sanitize } from "../../../utils/sanitize"; +import { typedKeys } from "@monkeytype/util/objects"; export function mergeWithDefaultFilters( filters: Partial, diff --git a/frontend/src/ts/config/lifecycle.ts b/frontend/src/ts/config/lifecycle.ts index 4f88247173e4..1ef978c67b1a 100644 --- a/frontend/src/ts/config/lifecycle.ts +++ b/frontend/src/ts/config/lifecycle.ts @@ -13,9 +13,10 @@ import { Config, setFullConfigStore } from "./store"; import { getDefaultConfig } from "../constants/default-config"; import { configEvent } from "../events/config"; import { migrateConfig } from "./utils"; -import { promiseWithResolvers, typedKeys } from "../utils/misc"; +import { promiseWithResolvers } from "../utils/misc"; import { setConfig } from "./setters"; import { deleteConfig } from "../ape/config"; +import { typedKeys } from "@monkeytype/util/objects"; export async function applyConfigFromJson(json: string): Promise { try { diff --git a/frontend/src/ts/config/setters.ts b/frontend/src/ts/config/setters.ts index de181296161f..f571baa401be 100644 --- a/frontend/src/ts/config/setters.ts +++ b/frontend/src/ts/config/setters.ts @@ -10,10 +10,11 @@ import { canSetFunboxWithConfig, } from "./funbox-validation"; import * as TestState from "../test/test-state"; -import { typedKeys, triggerResize, escapeHTML } from "../utils/misc"; +import { triggerResize, escapeHTML } from "../utils/misc"; import { camelCaseToWords, capitalizeFirstLetter } from "../utils/strings"; import { Config, setConfigStore } from "./store"; import { FunboxName } from "@monkeytype/schemas/configs"; +import { typedKeys } from "@monkeytype/util/objects"; export function setConfig( key: T, diff --git a/frontend/src/ts/config/utils.ts b/frontend/src/ts/config/utils.ts index 9621b2e717a3..006f7ecd5d85 100644 --- a/frontend/src/ts/config/utils.ts +++ b/frontend/src/ts/config/utils.ts @@ -4,11 +4,11 @@ import type { PartialConfig, FunboxName, } from "@monkeytype/schemas/configs"; -import { typedKeys } from "../utils/misc"; import { sanitize } from "../utils/sanitize"; import * as ConfigSchemas from "@monkeytype/schemas/configs"; import { getDefaultConfig } from "../constants/default-config"; import { Config } from "./store"; +import { typedKeys } from "@monkeytype/util/objects"; /** * migrates possible outdated config and merges with the default config values * @param config partial or possible outdated config diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 4b08fdc25606..94d611ddbe48 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -5,7 +5,6 @@ import { } from "../states/notifications"; import * as CustomText from "../test/custom-text"; import * as Funbox from "../test/funbox/funbox"; -import * as Misc from "../utils/misc"; import { setConfig } from "../config/setters"; import { Config } from "../config/store"; @@ -27,6 +26,7 @@ import { hideLoaderBar, showLoaderBar } from "../states/loader-bar"; import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; import { areUnsortedArraysEqual } from "../utils/arrays"; import { qs } from "../utils/dom"; +import { typedKeys } from "@monkeytype/util/objects"; let challengeLoading = false; @@ -136,7 +136,7 @@ function verifyRequirement( } } else if (requirementType === "config" && requirements.config) { const requirementValue = requirements.config; - for (const configKey of Misc.typedKeys(requirementValue)) { + for (const configKey of typedKeys(requirementValue)) { const configValue = requirementValue[configKey]; if (Config[configKey as keyof ConfigType] !== configValue) { requirementsMet = false; @@ -166,9 +166,7 @@ export function verify(result: CompletedEvent): string | null { } else { let requirementsMet = true; const failReasons: string[] = []; - for (const requirementType of Misc.typedKeys( - loadedChallenge.requirements, - )) { + for (const requirementType of typedKeys(loadedChallenge.requirements)) { const [passed, requirementFailReasons] = verifyRequirement( result, loadedChallenge.requirements, diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index 636a0c51e09e..01ea9b02b32f 100644 --- a/frontend/src/ts/controllers/chart-controller.ts +++ b/frontend/src/ts/controllers/chart-controller.ts @@ -60,12 +60,13 @@ import { Config } from "../config/store"; import { configEvent } from "../events/config"; import * as Arrays from "../utils/arrays"; import { blendTwoHexColors } from "../utils/colors"; -import { typedKeys } from "../utils/misc"; + import { getTheme } from "../states/theme"; import { Theme } from "../constants/themes"; import { createDebouncedEffectOn } from "../hooks/effects"; import { getWordIndexesForSecond } from "../test/events/stats"; import { lastEventLog } from "../test/test-state"; +import { typedKeys } from "@monkeytype/util/objects"; export class ChartWithUpdateColors< TType extends ChartType = ChartType, diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 6480e38ae000..669d93b3bba5 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -473,18 +473,6 @@ export function getBoundingRectOfElements(elements: HTMLElement[]): DOMRect { }; } -export function typedKeys( - obj: T, -): T extends T ? (keyof T)[] : never { - return Object.keys(obj) as unknown as T extends T ? (keyof T)[] : never; -} - -export function typedEntries( - obj: T, -): { [K in keyof T]: [K, T[K]] }[keyof T][] { - return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]; -} - export function reloadAfter(seconds: number): void { setTimeout(() => { window.location.reload(); diff --git a/packages/challenges/scripts/challenge-roles.ts b/packages/challenges/scripts/challenge-roles.ts index f369ce926dc8..dd27ec4b8042 100644 --- a/packages/challenges/scripts/challenge-roles.ts +++ b/packages/challenges/scripts/challenge-roles.ts @@ -1,16 +1,10 @@ import { Challenges } from "../src/index"; const known: Record = Object.fromEntries( - Object.entries(Challenges) - .filter(([, def]) => def.discordRoleId !== undefined) - .map(([name, def]) => [name, def.discordRoleId] as [string, string]), + Object.entries(Challenges).map( + ([name, def]) => [name, def.discordRoleId] as [string, string], + ), ); -const missing = Object.entries(Challenges) - .filter(([, def]) => def.discordRoleId === undefined) - .map(([name]) => name); - console.log("roleid mapping"); console.log(JSON.stringify(known, null, 2)); -console.log("missing challenges"); -console.log(JSON.stringify(missing, null, 2)); diff --git a/packages/util/src/objects.ts b/packages/util/src/objects.ts new file mode 100644 index 000000000000..35a295988db7 --- /dev/null +++ b/packages/util/src/objects.ts @@ -0,0 +1,15 @@ +export function typedKeys( + obj: T, +): T extends T ? (keyof T)[] : never { + return Object.keys(obj) as unknown as T extends T ? (keyof T)[] : never; +} + +export function typedEntries( + obj: T, +): { [K in keyof T]: [K, T[K]] }[keyof T][] { + return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]; +} + +export function typedValues(obj: T): T[keyof T][] { + return Object.values(obj) as T[keyof T][]; +} From e258cb25383cd43a39aa5de126994a3ee81b3ee7 Mon Sep 17 00:00:00 2001 From: Shizuko <83967781+ShizukoV@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:59:15 -0500 Subject: [PATCH 05/12] impr(challenges): Updates to Tracking Challenges (@ShizukoV) (#8151) Updated various challenges by adding their category and description, along with their Discord Role ID if it was missing. Also added every missing challenge role along with their Discord Role ID's. --- packages/challenges/src/index.ts | 445 +++++++++++++++++++++++++++++-- 1 file changed, 430 insertions(+), 15 deletions(-) diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index 55f01c425a08..954489186c63 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -2,24 +2,39 @@ import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; export const Challenges: Record> = { "100hours": { - display: "100 hours of typing", + display: "100 hours", autoRole: false, type: "hidden", discordRoleId: "761766710704603166", + category: "otherChallenges", + description: "Achieve 100 hours of typing.", parameters: [], }, "250hours": { - display: "250 hours of typing", + display: "250 hours", autoRole: false, type: "hidden", discordRoleId: "799825381733433344", + category: "otherChallenges", + description: "Achieve 250 hours of typing.", parameters: [], }, "500hours": { - display: "500 hours of typing", + display: "500 hours", autoRole: false, type: "hidden", discordRoleId: "951861792622125106", + category: "otherChallenges", + description: "Achieve 500 hours of typing.", + parameters: [], + }, + "1000hours": { + display: "1000 hours", + autoRole: false, + type: "hidden", + discordRoleId: "1262175323588395100", + category: "otherChallenges", + description: "Achieve 1000 hours of typing.", parameters: [], }, "69": { @@ -36,6 +51,8 @@ export const Challenges: Record> = { con: { exact: 69 }, }, discordRoleId: "749505965174292511", + category: "otherChallenges", + description: "Complete a 69-second test and achieve 69 WPM, 69 raw, 69% accuracy, and 69% consistency.", }, oneHourWarrior: { display: "One Hour Warrior", @@ -46,6 +63,8 @@ export const Challenges: Record> = { time: { min: 3600 }, }, discordRoleId: "728371749737201855", + category: "classicEndurance", + description: "Complete a one-hour test.", }, doubleDown: { display: "Double Down", @@ -56,6 +75,8 @@ export const Challenges: Record> = { time: { min: 7200 }, }, discordRoleId: "732008008514535544", + category: "classicEndurance", + description: "Complete a two-hour test.", }, tripleTrouble: { display: "Triple Trouble", @@ -66,6 +87,8 @@ export const Challenges: Record> = { time: { min: 10800 }, }, discordRoleId: "732008047618293762", + category: "classicEndurance", + description: "Complete a three-hour test.", }, quad: { display: "Quaaaaad", @@ -76,6 +99,8 @@ export const Challenges: Record> = { time: { min: 14400 }, }, discordRoleId: "736215666352455801", + category: "classicEndurance", + description: "Complete a four-hour test.", }, "8Ball": { display: "8 Ball", @@ -85,6 +110,8 @@ export const Challenges: Record> = { time: { min: 28800 }, }, discordRoleId: "736528159956271126", + category: "classicEndurance", + description: "Complete an eight-hour test.", }, theBig12: { display: "The Big 12", @@ -94,6 +121,8 @@ export const Challenges: Record> = { time: { min: 43200 }, }, discordRoleId: "740532256388546581", + category: "classicEndurance", + description: "Complete a twelve-hour test.", }, "1Day": { display: "1 Day", @@ -103,6 +132,8 @@ export const Challenges: Record> = { time: { min: 86400 }, }, discordRoleId: "751801958511149057", + category: "classicEndurance", + description: "Complete a twenty-four-hour test.", }, trueSimp: { display: "True Simp", @@ -110,6 +141,8 @@ export const Challenges: Record> = { type: "customText", parameters: ["miodec", "repeat", 10000, "word", false], discordRoleId: "744328648211038359", + category: "specificText", + description: "Type miodec ten thousand times.", }, bigramSalad: { display: "Bigram Salad", @@ -126,6 +159,8 @@ export const Challenges: Record> = { wpm: { min: 100 }, }, discordRoleId: "818535054145093652", + category: "speed", + description: "Get 100 WPM on a randomized, 100-word custom test with the words list: to of in it is as at be we he so on an or do if up by my go.", }, simp: { display: "Simp", @@ -133,6 +168,19 @@ export const Challenges: Record> = { type: "customText", parameters: ["miodec", "repeat", 1000, "word", false], discordRoleId: "743854992699687023", + category: "specificText", + description: "Type miodec one thousand times.", + }, + simpLord: { + display: "Simp Lord", + // false for now + autoRole: false, + type: "customText", + // this would be 100k times: + parameters: ["miodec", "repeat", 100000, "word", false], + discordRoleId: "984911956949479445", + category: "specificText", + description: "Type miodec one hundred thousand times.", }, antidiseWhat: { display: "Antidise-what?", @@ -143,6 +191,8 @@ export const Challenges: Record> = { wpm: { min: 200 }, }, discordRoleId: "782006507360616449", + category: "specificText", + description: "Get at least 200 wpm typing antidisestablishmentarianism.", }, whatsThisWebsiteCalledAgain: { display: "What's this website called again?", @@ -150,6 +200,8 @@ export const Challenges: Record> = { type: "customText", parameters: ["monkeytype", "repeat", 1000, "word", false], discordRoleId: "739276161603076116", + category: "specificText", + description: "Type monkeytype one thousand times.", }, developd: { display: "Develop'd", @@ -157,6 +209,8 @@ export const Challenges: Record> = { type: "customText", parameters: ["develop", "repeat", 1000, "word", false], discordRoleId: "735964917877964932", + category: "specificText", + description: "Type develop one thousand times.", }, slowAndSteady: { display: "Slow and Steady", @@ -168,6 +222,8 @@ export const Challenges: Record> = { config: { liveSpeedStyle: "off", paceCaret: "off" }, }, discordRoleId: "782005061935956008", + category: "speed", + description: "Complete a 5-minute test with exactly 60 WPM without using the live WPM or pace caret.", }, speedSpacer: { display: "Speed Spacer", @@ -184,6 +240,8 @@ export const Challenges: Record> = { wpm: { min: 100 }, }, discordRoleId: "755244049446731856", + category: "speed", + description: "Get 100 wpm on a randomised custom test with the input: a b c d e f g h i j k l m n o p q r s t u v w x y z (the alphabet) and a word count of 100.", }, iveGotThePower: { display: "I've got the POWER", @@ -194,6 +252,8 @@ export const Challenges: Record> = { wpm: { min: 400 }, }, discordRoleId: "764879734873915402", + category: "speed", + description: "Get 400 WPM while typing power 10 times.", }, accuracyExpert: { display: "Accuracy Expert", @@ -208,6 +268,8 @@ export const Challenges: Record> = { time: { min: 600 }, }, discordRoleId: "751168451263070259", + category: "accuracy", + description: "Complete a 10-minute Master mode test.", }, accuracyMaster: { display: "Accuracy Master", @@ -222,6 +284,8 @@ export const Challenges: Record> = { time: { min: 1200 }, }, discordRoleId: "751168567432708239", + category: "accuracy", + description: "Complete a 20-minute Master mode test.", }, accuracyGod: { display: "Accuracy God", @@ -236,15 +300,19 @@ export const Challenges: Record> = { time: { min: 1800 }, }, discordRoleId: "751168657626890361", + category: "accuracy", + description: "Complete a 30-minute Master mode test.", }, inAGalaxyFarFarAway: { - display: "In a galaxy far far away", + display: "In a galaxy far, far away", type: "script", parameters: ["episode4.txt", null, ["space_balls"]], requirements: { config: { tapeMode: "off" }, }, discordRoleId: "740004324301602907", + category: "scriptsOrSongs", + description: "Type out the entire Star Wars Episode 4 script with punctuation while watching the movie simultaneously.", }, beepBoop: { display: "Beep Boop", @@ -257,24 +325,30 @@ export const Challenges: Record> = { funbox: { exact: ["nospace"] }, }, discordRoleId: "813076265145729024", + category: "scriptsOrSongs", + description: "Type the beepboop script with 100% accuracy and at least 45 WPM.", }, whosYourDaddy: { - display: "Who's your daddy?", + display: "Who's your daddy", type: "script", parameters: ["episode5.txt", null, ["space_balls"]], requirements: { config: { tapeMode: "off" }, }, discordRoleId: "742171915405361204", + category: "scriptsOrSongs", + description: "Type out the entire Star Wars Episode 5 script with punctuation while watching the movie simultaneously.", }, itsATrap: { - display: "It's a trap!", + display: "It's a trap!!", type: "script", parameters: ["episode6.txt", null, ["space_balls"]], requirements: { config: { tapeMode: "off" }, }, discordRoleId: "744325174668820550", + category: "scriptsOrSongs", + description: "Type out the entire Star Wars Episode 6 script with punctuation while watching the movie simultaneously.", }, jolly: { display: "Jolly", @@ -286,13 +360,17 @@ export const Challenges: Record> = { wpm: { min: 70 }, }, discordRoleId: "768497412548329563", + category: "scriptsOrSongs", + description: "Type the Jolly script with a minimum of 70 wpm.", }, gottaCatchEmAll: { - display: "Gotta catch 'em all", + display: "Gotta Catch 'Em All", autoRole: true, type: "script", parameters: ["pokemon.txt", null, null], discordRoleId: "767069340599975998", + category: "scriptsOrSongs", + description: "Type out the names of all Pokemon.", }, rapGod: { display: "Rap God", @@ -306,6 +384,8 @@ export const Challenges: Record> = { afk: { max: 5 }, }, discordRoleId: "743844891045396603", + category: "scriptsOrSongs", + description: "Type out the lyrics of Eminem's Rap God at a minimum of 85 WPM and 90% accuracy, including punctuation.", }, navySeal: { display: "Navy Seal", @@ -319,30 +399,48 @@ export const Challenges: Record> = { afk: { max: 5 }, }, discordRoleId: "762345535969165342", + category: "scriptsOrSongs", + description: "Type out the Navy Seal copy pasta with 100% accuracy and minimum 60 WPM.", }, littleChef: { display: "Little Chef", type: "script", parameters: ["littlechef.txt", null, null], discordRoleId: "763544714028122153", + category: "scriptsOrSongs", + description: "Type out the entire Ratatouille script while watching the movie simultaneously.", }, crosstalk: { display: "(CROSSTALK)", type: "script", parameters: ["crosstalk.txt", null, null], discordRoleId: "761276009664217129", + category: "scriptsOrSongs", + description: "Type out the entire transcript of the first 2020 Presidential Debate.", }, bees: { - display: "Bees!", + display: "Bees!!!", type: "script", parameters: ["bees.txt", null, null], discordRoleId: "739636003182084307", + category: "scriptsOrSongs", + description: "Type out the entire Bee Movie script while watching the movie simultaneously.", }, getOffMySwamp: { - display: "Get off my swamp", + display: "Get Off My Swamp", type: "script", parameters: ["shrek.txt", null, null], discordRoleId: "757346966987342026", + category: "scriptsOrSongs", + description: "Type out the entire Shrek script with punctuation while watching the movie simultaneously.", + }, + fiftyShadesOfHell: { + display: "50 Shades of Hell", + type: "script", + parameters: [null, null, null], + discordRoleId: "751802155119280128", + category: "scriptsOrSongs", + description: "Type out your favourite chapter from 50 Shades of Gray.", }, lookAtMeIAmTheDeveloperNow: { display: "Look at me. I am the developer now.", @@ -350,13 +448,17 @@ export const Challenges: Record> = { type: "script", parameters: ["sourcecode.txt", null, null], discordRoleId: "937358772635074600", + category: "scriptsOrSongs", + description: "Type out the entire source code of Monkeytype, as it was in February 2022.", }, beLikeWater: { - display: "Be like water", + display: "Be Like Water", type: "funbox", parameters: [["layoutfluid"], "time", 60], message: "Remember: You need to achieve at least 50 wpm in each layout.", discordRoleId: "740568679485276201", + category: "funbox", + description: "Achieve at least 50 WPM in all three layouts in a 60-second time test using the layoutfluid mode. Layouts must be unique (e.g., QWERTY, Colemak, Dvorak).", }, rollercoaster: { display: "Rollercoaster", @@ -368,6 +470,8 @@ export const Challenges: Record> = { funbox: { exact: ["round_round_baby"] }, }, discordRoleId: "736032495526740001", + category: "funbox", + description: "Complete at least a one-hour test using the round round baby mode.", }, oneHourMirror: { display: "ɿoɿɿim ɿυoʜ ɘno", @@ -379,6 +483,8 @@ export const Challenges: Record> = { funbox: { exact: ["mirror"] }, }, discordRoleId: "737385182998429757", + category: "funbox", + description: "Complete at least a one-hour test using the mirror mode.", }, chooChoo: { display: "Choo choo", @@ -390,6 +496,8 @@ export const Challenges: Record> = { funbox: { exact: ["choo_choo"] }, }, discordRoleId: "739306439574683710", + category: "funbox", + description: "Complete at least a one-hour test using choo choo mode.", }, mnemonist: { display: "Mnemonist", @@ -399,6 +507,8 @@ export const Challenges: Record> = { config: { tapeMode: "off" }, }, discordRoleId: "782005606852067328", + category: "funbox", + description: "Achieve 100+ WPM with 100% accuracy on a 25-word test using the memory funbox.", }, earfquake: { display: "Earfquake", @@ -410,6 +520,8 @@ export const Challenges: Record> = { funbox: { exact: ["earthquake"] }, }, discordRoleId: "740730587429601291", + category: "funbox", + description: "Complete at least a one-hour test using the earthquake funbox mode.", }, simonSez: { display: "Simon Sez", @@ -421,6 +533,8 @@ export const Challenges: Record> = { funbox: { exact: ["simon_says"] }, }, discordRoleId: "742128871825997914", + category: "funbox", + description: "Complete at least a one-hour test using the simon says funbox mode.", }, accountant: { display: "Accountant", @@ -432,6 +546,8 @@ export const Challenges: Record> = { funbox: { exact: ["58008"] }, }, discordRoleId: "743962178821816391", + category: "funbox", + description: "Complete at least a one-hour test using the 58008 funbox mode.", }, hidden: { display: "Hidden", @@ -445,6 +561,8 @@ export const Challenges: Record> = { config: { tapeMode: "off" }, }, discordRoleId: "782006137742557194", + category: "funbox", + description: "Achieve 100+ WPM using the read ahead funbox on a 60-second test.", }, iCanSeeTheFuture: { display: "I can see the future", @@ -458,9 +576,11 @@ export const Challenges: Record> = { config: { tapeMode: "off" }, }, discordRoleId: "814877508008411226", + category: "funbox", + description: "Achieve 100+ WPM using the read ahead hard funbox on a 60-second test.", }, whatAreWordsAtThisPoint: { - display: "What are words at this point?", + display: "What are words at this point", autoRole: true, type: "funbox", parameters: [["gibberish"], "time", 3600], @@ -469,6 +589,8 @@ export const Challenges: Record> = { funbox: { exact: ["gibberish"] }, }, discordRoleId: "744209241396740176", + category: "funbox", + description: "Complete at least a one-hour test using the gibberish funbox mode.", }, specials: { display: "Specials", @@ -480,6 +602,8 @@ export const Challenges: Record> = { funbox: { exact: ["specials"] }, }, discordRoleId: "744209452714033162", + category: "funbox", + description: "Complete at least a one-hour test using the specials funbox mode.", }, aeiou: { display: "Aeiou.", @@ -491,6 +615,8 @@ export const Challenges: Record> = { funbox: { exact: ["tts"] }, }, discordRoleId: "744318102766092362", + category: "funbox", + description: "Complete at least a one-hour test using the tts funbox mode.", }, asciiWarrior: { display: "ASCII warrior", @@ -502,6 +628,8 @@ export const Challenges: Record> = { funbox: { exact: ["ascii"] }, }, discordRoleId: "746142791326760980", + category: "funbox", + description: "Complete at least a one-hour test using the ascii funbox mode.", }, iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS: { display: "i KINda LikE HoW inEFFICIeNt QwErtY Is.", @@ -513,6 +641,8 @@ export const Challenges: Record> = { funbox: { exact: ["sPoNgEcAsE"] }, }, discordRoleId: "760999194525171724", + category: "funbox", + description: "Complete at least a one-hour test using the randomcase funbox mode.", }, oneNauseousMonkey: { display: "One Nauseous Monkey", @@ -524,36 +654,48 @@ export const Challenges: Record> = { funbox: { exact: ["nausea"] }, }, discordRoleId: "760930262740631633", + category: "funbox", + description: "Complete at least a one-hour test using the nausea funbox mode.", }, thumbWarrior: { - display: "Thumb warrior", + display: "Thumb Warrior", type: "customTime", parameters: [3600], discordRoleId: "761794585109200906", + category: "otherChallenges", + description: "Complete a one-hour test using only your thumbs.", }, mouseWarrior: { display: "Mouse warrior", type: "customTime", parameters: [3600], discordRoleId: "744580294442614790", + category: "otherChallenges", + description: "Complete a one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", }, mobileWarrior: { display: "Mobile warrior", type: "customTime", parameters: [3600], discordRoleId: "744723801526370407", + category: "otherChallenges", + description: "Complete a one-hour test on mobile.", }, upsideDown: { - display: "Upside down", + display: "uʍop ǝpᴉsdn", type: "customTime", parameters: [60], discordRoleId: "782725716114014237", + category: "otherChallenges", + description: "Achieve at least 60 WPM on a one-minute test with your keyboard upside down.", }, oneArmedBandit: { display: "One armed bandit", type: "customWords", parameters: [10000], discordRoleId: "765919192557682708", + category: "otherChallenges", + description: "Complete a one-hour or 10k words test (whichever comes sooner, using an external timer) using a one-handed words list (either left or right) for your layout.", }, englishMaster: { display: "English master", @@ -565,12 +707,16 @@ export const Challenges: Record> = { config: { language: "english_10k", punctuation: true, numbers: true }, }, discordRoleId: "751166528824672396", + category: "otherChallenges", + description: "Complete a one-hour test using English 10k language with punctuation and numbers enabled.", }, - feetWarrior: { - display: "Feet warrior", + footWarrior: { + display: "Foot Warrior", type: "customTime", parameters: [3600], discordRoleId: "751953592860147822", + category: "otherChallenges", + description: "Complete a one-hour test using your feet. Don't ask me why.", }, wingdings: { display: "Ten Words of Pain", @@ -582,5 +728,274 @@ export const Challenges: Record> = { acc: { exact: 100 }, }, discordRoleId: "863192575984140338", + category: "otherChallenges", + description: "Complete a 10-word Master mode test using the Wingdings custom font.", + }, + ultimateMonkeyFlex: { + display: "Ultimate Monkey Flex", + parameters: [null, null, null], + discordRoleId: "768497815496032266", + category: "champions", + description: "Have the most champion roles in the server.", + }, + oneRoleToRuleThemAll: { + display: "One role to rule them all", + parameters: [null, null, null], + discordRoleId: "758784729151176755", + category: "champions", + description: "Have the most challenge roles in the server.", + }, + doYouKnowTheDefinitionOfInsanity: { + display: "Do You Know The Definition Of Insanity", + parameters: [null, null, null], + discordRoleId: "736527448757370880", + category: "champions", + description: "Complete the longest typing session in Monkeytype history.", + }, + oneHourChampion: { + display: "One Hour Champion", + parameters: [null, null, null], + discordRoleId: "728650773503934464", + category: "champions", + description: "Achieve the highest WPM in a one-hour test.", + }, + fluidChampion: { + display: "Fluid Champion", + parameters: [null, null, null], + discordRoleId: "740568718719058041", + category: "champions", + description: "Achieve the highest WPM in a 60-second layoutfluid test.", + }, + accuracyChampion: { + display: "Accuracy Champion", + parameters: [null, null, null], + discordRoleId: "768499906511110235", + category: "champions", + description: "Achieve the longest Master mode test.", + }, + literallyTheFastestPersonHere: { + display: "Literally The Fastest Person Here", + parameters: [null, null, null], + discordRoleId: "984922187385405460", + category: "champions", + description: "Achieve 1st place on the time 60 English all-time leaderboard.", + }, + // fehmer suggested putting it here under the champions section (as its a role obtainable by anyone) + bananaHoarder: { + display: "Banana Hoarder", + parameters: [null, null, null], + discordRoleId: "773590599227932754", + category: "champions", + description: "Achieve 1st place on the banana leaderboard.", + }, + alpha: { + display: "A l p h a", + parameters: [null, null, null], + discordRoleId: "773590612762034176", + category: "speed", + description: "Type the alphabet, with each letter separated by a space and in alphabetical order a b c d e f g h i j k l m n o p q r s t u v w x y z in LESS than 3.37 seconds.", + }, + blazeIt: { + display: "Blaze It", + parameters: [null, null, null], + discordRoleId: "803650889461006346", + category: "speed", + description: "Achieve 420 WPM (can be rounded) by typing weed.", + }, + burstMaster: { + display: "Burst Master", + parameters: [null, null, null], + discordRoleId: "757330922726096917", + category: "speed", + description: "Achieve 200+ WPM on the words 10 mode.", + }, + burstGod: { + display: "Burst God", + parameters: [null, null, null], + discordRoleId: "757330992821305366", + category: "speed", + description: "Achieve 250+ WPM on the words 10 mode.", + }, + shotgun: { + display: "Shotgun", + parameters: [null, null, null], + discordRoleId: "757331084366184539", + category: "speed", + description: "Achieve 300+ WPM on the words 10 mode.", + }, + nuke: { + display: "Nuke", + parameters: [null, null, null], + discordRoleId: "912522664604758016", + category: "speed", + description: "Achieve 350+ WPM on the words 10 mode.", + }, + orbitalCannon: { + display: "Orbital Cannon", + parameters: [null, null, null], + discordRoleId: "1084094136199684196", + category: "speed", + description: "Achieve 400+ WPM on the words 10 mode.", + }, + marathonSprinter: { + display: "Marathon Sprinter", + parameters: [null, null, null], + discordRoleId: "878715678830510111", + category: "speed", + description: "Achieve 200+ WPM on a one-hour test.", + }, + flawless: { + display: "Flawless", + parameters: [null, null, null], + discordRoleId: "767070815987695637", + category: "accuracy", + description: "Complete back-to-back tests in Master Mode: 15, 30, 60, 120 seconds and 10, 25, 50, 100 words. If you fail one, restart from the beginning. Order of modes is up to you.", + }, + hesBeginningToBelieve: { + display: "He's beginning to believe", + parameters: [null, null, null], + discordRoleId: "979729541096431688", + category: "accuracy", + description: "Achieve 100% accuracy in a 2-minute test under specified settings.", + }, + goldenHands: { + display: "Golden Hands", + parameters: [null, null, null], + discordRoleId: "851096860969795684", + category: "accuracy", + description: "Complete a 1-hour Master mode test.", + }, + fingerBlaster: { + display: "Finger Blaster", + parameters: [null, null, null], + discordRoleId: "787509606992969728", + category: "otherChallenges", + description: "Achieve at least 60 WPM using one finger on a 60-second test.", + }, + whyAreTheWallsMoving: { + display: "Why are the walls moving?", + parameters: [null, null, null], + discordRoleId: "910078947302191114", + category: "otherChallenges", + description: "Complete a one-hour test using tape mode and letter mode.", + }, + stickman: { + display: "stickman", + parameters: [null, null, null], + discordRoleId: "788107449151651890", + category: "otherChallenges", + description: "Complete a one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", + }, + waveDynamics: { + display: "Wave Dynamics", + parameters: [null, null, null], + discordRoleId: "1443311363794407586", + category: "otherChallenges", + description: "Achieve 30 wpm 100% acc on a 60 second test with the raw graph being a perfect wave (to achieve this, type 5 characters in 1 second, pause for 1 second, repeat). Must be completed with random words (time 60 mode). Must include words history in the screenshot.", + }, + apesTogetherStrong: { + display: "Apes Together Strong", + parameters: [null, null, null], + discordRoleId: "863193901153779713", + category: "otherChallenges", + description: "Complete a one-hour test in a Tribe lobby with at least 10 players.", + }, + apesTogetherStronger: { + display: "Apes Together Stronger", + parameters: [null, null, null], + discordRoleId: "898964842726195220", + category: "otherChallenges", + description: "Complete a two-hour test in a Tribe lobby with at least 10 players.", + }, + apesTogetherInvincible: { + display: "Apes Together Invincible", + parameters: [null, null, null], + discordRoleId: "1367559768746758194", + category: "otherChallenges", + description: "Complete a three-hour test in a Tribe lobby with at least 10 players.", + }, + footBarbarian: { + display: "Foot Barbarian", + parameters: [null, null, null], + discordRoleId: "1025814170962231336", + category: "otherChallenges", + description: "Complete a two-hour test using your feet.", + }, + bigFoot: { + display: "Big Foot", + parameters: [null, null, null], + discordRoleId: "1030531753082900610", + category: "otherChallenges", + description: "Complete a three-hour test using your feet.", + }, + woodPecker: { + display: "Wood Pecker", + parameters: [null, null, null], + discordRoleId: "753724531666845830", + category: "otherChallenges", + description: "Complete a 200-word test using only your nose.", + }, + mrWorldwide: { + display: "Mr Worldwide", + parameters: [null, null, null], + discordRoleId: "762345904279519292", + category: "otherChallenges", + description: "Achieve 100 WPM on a 60-second test in 5 different languages (English, English expanded, English 10k and coding languages all count as English which is 1 language).", + }, + internalMetronome: { + display: "Internal Metronome", + parameters: [null, null, null], + discordRoleId: "934067904884916234", + category: "otherChallenges", + description: "Complete a 60-second test (standard English) with a minimum consistency of 90%, 100% accuracy and within 25% of your 60-second personal best.", + }, + roleCollector: { + display: "Role Collector", + parameters: [null, null, null], + discordRoleId: "739306809554108520", + category: "roleCount", + description: "Collect 10 roles.", + }, + roleEnthusiast: { + display: "Role Enthusiast", + parameters: [null, null, null], + discordRoleId: "753360663656529931", + category: "roleCount", + description: "Collect 20 roles.", + }, + roleAddict: { + display: "Role Addict", + parameters: [null, null, null], + discordRoleId: "758783172833443850", + category: "roleCount", + description: "Collect 30 roles.", + }, + roleOverdose: { + display: "Role Overdose", + parameters: [null, null, null], + discordRoleId: "758783365930811423", + category: "roleCount", + description: "Collect 40 roles.", + }, + roleZombie: { + display: "Role Zombie", + parameters: [null, null, null], + discordRoleId: "762701731993616405", + category: "roleCount", + description: "Collect 50 roles.", + }, + roleOverlord: { + display: "Role Overlord", + parameters: [null, null, null], + discordRoleId: "805519411502514187", + category: "roleCount", + description: "Collect 60 roles.", + }, + roleImp: { + display: "Role Imp", + parameters: [null, null, null], + discordRoleId: "906565521271558214", + category: "roleCount", + description: "Collect 70 roles.", }, }; From 65dc13c1c00abdde0b2cdf73b79d5ae049fd1398 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Jun 2026 18:13:59 +0200 Subject: [PATCH 06/12] cleanup --- packages/challenges/src/index.ts | 357 ++++++++++++++++++----------- packages/schemas/src/challenges.ts | 53 +++++ 2 files changed, 273 insertions(+), 137 deletions(-) diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index 954489186c63..493f0f2fc261 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -6,7 +6,7 @@ export const Challenges: Record> = { autoRole: false, type: "hidden", discordRoleId: "761766710704603166", - category: "otherChallenges", + category: "other", description: "Achieve 100 hours of typing.", parameters: [], }, @@ -15,7 +15,7 @@ export const Challenges: Record> = { autoRole: false, type: "hidden", discordRoleId: "799825381733433344", - category: "otherChallenges", + category: "other", description: "Achieve 250 hours of typing.", parameters: [], }, @@ -24,7 +24,7 @@ export const Challenges: Record> = { autoRole: false, type: "hidden", discordRoleId: "951861792622125106", - category: "otherChallenges", + category: "other", description: "Achieve 500 hours of typing.", parameters: [], }, @@ -33,7 +33,7 @@ export const Challenges: Record> = { autoRole: false, type: "hidden", discordRoleId: "1262175323588395100", - category: "otherChallenges", + category: "other", description: "Achieve 1000 hours of typing.", parameters: [], }, @@ -51,8 +51,9 @@ export const Challenges: Record> = { con: { exact: 69 }, }, discordRoleId: "749505965174292511", - category: "otherChallenges", - description: "Complete a 69-second test and achieve 69 WPM, 69 raw, 69% accuracy, and 69% consistency.", + category: "other", + description: + "Complete a 69-second test and achieve 69 WPM, 69 raw, 69% accuracy, and 69% consistency.", }, oneHourWarrior: { display: "One Hour Warrior", @@ -63,7 +64,7 @@ export const Challenges: Record> = { time: { min: 3600 }, }, discordRoleId: "728371749737201855", - category: "classicEndurance", + category: "endurance", description: "Complete a one-hour test.", }, doubleDown: { @@ -75,7 +76,7 @@ export const Challenges: Record> = { time: { min: 7200 }, }, discordRoleId: "732008008514535544", - category: "classicEndurance", + category: "endurance", description: "Complete a two-hour test.", }, tripleTrouble: { @@ -87,7 +88,7 @@ export const Challenges: Record> = { time: { min: 10800 }, }, discordRoleId: "732008047618293762", - category: "classicEndurance", + category: "endurance", description: "Complete a three-hour test.", }, quad: { @@ -99,7 +100,7 @@ export const Challenges: Record> = { time: { min: 14400 }, }, discordRoleId: "736215666352455801", - category: "classicEndurance", + category: "endurance", description: "Complete a four-hour test.", }, "8Ball": { @@ -110,7 +111,7 @@ export const Challenges: Record> = { time: { min: 28800 }, }, discordRoleId: "736528159956271126", - category: "classicEndurance", + category: "endurance", description: "Complete an eight-hour test.", }, theBig12: { @@ -121,7 +122,7 @@ export const Challenges: Record> = { time: { min: 43200 }, }, discordRoleId: "740532256388546581", - category: "classicEndurance", + category: "endurance", description: "Complete a twelve-hour test.", }, "1Day": { @@ -132,7 +133,7 @@ export const Challenges: Record> = { time: { min: 86400 }, }, discordRoleId: "751801958511149057", - category: "classicEndurance", + category: "endurance", description: "Complete a twenty-four-hour test.", }, trueSimp: { @@ -141,7 +142,7 @@ export const Challenges: Record> = { type: "customText", parameters: ["miodec", "repeat", 10000, "word", false], discordRoleId: "744328648211038359", - category: "specificText", + category: "script", description: "Type miodec ten thousand times.", }, bigramSalad: { @@ -160,7 +161,8 @@ export const Challenges: Record> = { }, discordRoleId: "818535054145093652", category: "speed", - description: "Get 100 WPM on a randomized, 100-word custom test with the words list: to of in it is as at be we he so on an or do if up by my go.", + description: + "Get 100 WPM on a randomized, 100-word custom test with the words list: to of in it is as at be we he so on an or do if up by my go.", }, simp: { display: "Simp", @@ -168,7 +170,7 @@ export const Challenges: Record> = { type: "customText", parameters: ["miodec", "repeat", 1000, "word", false], discordRoleId: "743854992699687023", - category: "specificText", + category: "script", description: "Type miodec one thousand times.", }, simpLord: { @@ -179,7 +181,7 @@ export const Challenges: Record> = { // this would be 100k times: parameters: ["miodec", "repeat", 100000, "word", false], discordRoleId: "984911956949479445", - category: "specificText", + category: "script", description: "Type miodec one hundred thousand times.", }, antidiseWhat: { @@ -191,7 +193,7 @@ export const Challenges: Record> = { wpm: { min: 200 }, }, discordRoleId: "782006507360616449", - category: "specificText", + category: "script", description: "Get at least 200 wpm typing antidisestablishmentarianism.", }, whatsThisWebsiteCalledAgain: { @@ -200,7 +202,7 @@ export const Challenges: Record> = { type: "customText", parameters: ["monkeytype", "repeat", 1000, "word", false], discordRoleId: "739276161603076116", - category: "specificText", + category: "script", description: "Type monkeytype one thousand times.", }, developd: { @@ -209,7 +211,7 @@ export const Challenges: Record> = { type: "customText", parameters: ["develop", "repeat", 1000, "word", false], discordRoleId: "735964917877964932", - category: "specificText", + category: "script", description: "Type develop one thousand times.", }, slowAndSteady: { @@ -223,7 +225,8 @@ export const Challenges: Record> = { }, discordRoleId: "782005061935956008", category: "speed", - description: "Complete a 5-minute test with exactly 60 WPM without using the live WPM or pace caret.", + description: + "Complete a 5-minute test with exactly 60 WPM without using the live WPM or pace caret.", }, speedSpacer: { display: "Speed Spacer", @@ -241,7 +244,8 @@ export const Challenges: Record> = { }, discordRoleId: "755244049446731856", category: "speed", - description: "Get 100 wpm on a randomised custom test with the input: a b c d e f g h i j k l m n o p q r s t u v w x y z (the alphabet) and a word count of 100.", + description: + "Get 100 wpm on a randomised custom test with the input: a b c d e f g h i j k l m n o p q r s t u v w x y z (the alphabet) and a word count of 100.", }, iveGotThePower: { display: "I've got the POWER", @@ -311,8 +315,9 @@ export const Challenges: Record> = { config: { tapeMode: "off" }, }, discordRoleId: "740004324301602907", - category: "scriptsOrSongs", - description: "Type out the entire Star Wars Episode 4 script with punctuation while watching the movie simultaneously.", + category: "script", + description: + "Type out the entire Star Wars Episode 4 script with punctuation while watching the movie simultaneously.", }, beepBoop: { display: "Beep Boop", @@ -325,8 +330,9 @@ export const Challenges: Record> = { funbox: { exact: ["nospace"] }, }, discordRoleId: "813076265145729024", - category: "scriptsOrSongs", - description: "Type the beepboop script with 100% accuracy and at least 45 WPM.", + category: "script", + description: + "Type the beepboop script with 100% accuracy and at least 45 WPM.", }, whosYourDaddy: { display: "Who's your daddy", @@ -336,8 +342,9 @@ export const Challenges: Record> = { config: { tapeMode: "off" }, }, discordRoleId: "742171915405361204", - category: "scriptsOrSongs", - description: "Type out the entire Star Wars Episode 5 script with punctuation while watching the movie simultaneously.", + category: "script", + description: + "Type out the entire Star Wars Episode 5 script with punctuation while watching the movie simultaneously.", }, itsATrap: { display: "It's a trap!!", @@ -347,8 +354,9 @@ export const Challenges: Record> = { config: { tapeMode: "off" }, }, discordRoleId: "744325174668820550", - category: "scriptsOrSongs", - description: "Type out the entire Star Wars Episode 6 script with punctuation while watching the movie simultaneously.", + category: "script", + description: + "Type out the entire Star Wars Episode 6 script with punctuation while watching the movie simultaneously.", }, jolly: { display: "Jolly", @@ -360,7 +368,7 @@ export const Challenges: Record> = { wpm: { min: 70 }, }, discordRoleId: "768497412548329563", - category: "scriptsOrSongs", + category: "script", description: "Type the Jolly script with a minimum of 70 wpm.", }, gottaCatchEmAll: { @@ -369,7 +377,7 @@ export const Challenges: Record> = { type: "script", parameters: ["pokemon.txt", null, null], discordRoleId: "767069340599975998", - category: "scriptsOrSongs", + category: "script", description: "Type out the names of all Pokemon.", }, rapGod: { @@ -384,8 +392,9 @@ export const Challenges: Record> = { afk: { max: 5 }, }, discordRoleId: "743844891045396603", - category: "scriptsOrSongs", - description: "Type out the lyrics of Eminem's Rap God at a minimum of 85 WPM and 90% accuracy, including punctuation.", + category: "script", + description: + "Type out the lyrics of Eminem's Rap God at a minimum of 85 WPM and 90% accuracy, including punctuation.", }, navySeal: { display: "Navy Seal", @@ -399,47 +408,52 @@ export const Challenges: Record> = { afk: { max: 5 }, }, discordRoleId: "762345535969165342", - category: "scriptsOrSongs", - description: "Type out the Navy Seal copy pasta with 100% accuracy and minimum 60 WPM.", + category: "script", + description: + "Type out the Navy Seal copy pasta with 100% accuracy and minimum 60 WPM.", }, littleChef: { display: "Little Chef", type: "script", parameters: ["littlechef.txt", null, null], discordRoleId: "763544714028122153", - category: "scriptsOrSongs", - description: "Type out the entire Ratatouille script while watching the movie simultaneously.", + category: "script", + description: + "Type out the entire Ratatouille script while watching the movie simultaneously.", }, crosstalk: { display: "(CROSSTALK)", type: "script", parameters: ["crosstalk.txt", null, null], discordRoleId: "761276009664217129", - category: "scriptsOrSongs", - description: "Type out the entire transcript of the first 2020 Presidential Debate.", + category: "script", + description: + "Type out the entire transcript of the first 2020 Presidential Debate.", }, bees: { display: "Bees!!!", type: "script", parameters: ["bees.txt", null, null], discordRoleId: "739636003182084307", - category: "scriptsOrSongs", - description: "Type out the entire Bee Movie script while watching the movie simultaneously.", + category: "script", + description: + "Type out the entire Bee Movie script while watching the movie simultaneously.", }, getOffMySwamp: { display: "Get Off My Swamp", type: "script", parameters: ["shrek.txt", null, null], discordRoleId: "757346966987342026", - category: "scriptsOrSongs", - description: "Type out the entire Shrek script with punctuation while watching the movie simultaneously.", + category: "script", + description: + "Type out the entire Shrek script with punctuation while watching the movie simultaneously.", }, fiftyShadesOfHell: { display: "50 Shades of Hell", type: "script", - parameters: [null, null, null], + parameters: [], discordRoleId: "751802155119280128", - category: "scriptsOrSongs", + category: "script", description: "Type out your favourite chapter from 50 Shades of Gray.", }, lookAtMeIAmTheDeveloperNow: { @@ -448,8 +462,9 @@ export const Challenges: Record> = { type: "script", parameters: ["sourcecode.txt", null, null], discordRoleId: "937358772635074600", - category: "scriptsOrSongs", - description: "Type out the entire source code of Monkeytype, as it was in February 2022.", + category: "script", + description: + "Type out the entire source code of Monkeytype, as it was in February 2022.", }, beLikeWater: { display: "Be Like Water", @@ -458,7 +473,8 @@ export const Challenges: Record> = { message: "Remember: You need to achieve at least 50 wpm in each layout.", discordRoleId: "740568679485276201", category: "funbox", - description: "Achieve at least 50 WPM in all three layouts in a 60-second time test using the layoutfluid mode. Layouts must be unique (e.g., QWERTY, Colemak, Dvorak).", + description: + "Achieve at least 50 WPM in all three layouts in a 60-second time test using the layoutfluid mode. Layouts must be unique (e.g., QWERTY, Colemak, Dvorak).", }, rollercoaster: { display: "Rollercoaster", @@ -471,7 +487,8 @@ export const Challenges: Record> = { }, discordRoleId: "736032495526740001", category: "funbox", - description: "Complete at least a one-hour test using the round round baby mode.", + description: + "Complete at least a one-hour test using the round round baby mode.", }, oneHourMirror: { display: "ɿoɿɿim ɿυoʜ ɘno", @@ -508,7 +525,8 @@ export const Challenges: Record> = { }, discordRoleId: "782005606852067328", category: "funbox", - description: "Achieve 100+ WPM with 100% accuracy on a 25-word test using the memory funbox.", + description: + "Achieve 100+ WPM with 100% accuracy on a 25-word test using the memory funbox.", }, earfquake: { display: "Earfquake", @@ -521,7 +539,8 @@ export const Challenges: Record> = { }, discordRoleId: "740730587429601291", category: "funbox", - description: "Complete at least a one-hour test using the earthquake funbox mode.", + description: + "Complete at least a one-hour test using the earthquake funbox mode.", }, simonSez: { display: "Simon Sez", @@ -534,7 +553,8 @@ export const Challenges: Record> = { }, discordRoleId: "742128871825997914", category: "funbox", - description: "Complete at least a one-hour test using the simon says funbox mode.", + description: + "Complete at least a one-hour test using the simon says funbox mode.", }, accountant: { display: "Accountant", @@ -547,7 +567,8 @@ export const Challenges: Record> = { }, discordRoleId: "743962178821816391", category: "funbox", - description: "Complete at least a one-hour test using the 58008 funbox mode.", + description: + "Complete at least a one-hour test using the 58008 funbox mode.", }, hidden: { display: "Hidden", @@ -562,7 +583,8 @@ export const Challenges: Record> = { }, discordRoleId: "782006137742557194", category: "funbox", - description: "Achieve 100+ WPM using the read ahead funbox on a 60-second test.", + description: + "Achieve 100+ WPM using the read ahead funbox on a 60-second test.", }, iCanSeeTheFuture: { display: "I can see the future", @@ -577,7 +599,8 @@ export const Challenges: Record> = { }, discordRoleId: "814877508008411226", category: "funbox", - description: "Achieve 100+ WPM using the read ahead hard funbox on a 60-second test.", + description: + "Achieve 100+ WPM using the read ahead hard funbox on a 60-second test.", }, whatAreWordsAtThisPoint: { display: "What are words at this point", @@ -590,7 +613,8 @@ export const Challenges: Record> = { }, discordRoleId: "744209241396740176", category: "funbox", - description: "Complete at least a one-hour test using the gibberish funbox mode.", + description: + "Complete at least a one-hour test using the gibberish funbox mode.", }, specials: { display: "Specials", @@ -603,7 +627,8 @@ export const Challenges: Record> = { }, discordRoleId: "744209452714033162", category: "funbox", - description: "Complete at least a one-hour test using the specials funbox mode.", + description: + "Complete at least a one-hour test using the specials funbox mode.", }, aeiou: { display: "Aeiou.", @@ -629,7 +654,8 @@ export const Challenges: Record> = { }, discordRoleId: "746142791326760980", category: "funbox", - description: "Complete at least a one-hour test using the ascii funbox mode.", + description: + "Complete at least a one-hour test using the ascii funbox mode.", }, iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS: { display: "i KINda LikE HoW inEFFICIeNt QwErtY Is.", @@ -642,7 +668,8 @@ export const Challenges: Record> = { }, discordRoleId: "760999194525171724", category: "funbox", - description: "Complete at least a one-hour test using the randomcase funbox mode.", + description: + "Complete at least a one-hour test using the randomcase funbox mode.", }, oneNauseousMonkey: { display: "One Nauseous Monkey", @@ -655,14 +682,15 @@ export const Challenges: Record> = { }, discordRoleId: "760930262740631633", category: "funbox", - description: "Complete at least a one-hour test using the nausea funbox mode.", + description: + "Complete at least a one-hour test using the nausea funbox mode.", }, thumbWarrior: { display: "Thumb Warrior", type: "customTime", parameters: [3600], discordRoleId: "761794585109200906", - category: "otherChallenges", + category: "other", description: "Complete a one-hour test using only your thumbs.", }, mouseWarrior: { @@ -670,15 +698,16 @@ export const Challenges: Record> = { type: "customTime", parameters: [3600], discordRoleId: "744580294442614790", - category: "otherChallenges", - description: "Complete a one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", + category: "other", + description: + "Complete a one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", }, mobileWarrior: { display: "Mobile warrior", type: "customTime", parameters: [3600], discordRoleId: "744723801526370407", - category: "otherChallenges", + category: "other", description: "Complete a one-hour test on mobile.", }, upsideDown: { @@ -686,16 +715,18 @@ export const Challenges: Record> = { type: "customTime", parameters: [60], discordRoleId: "782725716114014237", - category: "otherChallenges", - description: "Achieve at least 60 WPM on a one-minute test with your keyboard upside down.", + category: "other", + description: + "Achieve at least 60 WPM on a one-minute test with your keyboard upside down.", }, oneArmedBandit: { display: "One armed bandit", type: "customWords", parameters: [10000], discordRoleId: "765919192557682708", - category: "otherChallenges", - description: "Complete a one-hour or 10k words test (whichever comes sooner, using an external timer) using a one-handed words list (either left or right) for your layout.", + category: "other", + description: + "Complete a one-hour or 10k words test (whichever comes sooner, using an external timer) using a one-handed words list (either left or right) for your layout.", }, englishMaster: { display: "English master", @@ -707,15 +738,16 @@ export const Challenges: Record> = { config: { language: "english_10k", punctuation: true, numbers: true }, }, discordRoleId: "751166528824672396", - category: "otherChallenges", - description: "Complete a one-hour test using English 10k language with punctuation and numbers enabled.", + category: "other", + description: + "Complete a one-hour test using English 10k language with punctuation and numbers enabled.", }, - footWarrior: { + feetWarrior: { display: "Foot Warrior", type: "customTime", parameters: [3600], discordRoleId: "751953592860147822", - category: "otherChallenges", + category: "other", description: "Complete a one-hour test using your feet. Don't ask me why.", }, wingdings: { @@ -728,272 +760,323 @@ export const Challenges: Record> = { acc: { exact: 100 }, }, discordRoleId: "863192575984140338", - category: "otherChallenges", - description: "Complete a 10-word Master mode test using the Wingdings custom font.", + category: "other", + description: + "Complete a 10-word Master mode test using the Wingdings custom font.", }, ultimateMonkeyFlex: { display: "Ultimate Monkey Flex", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "768497815496032266", category: "champions", description: "Have the most champion roles in the server.", }, oneRoleToRuleThemAll: { display: "One role to rule them all", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "758784729151176755", category: "champions", description: "Have the most challenge roles in the server.", }, doYouKnowTheDefinitionOfInsanity: { display: "Do You Know The Definition Of Insanity", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "736527448757370880", category: "champions", description: "Complete the longest typing session in Monkeytype history.", }, oneHourChampion: { display: "One Hour Champion", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "728650773503934464", category: "champions", description: "Achieve the highest WPM in a one-hour test.", }, fluidChampion: { display: "Fluid Champion", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "740568718719058041", category: "champions", description: "Achieve the highest WPM in a 60-second layoutfluid test.", }, accuracyChampion: { display: "Accuracy Champion", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "768499906511110235", category: "champions", description: "Achieve the longest Master mode test.", }, literallyTheFastestPersonHere: { display: "Literally The Fastest Person Here", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "984922187385405460", category: "champions", - description: "Achieve 1st place on the time 60 English all-time leaderboard.", + description: + "Achieve 1st place on the time 60 English all-time leaderboard.", }, // fehmer suggested putting it here under the champions section (as its a role obtainable by anyone) bananaHoarder: { display: "Banana Hoarder", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "773590599227932754", category: "champions", description: "Achieve 1st place on the banana leaderboard.", }, alpha: { display: "A l p h a", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "773590612762034176", category: "speed", - description: "Type the alphabet, with each letter separated by a space and in alphabetical order a b c d e f g h i j k l m n o p q r s t u v w x y z in LESS than 3.37 seconds.", + description: + "Type the alphabet, with each letter separated by a space and in alphabetical order a b c d e f g h i j k l m n o p q r s t u v w x y z in LESS than 3.37 seconds.", }, blazeIt: { display: "Blaze It", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "803650889461006346", category: "speed", description: "Achieve 420 WPM (can be rounded) by typing weed.", }, burstMaster: { display: "Burst Master", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "757330922726096917", category: "speed", description: "Achieve 200+ WPM on the words 10 mode.", }, burstGod: { display: "Burst God", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "757330992821305366", category: "speed", description: "Achieve 250+ WPM on the words 10 mode.", }, shotgun: { display: "Shotgun", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "757331084366184539", category: "speed", description: "Achieve 300+ WPM on the words 10 mode.", }, nuke: { display: "Nuke", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "912522664604758016", category: "speed", description: "Achieve 350+ WPM on the words 10 mode.", }, orbitalCannon: { display: "Orbital Cannon", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "1084094136199684196", category: "speed", description: "Achieve 400+ WPM on the words 10 mode.", }, marathonSprinter: { display: "Marathon Sprinter", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "878715678830510111", category: "speed", description: "Achieve 200+ WPM on a one-hour test.", }, flawless: { display: "Flawless", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "767070815987695637", category: "accuracy", - description: "Complete back-to-back tests in Master Mode: 15, 30, 60, 120 seconds and 10, 25, 50, 100 words. If you fail one, restart from the beginning. Order of modes is up to you.", + description: + "Complete back-to-back tests in Master Mode: 15, 30, 60, 120 seconds and 10, 25, 50, 100 words. If you fail one, restart from the beginning. Order of modes is up to you.", }, hesBeginningToBelieve: { display: "He's beginning to believe", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "979729541096431688", category: "accuracy", - description: "Achieve 100% accuracy in a 2-minute test under specified settings.", + description: + "Achieve 100% accuracy in a 2-minute test under specified settings.", }, goldenHands: { display: "Golden Hands", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "851096860969795684", category: "accuracy", description: "Complete a 1-hour Master mode test.", }, fingerBlaster: { display: "Finger Blaster", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "787509606992969728", - category: "otherChallenges", - description: "Achieve at least 60 WPM using one finger on a 60-second test.", + category: "other", + description: + "Achieve at least 60 WPM using one finger on a 60-second test.", }, whyAreTheWallsMoving: { display: "Why are the walls moving?", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "910078947302191114", - category: "otherChallenges", + category: "other", description: "Complete a one-hour test using tape mode and letter mode.", }, stickman: { display: "stickman", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "788107449151651890", - category: "otherChallenges", - description: "Complete a one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", + category: "other", + description: + "Complete a one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", }, waveDynamics: { display: "Wave Dynamics", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "1443311363794407586", - category: "otherChallenges", - description: "Achieve 30 wpm 100% acc on a 60 second test with the raw graph being a perfect wave (to achieve this, type 5 characters in 1 second, pause for 1 second, repeat). Must be completed with random words (time 60 mode). Must include words history in the screenshot.", + category: "other", + description: + "Achieve 30 wpm 100% acc on a 60 second test with the raw graph being a perfect wave (to achieve this, type 5 characters in 1 second, pause for 1 second, repeat). Must be completed with random words (time 60 mode). Must include words history in the screenshot.", }, apesTogetherStrong: { display: "Apes Together Strong", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "863193901153779713", - category: "otherChallenges", - description: "Complete a one-hour test in a Tribe lobby with at least 10 players.", + category: "other", + description: + "Complete a one-hour test in a Tribe lobby with at least 10 players.", }, apesTogetherStronger: { display: "Apes Together Stronger", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "898964842726195220", - category: "otherChallenges", - description: "Complete a two-hour test in a Tribe lobby with at least 10 players.", + category: "other", + description: + "Complete a two-hour test in a Tribe lobby with at least 10 players.", }, apesTogetherInvincible: { display: "Apes Together Invincible", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "1367559768746758194", - category: "otherChallenges", - description: "Complete a three-hour test in a Tribe lobby with at least 10 players.", + category: "other", + description: + "Complete a three-hour test in a Tribe lobby with at least 10 players.", }, footBarbarian: { display: "Foot Barbarian", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "1025814170962231336", - category: "otherChallenges", + category: "other", description: "Complete a two-hour test using your feet.", }, bigFoot: { display: "Big Foot", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "1030531753082900610", - category: "otherChallenges", + category: "other", description: "Complete a three-hour test using your feet.", }, woodPecker: { display: "Wood Pecker", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "753724531666845830", - category: "otherChallenges", + category: "other", description: "Complete a 200-word test using only your nose.", }, mrWorldwide: { display: "Mr Worldwide", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "762345904279519292", - category: "otherChallenges", - description: "Achieve 100 WPM on a 60-second test in 5 different languages (English, English expanded, English 10k and coding languages all count as English which is 1 language).", + category: "other", + description: + "Achieve 100 WPM on a 60-second test in 5 different languages (English, English expanded, English 10k and coding languages all count as English which is 1 language).", }, internalMetronome: { display: "Internal Metronome", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "934067904884916234", - category: "otherChallenges", - description: "Complete a 60-second test (standard English) with a minimum consistency of 90%, 100% accuracy and within 25% of your 60-second personal best.", + category: "other", + description: + "Complete a 60-second test (standard English) with a minimum consistency of 90%, 100% accuracy and within 25% of your 60-second personal best.", }, roleCollector: { display: "Role Collector", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "739306809554108520", category: "roleCount", description: "Collect 10 roles.", }, roleEnthusiast: { display: "Role Enthusiast", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "753360663656529931", category: "roleCount", description: "Collect 20 roles.", }, roleAddict: { display: "Role Addict", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "758783172833443850", category: "roleCount", description: "Collect 30 roles.", }, roleOverdose: { display: "Role Overdose", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "758783365930811423", category: "roleCount", description: "Collect 40 roles.", }, roleZombie: { display: "Role Zombie", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "762701731993616405", category: "roleCount", description: "Collect 50 roles.", }, roleOverlord: { display: "Role Overlord", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "805519411502514187", category: "roleCount", description: "Collect 60 roles.", }, roleImp: { display: "Role Imp", - parameters: [null, null, null], + type: "hidden", + parameters: [], discordRoleId: "906565521271558214", category: "roleCount", description: "Collect 70 roles.", diff --git a/packages/schemas/src/challenges.ts b/packages/schemas/src/challenges.ts index a1023ea22bb3..525f3c5a1cea 100644 --- a/packages/schemas/src/challenges.ts +++ b/packages/schemas/src/challenges.ts @@ -19,6 +19,7 @@ export const ChallengeNameSchema = z.enum( "trueSimp", "bigramSalad", "simp", + "simpLord", "antidiseWhat", "whatsThisWebsiteCalledAgain", "developd", @@ -40,6 +41,7 @@ export const ChallengeNameSchema = z.enum( "crosstalk", "bees", "getOffMySwamp", + "fiftyShadesOfHell", "lookAtMeIAmTheDeveloperNow", "beLikeWater", "rollercoaster", @@ -69,6 +71,45 @@ export const ChallengeNameSchema = z.enum( "100hours", "250hours", "500hours", + "1000hours", + "ultimateMonkeyFlex", + "oneRoleToRuleThemAll", + "doYouKnowTheDefinitionOfInsanity", + "oneHourChampion", + "fluidChampion", + "accuracyChampion", + "literallyTheFastestPersonHere", + "bananaHoarder", + "alpha", + "blazeIt", + "burstMaster", + "burstGod", + "shotgun", + "nuke", + "orbitalCannon", + "marathonSprinter", + "flawless", + "hesBeginningToBelieve", + "goldenHands", + "fingerBlaster", + "whyAreTheWallsMoving", + "stickman", + "waveDynamics", + "apesTogetherStrong", + "apesTogetherStronger", + "apesTogetherInvincible", + "footBarbarian", + "bigFoot", + "woodPecker", + "mrWorldwide", + "internalMetronome", + "roleCollector", + "roleEnthusiast", + "roleAddict", + "roleOverdose", + "roleZombie", + "roleOverlord", + "roleImp", ], { errorMap: customEnumErrorHandler("Must be a known challenge name"), @@ -120,6 +161,18 @@ export const ChallengeSchema = z .partial() .strict() .optional(), + + category: z.enum([ + "other", + "endurance", + "script", + "speed", + "accuracy", + "funbox", + "champions", + "roleCount", + ]), + description: z.string(), }) .strict(); From e5b8b5f0d428b684cddad0e0f05f4fafd6828ad0 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Jun 2026 21:33:27 +0200 Subject: [PATCH 07/12] cleanup --- backend/src/utils/test.json | 46 ------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 backend/src/utils/test.json diff --git a/backend/src/utils/test.json b/backend/src/utils/test.json deleted file mode 100644 index 20c5c2f48997..000000000000 --- a/backend/src/utils/test.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "avatar": null, - "banner": null, - "communication_disabled_until": null, - "flags": 0, - "joined_at": "2026-06-23T07:34:29.080000+00:00", - "nick": null, - "pending": false, - "premium_since": null, - "roles": ["730095916547309641", "873307090242908242"], - "unusual_dm_activity_until": null, - "collectibles": null, - "display_name_styles": null, - "user": { - "id": "1220521103592652822", - "username": "cute.terror", - "avatar": "b1599e23cdca04d352d5af2a7aab3929", - "discriminator": "0", - "public_flags": 0, - "flags": 0, - "banner": null, - "accent_color": 14588849, - "global_name": "Cosmiqu\u00e9", - "avatar_decoration_data": null, - "collectibles": null, - "display_name_styles": null, - "banner_color": "#de9bb1", - "clan": { - "identity_guild_id": "262268073363505164", - "identity_enabled": true, - "tag": "\u14da\u160f\u15e2", - "badge": "439c5b7117f3f3e8e574b4766d1b7e73" - }, - "primary_guild": { - "identity_guild_id": "262268073363505164", - "identity_enabled": true, - "tag": "\u14da\u160f\u15e2", - "badge": "439c5b7117f3f3e8e574b4766d1b7e73" - } - }, - "mute": false, - "deaf": false, - "bio": "", - "permissions": 104189505, - "permissions_new": "1091684154068545" -} From 9020341674153da9bc1ed719661110b30107f893 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 24 Jun 2026 11:03:05 +0200 Subject: [PATCH 08/12] ui, small refactor --- .../__tests__/api/controllers/user.spec.ts | 4 +- backend/src/api/controllers/user.ts | 11 +- backend/src/constants/auto-roles.ts | 8 +- .../ts/commandline/lists/load-challenge.ts | 27 ++-- .../ts/components/modals/EditProfileModal.tsx | 2 +- .../components/pages/profile/Challenges.tsx | 119 ++++++++++++++++++ .../components/pages/profile/UserDetails.tsx | 20 ++- .../components/pages/profile/UserProfile.tsx | 6 + .../ts/controllers/challenge-controller.ts | 36 +----- frontend/src/ts/db.ts | 1 + packages/challenges/package.json | 3 +- .../challenges/scripts/challenge-roles.ts | 8 +- packages/challenges/src/index.ts | 21 +++- packages/schemas/src/results.ts | 3 +- pnpm-lock.yaml | 3 + 15 files changed, 200 insertions(+), 72 deletions(-) create mode 100644 frontend/src/ts/components/pages/profile/Challenges.tsx diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index eeee13c1006d..baf7a0e52feb 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -37,7 +37,7 @@ import * as WeeklyXpLeaderboard from "../../../src/services/weekly-xp-leaderboar import * as ConnectionsDal from "../../../src/dal/connections"; import { pb } from "../../__testData__/users"; import Test from "supertest/lib/test"; -import { Challenges } from "@monkeytype/challenges"; +import { getChallenge } from "@monkeytype/challenges"; const { mockApp, uid, mockAuth } = setup(); const configuration = Configuration.getCachedConfiguration(); @@ -1604,7 +1604,7 @@ describe("user controller test", () => { avatar: "discordUserAvatar", }); getDiscordRoleIdsMock.mockResolvedValue([ - Challenges["100hours"].discordRoleId, + getChallenge("100hours").discordRoleId, ]); isDiscordIdAvailableMock.mockResolvedValue(true); blocklistContainsMock.mockResolvedValue(false); diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 717cd00f9115..643423431976 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -96,16 +96,13 @@ import { tryCatch } from "@monkeytype/util/trycatch"; import * as ConnectionsDal from "../../dal/connections"; import { PersonalBest } from "@monkeytype/schemas/shared"; -import { Challenges } from "@monkeytype/challenges"; import { ChallengeName } from "@monkeytype/schemas/challenges"; +import { getChallenges } from "@monkeytype/challenges"; const challengeNameByRoleId: Record = Object.fromEntries( - Object.entries(Challenges) - .filter(([_, challenge]) => challenge.discordRoleId !== undefined) - .map(([name, challenge]) => [ - challenge.discordRoleId, - name as ChallengeName, - ]), + getChallenges() + .filter((it) => it.discordRoleId !== undefined) + .map((it) => [it.discordRoleId, it.name]), ); async function verifyCaptcha(captcha: string): Promise { diff --git a/backend/src/constants/auto-roles.ts b/backend/src/constants/auto-roles.ts index 2f719abf441c..dba66537ce23 100644 --- a/backend/src/constants/auto-roles.ts +++ b/backend/src/constants/auto-roles.ts @@ -1,5 +1,5 @@ -import { Challenges } from "@monkeytype/challenges"; +import { getChallenges } from "@monkeytype/challenges"; -export default Object.entries(Challenges) - .filter(([_, challenge]) => challenge.autoRole) - .map(([name]) => name); +export default getChallenges() + .filter((it) => it.autoRole) + .map((it) => it.name); diff --git a/frontend/src/ts/commandline/lists/load-challenge.ts b/frontend/src/ts/commandline/lists/load-challenge.ts index 3deea21072ae..023a2d4d3d14 100644 --- a/frontend/src/ts/commandline/lists/load-challenge.ts +++ b/frontend/src/ts/commandline/lists/load-challenge.ts @@ -1,26 +1,23 @@ -import { Challenges } from "@monkeytype/challenges"; +import { getRegularChallenges } from "@monkeytype/challenges"; import * as ChallengeController from "../../controllers/challenge-controller"; import { navigate } from "../../controllers/route-controller"; import * as TestLogic from "../../test/test-logic"; import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; import { Command, CommandsSubgroup } from "../types"; -import { typedEntries } from "@monkeytype/util/objects"; const subgroup: CommandsSubgroup = { title: "Load challenge...", - list: typedEntries(Challenges) - .filter(([_, challenge]) => challenge.type !== "hidden") - .map(([name, challenge]) => ({ - id: `loadChallenge${capitalizeFirstLetterOfEachWord(name)}`, - display: challenge.display, - exec: async (): Promise => { - await navigate("/"); - await ChallengeController.setup(name); - TestLogic.restart({ - nosave: true, - }); - }, - })), + list: getRegularChallenges().map((challenge) => ({ + id: `loadChallenge${capitalizeFirstLetterOfEachWord(challenge.name)}`, + display: challenge.display, + exec: async (): Promise => { + await navigate("/"); + await ChallengeController.setup(challenge.name); + TestLogic.restart({ + nosave: true, + }); + }, + })), }; const commands: Command[] = [ diff --git a/frontend/src/ts/components/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx index 756517926a3f..4bd2574a0cbf 100644 --- a/frontend/src/ts/components/modals/EditProfileModal.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -55,7 +55,7 @@ export function EditProfile() { website: value.website || undefined, }, showActivityOnPublicProfile: value.showActivityOnPublicProfile, - showChallengesOnPublicProfile: value.showActivityOnPublicProfile, + showChallengesOnPublicProfile: value.showChallengesOnPublicProfile, }; const response = await Ape.users.updateProfile({ diff --git a/frontend/src/ts/components/pages/profile/Challenges.tsx b/frontend/src/ts/components/pages/profile/Challenges.tsx new file mode 100644 index 000000000000..b283e5e808b9 --- /dev/null +++ b/frontend/src/ts/components/pages/profile/Challenges.tsx @@ -0,0 +1,119 @@ +import { getChallenge, getRegularChallenges } from "@monkeytype/challenges"; +import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; +import { UserChallenges } from "@monkeytype/schemas/users"; +import { typedEntries } from "@monkeytype/util/objects"; +import { createMemo, For, Show } from "solid-js"; + +import { FaSolidIcon } from "../../../types/font-awesome"; +import { cn } from "../../../utils/cn"; +import { Fa } from "../../common/Fa"; + +function sortNewestFirst( + a: [ChallengeName, { addedAt?: number | undefined } | undefined], + b: [ChallengeName, { addedAt?: number | undefined } | undefined], +): number { + const aHas = a[1]?.addedAt !== undefined; + const bHas = b[1]?.addedAt !== undefined; + if (aHas && !bHas) return -1; + if (!aHas && bHas) return 1; + if (aHas && bHas) return (b[1]?.addedAt ?? 0) - (a[1]?.addedAt ?? 0); + return a[0].localeCompare(b[0]); +} + +export function Challenges(props: { + isAccountPage?: true; + challenges: UserChallenges | undefined; +}) { + const completedChallenges = createMemo((): Challenge[] => + ( + typedEntries(props.challenges ?? {}) as [ + ChallengeName, + { addedAt?: number | undefined } | undefined, + ][] + ) + .sort(sortNewestFirst) + .map(([name]) => getChallenge(name)) + .filter((it) => it !== undefined), + ); + + const completedNames = createMemo( + () => new Set(completedChallenges().map((it) => it.name)), + ); + + const incompleteChallenges = createMemo((): Challenge[] => + getRegularChallenges().filter((it) => !completedNames().has(it.name)), + ); + + return ( + +
+
+

Challenges

+
+ {Object.keys(props.challenges ?? {}).length} /{" "} + {getRegularChallenges().length} completed +
+
+ +
+ + {(challenge) => ( + + )} + + + + {(challenge) => ( + + )} + + +
+
+
+ ); +} + +function ChallengeItem(props: { completed: boolean; challenge: Challenge }) { + const icon = (): FaSolidIcon => { + switch (props.challenge.category) { + case "accuracy": + return "fa-bullseye"; + case "champions": + return "fa-crown"; + case "endurance": + return "fa-running"; + case "funbox": + return "fa-gamepad"; + case "speed": + return "fa-tachometer-alt"; + case "script": + return "fa-file-alt"; + + default: + return "fa-trophy"; + } + }; + return ( +
+
+ +
+
+

{props.challenge.display}

+

{props.challenge.description}

+
+
+ ); +} diff --git a/frontend/src/ts/components/pages/profile/UserDetails.tsx b/frontend/src/ts/components/pages/profile/UserDetails.tsx index 1824eac6ed2f..8e7ad0924b64 100644 --- a/frontend/src/ts/components/pages/profile/UserDetails.tsx +++ b/frontend/src/ts/components/pages/profile/UserDetails.tsx @@ -1,3 +1,4 @@ +import { getRegularChallenges } from "@monkeytype/challenges"; import { TypingStats as TypingStatsType, UserProfile, @@ -9,6 +10,7 @@ import { getCurrentDayTimestamp, } from "@monkeytype/util/date-and-time"; import { isSafeNumber } from "@monkeytype/util/numbers"; +import { typedKeys } from "@monkeytype/util/objects"; import { differenceInDays } from "date-fns/differenceInDays"; import { formatDate } from "date-fns/format"; import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict"; @@ -80,6 +82,7 @@ export function UserDetails(props: { @@ -409,6 +412,7 @@ function BioAndKeyboard(props: { function TypingStats(props: { typingStats: TypingStatsType; + completedChallenges: number | undefined; variant: Variant; }): JSXElement { const stats = () => formatTypingStatsRatio(props.typingStats); @@ -429,13 +433,13 @@ function TypingStats(props: { class={cn( "grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-2", props.variant === "basic" && - "sm:grid-cols-3 md:grid-cols-1 lg:grid-cols-3 lg:text-[1.25rem]", + "sm:grid-cols-3 md:grid-cols-1 lg:grid-cols-4 lg:text-[1.25rem]", props.variant === "hasBioOrKeyboard" && "sm:col-span-2 md:order-2 md:col-span-1 md:grid-cols-1", props.variant === "hasSocials" && - "sm:col-span-2 sm:grid-cols-3 md:col-span-1 md:grid-cols-1 lg:grid-cols-3 xl:text-[1.25rem]", + "sm:col-span-2 sm:grid-cols-4 md:col-span-1 md:grid-cols-1 lg:grid-cols-4 xl:text-[1.25rem]", props.variant === "full" && - "sm:col-span-2 sm:grid-cols-3 md:col-span-3 md:grid-cols-3 lg:order-2 lg:col-span-1 lg:grid-cols-1", + "sm:col-span-2 sm:grid-cols-4 md:col-span-3 md:grid-cols-4 lg:order-2 lg:col-span-1 lg:grid-cols-1", )} >
@@ -467,6 +471,16 @@ function TypingStats(props: { )}
+ + +
+
challenges
+
+ {props.completedChallenges}{" "} + / {getRegularChallenges().length} +
+
+
); diff --git a/frontend/src/ts/components/pages/profile/UserProfile.tsx b/frontend/src/ts/components/pages/profile/UserProfile.tsx index 4e8906a311da..58423fd92dc9 100644 --- a/frontend/src/ts/components/pages/profile/UserProfile.tsx +++ b/frontend/src/ts/components/pages/profile/UserProfile.tsx @@ -11,6 +11,7 @@ import { getFormatting } from "../../../states/core"; import { formatTopPercentage } from "../../../utils/misc"; import { Button } from "../../common/Button"; import { ActivityCalendar } from "./ActivityCalendar"; +import { Challenges } from "./Challenges"; import { UserDetails } from "./UserDetails"; export function UserProfile(props: { @@ -55,6 +56,11 @@ export function UserProfile(props: { testActivity={props.profile.testActivity} isAccountPage={props.isAccountPage} /> + + ); } diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 94d611ddbe48..3a1a384b984a 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -11,7 +11,6 @@ import { Config } from "../config/store"; import { configEvent } from "../events/config"; import * as TestState from "../test/test-state"; -import { Challenges } from "@monkeytype/challenges"; import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; import { Config as ConfigType, @@ -27,6 +26,7 @@ import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; import { areUnsortedArraysEqual } from "../utils/arrays"; import { qs } from "../utils/dom"; import { typedKeys } from "@monkeytype/util/objects"; +import { getChallenge } from "@monkeytype/challenges"; let challengeLoading = false; @@ -147,7 +147,7 @@ function verifyRequirement( return [requirementsMet, failReasons]; } -export function verify(result: CompletedEvent): string | null { +export function verify(result: CompletedEvent): ChallengeName | null { const loadedChallenge = getLoadedChallenge(); if (loadedChallenge === null) return null; @@ -209,7 +209,7 @@ export async function setup(challengeName: ChallengeName): Promise { setConfig("funbox", []); - const challenge = Challenges[challengeName]; + const challenge = getChallenge(challengeName); let notitext; try { @@ -333,34 +333,6 @@ export async function setup(challengeName: ChallengeName): Promise { throw new Error("Can't load challenge with current config"); } } else if (challenge.type === "other") { - /* TODO: missing challenge - if (challenge.name === "semimak") { - // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout? - setConfig("mode", "time", { - nosave: true, - }); - setConfig("time", 120, { - nosave: true, - }); - setConfig("language", "english_10k", { - nosave: true, - }); - setConfig("punctuation", true, { - nosave: true, - }); - setConfig("stopOnError", "word", { - nosave: true, - }); - setConfig("layout", "semimak", { - nosave: true, - }); - setConfig("keymapLayout", "overrideSync", { - nosave: true, - }); - setConfig("keymapMode", "static", { - nosave: true, - }); - } else */ if (challengeName === "wingdings") { // Ten Words of Pain: 10-word Master mode test using the Wingdings custom font, no keymap setConfig("mode", "words", { @@ -389,7 +361,7 @@ export async function setup(challengeName: ChallengeName): Promise { } else { showSuccessNotification(`Challenge loaded. ${notitext}`); } - setLoadedChallenge({ name: challengeName, ...challenge }); + setLoadedChallenge(challenge); challengeLoading = false; return true; } catch (e) { diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index cb17d5f81dc3..3cbfb7ea7a9c 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -138,6 +138,7 @@ export async function initSnapshot(): Promise { firstDayOfTheWeek, ); } + snap.challenges = userData.challenges; const hourOffset = userData?.streak?.hourOffset; snap.streakHourOffset = hourOffset ?? undefined; diff --git a/packages/challenges/package.json b/packages/challenges/package.json index a3ffc505242d..da5317a27d41 100644 --- a/packages/challenges/package.json +++ b/packages/challenges/package.json @@ -19,7 +19,8 @@ "george-mapping": "tsx ./scripts/challenge-roles" }, "dependencies": { - "@monkeytype/schemas": "workspace:*" + "@monkeytype/schemas": "workspace:*", + "@monkeytype/util": "workspace:*" }, "devDependencies": { "@monkeytype/tsup-config": "workspace:*", diff --git a/packages/challenges/scripts/challenge-roles.ts b/packages/challenges/scripts/challenge-roles.ts index dd27ec4b8042..80c54edf22c7 100644 --- a/packages/challenges/scripts/challenge-roles.ts +++ b/packages/challenges/scripts/challenge-roles.ts @@ -1,9 +1,7 @@ -import { Challenges } from "../src/index"; +import { getChallenges } from "../src/index"; -const known: Record = Object.fromEntries( - Object.entries(Challenges).map( - ([name, def]) => [name, def.discordRoleId] as [string, string], - ), +const known = Object.fromEntries( + getChallenges().map((it) => [it.name, it.discordRoleId]), ); console.log("roleid mapping"); diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index 493f0f2fc261..4bd5f917ea4c 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -1,6 +1,6 @@ import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; -export const Challenges: Record> = { +const challenges: Record> = { "100hours": { display: "100 hours", autoRole: false, @@ -1082,3 +1082,22 @@ export const Challenges: Record> = { description: "Collect 70 roles.", }, }; + +const map: Record = Object.fromEntries( + Object.entries(challenges).map(([name, def]) => [name, { ...def, name }]), +) as Record; + +const list: Challenge[] = Object.values(map); +const regular: Challenge[] = list.filter((it) => it.type !== "hidden"); + +export function getChallenges(): Challenge[] { + return list; +} + +export function getRegularChallenges(): Challenge[] { + return regular; +} + +export function getChallenge(name: ChallengeName): Challenge { + return map[name]; +} diff --git a/packages/schemas/src/results.ts b/packages/schemas/src/results.ts index fe3b8d594aae..0575644e8dca 100644 --- a/packages/schemas/src/results.ts +++ b/packages/schemas/src/results.ts @@ -10,6 +10,7 @@ import { import { LanguageSchema } from "./languages"; import { Mode, Mode2, Mode2Schema, ModeSchema } from "./shared"; import { DifficultySchema, FunboxSchema } from "./configs"; +import { ChallengeNameSchema } from "./challenges"; export const IncompleteTestSchema = z.object({ acc: PercentageSchema, @@ -136,7 +137,7 @@ export const CompletedEventSchema = ResultBaseSchema.required({ }) .extend({ charTotal: z.number().int().nonnegative(), - challenge: token().max(100).optional(), + challenge: ChallengeNameSchema.optional(), customText: CompletedEventCustomTextSchema.optional(), hash: token().max(100), keyDuration: z.array(z.number().nonnegative()).or(z.literal("toolong")), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76b19160ded2..fe94e485d080 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -653,6 +653,9 @@ importers: '@monkeytype/schemas': specifier: workspace:* version: link:../schemas + '@monkeytype/util': + specifier: workspace:* + version: link:../util devDependencies: '@monkeytype/tsup-config': specifier: workspace:* From 62c4b2e6b7ea62b241a8d3297b1e7c24b00ec05d Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 25 Jun 2026 09:29:55 +0200 Subject: [PATCH 09/12] refactor challenge type --- backend/src/constants/auto-roles.ts | 2 +- frontend/scripts/check-assets.ts | 29 +- .../ts/commandline/lists/load-challenge.ts | 26 +- .../components/pages/profile/Challenges.tsx | 8 +- .../ts/controllers/challenge-controller.ts | 90 +- frontend/src/ts/states/test.ts | 2 +- packages/challenges/src/index.ts | 994 ++++++++++-------- packages/schemas/src/challenges.ts | 66 -- 8 files changed, 596 insertions(+), 621 deletions(-) diff --git a/backend/src/constants/auto-roles.ts b/backend/src/constants/auto-roles.ts index dba66537ce23..75d6ca13a140 100644 --- a/backend/src/constants/auto-roles.ts +++ b/backend/src/constants/auto-roles.ts @@ -1,5 +1,5 @@ import { getChallenges } from "@monkeytype/challenges"; export default getChallenges() - .filter((it) => it.autoRole) + .filter((it) => it.settings?.autoRole) .map((it) => it.name); diff --git a/frontend/scripts/check-assets.ts b/frontend/scripts/check-assets.ts index 8ba8e7fa9400..bc74ae38adaf 100644 --- a/frontend/scripts/check-assets.ts +++ b/frontend/scripts/check-assets.ts @@ -2,7 +2,6 @@ * Example usage in root or frontend: * pnpm check-assets (npm run check-assets) * pnpm check-assets -- -- quotes others (npm run check-assets -- -- quotes others) - * pnpm check-assets -- -- challenges sound -p (npm run check-assets -- -- challenges sound -p) */ import * as fs from "fs"; @@ -18,7 +17,6 @@ import { KnownFontName } from "@monkeytype/schemas/fonts"; import { Fonts } from "../src/ts/constants/fonts"; import { themes, ThemeSchema, ThemesList } from "../src/ts/constants/themes"; import { z } from "zod"; -import { ChallengeSchema, Challenge } from "@monkeytype/schemas/challenges"; import { LayoutObject, LayoutObjectSchema } from "@monkeytype/schemas/layouts"; import { QuoteDataSchema, QuoteData } from "@monkeytype/schemas/quotes"; import { clickSoundConfig } from "../src/ts/constants/sounds"; @@ -99,24 +97,6 @@ function findDuplicates(items: T[]): T[] { return Array.from(duplicates); } -async function validateChallenges(): Promise { - const problems = new Problems<"_list.json", never>("Challenges", {}); - - const challengesData = JSON.parse( - fs.readFileSync("./static/challenges/_list.json", { - encoding: "utf8", - flag: "r", - }), - ) as Challenge; - const validationResult = z.array(ChallengeSchema).safeParse(challengesData); - problems.addValidation("_list.json", validationResult); - - console.log(problems.toString()); - if (problems.hasError()) { - throw new Error("challenges with errors"); - } -} - async function validateLayouts(): Promise { const problems = new Problems("Layouts", { _additional: @@ -496,17 +476,10 @@ async function main(): Promise { quotes: [validateQuotes], languages: [validateLanguages], layouts: [validateLayouts], - challenges: [validateChallenges], fonts: [validateFonts], themes: [validateThemes], sounds: [validateSounds], - others: [ - validateChallenges, - validateLayouts, - validateFonts, - validateThemes, - validateSounds, - ], + others: [validateLayouts, validateFonts, validateThemes, validateSounds], }; // flags diff --git a/frontend/src/ts/commandline/lists/load-challenge.ts b/frontend/src/ts/commandline/lists/load-challenge.ts index 023a2d4d3d14..a7dfbbfc1524 100644 --- a/frontend/src/ts/commandline/lists/load-challenge.ts +++ b/frontend/src/ts/commandline/lists/load-challenge.ts @@ -1,4 +1,4 @@ -import { getRegularChallenges } from "@monkeytype/challenges"; +import { getChallenges } from "@monkeytype/challenges"; import * as ChallengeController from "../../controllers/challenge-controller"; import { navigate } from "../../controllers/route-controller"; import * as TestLogic from "../../test/test-logic"; @@ -7,17 +7,19 @@ import { Command, CommandsSubgroup } from "../types"; const subgroup: CommandsSubgroup = { title: "Load challenge...", - list: getRegularChallenges().map((challenge) => ({ - id: `loadChallenge${capitalizeFirstLetterOfEachWord(challenge.name)}`, - display: challenge.display, - exec: async (): Promise => { - await navigate("/"); - await ChallengeController.setup(challenge.name); - TestLogic.restart({ - nosave: true, - }); - }, - })), + list: getChallenges() + .filter((it) => it.settings !== undefined) + .map((challenge) => ({ + id: `loadChallenge${capitalizeFirstLetterOfEachWord(challenge.name)}`, + display: challenge.display, + exec: async (): Promise => { + await navigate("/"); + await ChallengeController.setup(challenge.name); + TestLogic.restart({ + nosave: true, + }); + }, + })), }; const commands: Command[] = [ diff --git a/frontend/src/ts/components/pages/profile/Challenges.tsx b/frontend/src/ts/components/pages/profile/Challenges.tsx index b283e5e808b9..bb7bc34c01ec 100644 --- a/frontend/src/ts/components/pages/profile/Challenges.tsx +++ b/frontend/src/ts/components/pages/profile/Challenges.tsx @@ -1,5 +1,9 @@ -import { getChallenge, getRegularChallenges } from "@monkeytype/challenges"; -import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; +import { + Challenge, + getChallenge, + getRegularChallenges, +} from "@monkeytype/challenges"; +import { ChallengeName } from "@monkeytype/schemas/challenges"; import { UserChallenges } from "@monkeytype/schemas/users"; import { typedEntries } from "@monkeytype/util/objects"; import { createMemo, For, Show } from "solid-js"; diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 3a1a384b984a..915251bfa352 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -11,22 +11,17 @@ import { Config } from "../config/store"; import { configEvent } from "../events/config"; import * as TestState from "../test/test-state"; -import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; -import { - Config as ConfigType, - Difficulty, - FunboxName, - ThemeName, -} from "@monkeytype/schemas/configs"; +import { ChallengeSettings, getChallenge } from "@monkeytype/challenges"; +import { ChallengeName } from "@monkeytype/schemas/challenges"; +import { Difficulty, FunboxName, ThemeName } from "@monkeytype/schemas/configs"; import { CompletedEvent } from "@monkeytype/schemas/results"; import { Mode } from "@monkeytype/schemas/shared"; import { CustomTextLimitMode, CustomTextMode } from "@monkeytype/schemas/util"; +import { typedKeys } from "@monkeytype/util/objects"; import { hideLoaderBar, showLoaderBar } from "../states/loader-bar"; import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; import { areUnsortedArraysEqual } from "../utils/arrays"; import { qs } from "../utils/dom"; -import { typedKeys } from "@monkeytype/util/objects"; -import { getChallenge } from "@monkeytype/challenges"; let challengeLoading = false; @@ -43,8 +38,8 @@ export function clearActive(): void { function verifyRequirement( result: CompletedEvent, - requirements: NonNullable, - requirementType: keyof NonNullable, + requirements: NonNullable, + requirementType: keyof NonNullable, ): [boolean, string[]] { let requirementsMet = true; let failReasons: string[] = []; @@ -138,7 +133,7 @@ function verifyRequirement( const requirementValue = requirements.config; for (const configKey of typedKeys(requirementValue)) { const configValue = requirementValue[configKey]; - if (Config[configKey as keyof ConfigType] !== configValue) { + if (Config[configKey] !== configValue) { requirementsMet = false; failReasons.push(`${configKey} not set to ${configValue}`); } @@ -160,16 +155,18 @@ export function verify(result: CompletedEvent): ChallengeName | null { return null; } - if (loadedChallenge.requirements === undefined) { + if (loadedChallenge.settings?.requirements === undefined) { showSuccessNotification(`${loadedChallenge.display} challenge passed!`); return loadedChallenge.name || null; } else { let requirementsMet = true; const failReasons: string[] = []; - for (const requirementType of typedKeys(loadedChallenge.requirements)) { + for (const requirementType of typedKeys( + loadedChallenge.settings.requirements, + )) { const [passed, requirementFailReasons] = verifyRequirement( result, - loadedChallenge.requirements, + loadedChallenge.settings.requirements, requirementType, ); if (!passed) { @@ -178,7 +175,7 @@ export function verify(result: CompletedEvent): ChallengeName | null { failReasons.push(...requirementFailReasons); } if (requirementsMet) { - if (loadedChallenge.autoRole) { + if (loadedChallenge.settings.autoRole) { showSuccessNotification( "You will receive a role shortly. Please don't post a screenshot in challenge submissions.", { durationMs: 5000 }, @@ -210,19 +207,20 @@ export async function setup(challengeName: ChallengeName): Promise { setConfig("funbox", []); const challenge = getChallenge(challengeName); + const settings = challenge.settings; let notitext; try { - if (challenge === undefined) { - showNoticeNotification("Challenge not found"); + if (challenge === undefined || settings === undefined) { + showNoticeNotification("Challenge not found or missing settings"); setTimeout(() => { qs("header .config")?.show(); qs(".page.pageTest")?.show(); }, 250); return false; } - if (challenge.type === "customTime") { - setConfig("time", challenge.parameters[0] as number, { + if (settings.type === "customTime") { + setConfig("time", settings.parameters[0] as number, { nosave: true, }); setConfig("mode", "time", { @@ -242,8 +240,8 @@ export async function setup(challengeName: ChallengeName): Promise { nosave: true, }); } - } else if (challenge.type === "customWords") { - setConfig("words", challenge.parameters[0] as number, { + } else if (settings.type === "customWords") { + setConfig("words", settings.parameters[0] as number, { nosave: true, }); setConfig("mode", "words", { @@ -252,22 +250,22 @@ export async function setup(challengeName: ChallengeName): Promise { setConfig("difficulty", "normal", { nosave: true, }); - } else if (challenge.type === "customText") { - CustomText.setText((challenge.parameters[0] as string).split(" ")); - CustomText.setMode(challenge.parameters[1] as CustomTextMode); - CustomText.setLimitValue(challenge.parameters[2] as number); - CustomText.setLimitMode(challenge.parameters[3] as CustomTextLimitMode); - CustomText.setPipeDelimiter(challenge.parameters[4] as boolean); + } else if (settings.type === "customText") { + CustomText.setText((settings.parameters[0] as string).split(" ")); + CustomText.setMode(settings.parameters[1] as CustomTextMode); + CustomText.setLimitValue(settings.parameters[2] as number); + CustomText.setLimitMode(settings.parameters[3] as CustomTextLimitMode); + CustomText.setPipeDelimiter(settings.parameters[4] as boolean); setConfig("mode", "custom", { nosave: true, }); setConfig("difficulty", "normal", { nosave: true, }); - } else if (challenge.type === "script") { + } else if (settings.type === "script") { showLoaderBar(); const response = await fetch( - `/challenges/${challenge.parameters[0] as string}`, + `/challenges/${settings.parameters[0] as string}`, ); hideLoaderBar(); if (response.status !== 200) { @@ -287,13 +285,13 @@ export async function setup(challengeName: ChallengeName): Promise { setConfig("difficulty", "normal", { nosave: true, }); - if (challenge.parameters[1] !== null) { - setConfig("theme", challenge.parameters[1] as ThemeName); + if (settings.parameters[1] !== null) { + setConfig("theme", settings.parameters[1] as ThemeName); } - if (challenge.parameters[2] !== null) { - void Funbox.activate(challenge.parameters[2] as FunboxName[]); + if (settings.parameters[2] !== null) { + void Funbox.activate(settings.parameters[2] as FunboxName[]); } - } else if (challenge.type === "accuracy") { + } else if (settings.type === "accuracy") { setConfig("time", 0, { nosave: true, }); @@ -303,36 +301,36 @@ export async function setup(challengeName: ChallengeName): Promise { setConfig("difficulty", "master", { nosave: true, }); - } else if (challenge.type === "funbox") { + } else if (settings.type === "funbox") { setConfig("difficulty", "normal", { nosave: true, }); - if (challenge.parameters[1] === "words") { - setConfig("words", challenge.parameters[2] as number, { + if (settings.parameters[1] === "words") { + setConfig("words", settings.parameters[2] as number, { nosave: true, }); - } else if (challenge.parameters[1] === "time") { - setConfig("time", challenge.parameters[2] as number, { + } else if (settings.parameters[1] === "time") { + setConfig("time", settings.parameters[2] as number, { nosave: true, }); } - setConfig("mode", challenge.parameters[1] as Mode, { + setConfig("mode", settings.parameters[1] as Mode, { nosave: true, }); - if (challenge.parameters[3] !== undefined) { - setConfig("difficulty", challenge.parameters[3] as Difficulty, { + if (settings.parameters[3] !== undefined) { + setConfig("difficulty", settings.parameters[3] as Difficulty, { nosave: true, }); } if ( - !setConfig("funbox", challenge.parameters[0] as FunboxName[], { + !setConfig("funbox", settings.parameters[0] as FunboxName[], { nosave: true, }) ) { throw new Error("Can't load challenge with current config"); } - } else if (challenge.type === "other") { + } else if (settings.type === "other") { if (challengeName === "wingdings") { // Ten Words of Pain: 10-word Master mode test using the Wingdings custom font, no keymap setConfig("mode", "words", { @@ -352,7 +350,7 @@ export async function setup(challengeName: ChallengeName): Promise { }); } } - notitext = challenge.message; + notitext = settings.message; qs("header .config")?.show(); qs(".page.pageTest")?.show(); diff --git a/frontend/src/ts/states/test.ts b/frontend/src/ts/states/test.ts index 9654775e29f0..367452e23cd9 100644 --- a/frontend/src/ts/states/test.ts +++ b/frontend/src/ts/states/test.ts @@ -1,5 +1,4 @@ import { createSignal, createEffect, createMemo } from "solid-js"; -import { Challenge } from "@monkeytype/schemas/challenges"; import { getConfig } from "../config/store"; import { canQuickRestart } from "../utils/quick-restart"; @@ -8,6 +7,7 @@ import { getActivePage, getCustomTextIndicator } from "./core"; import { QuoteWithTextSplit } from "../types/quotes"; import { CompletedEvent, IncompleteTest } from "@monkeytype/schemas/results"; import { createSignalWithSetters } from "../hooks/createSignalWithSetters"; +import { Challenge } from "@monkeytype/challenges"; export const [wordsHaveNewline, setWordsHaveNewline] = createSignal(false); export const [wordsHaveTab, setWordsHaveTab] = createSignal(false); diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index 4bd5f917ea4c..44adb33c24f3 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -1,904 +1,989 @@ -import { Challenge, ChallengeName } from "@monkeytype/schemas/challenges"; +import { ChallengeName } from "@monkeytype/schemas/challenges"; +import { Config, FunboxName } from "@monkeytype/schemas/configs"; + +export type ChallengeSettings = { + autoRole?: boolean; + type: + | "customTime" + | "customWords" + | "customText" + | "script" + | "accuracy" + | "funbox" + | "other"; + message?: string; + requirements?: { + wpm?: { min: number } | { exact: number }; + acc?: { min: number } | { exact: number }; + raw?: { exact: number }; + con?: { exact: number }; + afk?: { max: number }; + time?: { min: number }; + funbox?: { exact: FunboxName[] }; + config?: Partial; + }; + parameters: (string | null | number | boolean | FunboxName[])[]; +}; + +export type Challenge = { + name: ChallengeName; + display: string; + description: string; + isHidden?: boolean; + discordRoleId: string; + category: + | "other" + | "endurance" + | "script" + | "speed" + | "accuracy" + | "funbox" + | "champions" + | "roleCount"; + settings?: ChallengeSettings; +}; const challenges: Record> = { + "69": { + display: "6969696969", + isHidden: true, + discordRoleId: "749505965174292511", + category: "other", + description: + "Complete a 69-second test and achieve 69 WPM, 69 raw, 69% accuracy, and 69% consistency.", + settings: { + autoRole: true, + type: "customTime", + message: + "You need to achieve 69 wpm, 69 raw, 69% accuracy and 69% consistency.", + parameters: [69], + requirements: { + wpm: { exact: 69 }, + raw: { exact: 69 }, + acc: { exact: 69 }, + con: { exact: 69 }, + }, + }, + }, "100hours": { display: "100 hours", - autoRole: false, - type: "hidden", + isHidden: true, discordRoleId: "761766710704603166", category: "other", description: "Achieve 100 hours of typing.", - parameters: [], }, "250hours": { display: "250 hours", - autoRole: false, - type: "hidden", + isHidden: true, discordRoleId: "799825381733433344", category: "other", description: "Achieve 250 hours of typing.", - parameters: [], }, "500hours": { display: "500 hours", - autoRole: false, - type: "hidden", + isHidden: true, discordRoleId: "951861792622125106", category: "other", description: "Achieve 500 hours of typing.", - parameters: [], }, "1000hours": { display: "1000 hours", - autoRole: false, - type: "hidden", + isHidden: true, discordRoleId: "1262175323588395100", category: "other", description: "Achieve 1000 hours of typing.", - parameters: [], - }, - "69": { - display: "6969696969", - autoRole: true, - type: "customTime", - parameters: [69], - message: - "You need to achieve 69 wpm, 69 raw, 69% accuracy and 69% consistency.", - requirements: { - wpm: { exact: 69 }, - raw: { exact: 69 }, - acc: { exact: 69 }, - con: { exact: 69 }, - }, - discordRoleId: "749505965174292511", - category: "other", - description: - "Complete a 69-second test and achieve 69 WPM, 69 raw, 69% accuracy, and 69% consistency.", }, oneHourWarrior: { display: "One Hour Warrior", - autoRole: true, - type: "customTime", - parameters: [3600], - requirements: { - time: { min: 3600 }, - }, + isHidden: true, discordRoleId: "728371749737201855", category: "endurance", - description: "Complete a one-hour test.", + description: "Complete an one-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: [3600], + requirements: { time: { min: 3600 } }, + }, }, doubleDown: { display: "Double Down", - autoRole: true, - type: "customTime", - parameters: [7200], - requirements: { - time: { min: 7200 }, - }, + isHidden: true, discordRoleId: "732008008514535544", category: "endurance", description: "Complete a two-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: [7200], + requirements: { time: { min: 7200 } }, + }, }, tripleTrouble: { display: "Triple Trouble", - autoRole: true, - type: "customTime", - parameters: [10800], - requirements: { - time: { min: 10800 }, - }, + isHidden: true, discordRoleId: "732008047618293762", category: "endurance", description: "Complete a three-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: [10800], + requirements: { time: { min: 10800 } }, + }, }, quad: { display: "Quaaaaad", - autoRole: true, - type: "customTime", - parameters: [14400], - requirements: { - time: { min: 14400 }, - }, + isHidden: true, discordRoleId: "736215666352455801", category: "endurance", description: "Complete a four-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: [14400], + requirements: { time: { min: 14400 } }, + }, }, "8Ball": { display: "8 Ball", - type: "customTime", - parameters: [28800], - requirements: { - time: { min: 28800 }, - }, + isHidden: true, discordRoleId: "736528159956271126", category: "endurance", description: "Complete an eight-hour test.", + settings: { + type: "customTime", + parameters: [28800], + requirements: { time: { min: 28800 } }, + }, }, theBig12: { display: "The Big 12", - type: "customTime", - parameters: [43200], - requirements: { - time: { min: 43200 }, - }, + isHidden: true, discordRoleId: "740532256388546581", category: "endurance", description: "Complete a twelve-hour test.", + settings: { + type: "customTime", + parameters: [43200], + requirements: { time: { min: 43200 } }, + }, }, "1Day": { display: "1 Day", - type: "customTime", - parameters: [86400], - requirements: { - time: { min: 86400 }, - }, + isHidden: true, discordRoleId: "751801958511149057", category: "endurance", description: "Complete a twenty-four-hour test.", + settings: { + type: "customTime", + parameters: [86400], + requirements: { time: { min: 86400 } }, + }, }, trueSimp: { display: "True Simp", - autoRole: true, - type: "customText", - parameters: ["miodec", "repeat", 10000, "word", false], + isHidden: true, discordRoleId: "744328648211038359", category: "script", description: "Type miodec ten thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: ["miodec", "repeat", 10000, "word", false], + }, }, bigramSalad: { display: "Bigram Salad", - autoRole: true, - type: "customText", - parameters: [ - "to of in it is as at be we he so on an or do if up by my go", - "random", - 100, - "word", - false, - ], - requirements: { - wpm: { min: 100 }, - }, + isHidden: true, discordRoleId: "818535054145093652", category: "speed", description: "Get 100 WPM on a randomized, 100-word custom test with the words list: to of in it is as at be we he so on an or do if up by my go.", + settings: { + autoRole: true, + type: "customText", + parameters: [ + "to of in it is as at be we he so on an or do if up by my go", + "random", + 100, + "word", + false, + ], + requirements: { wpm: { min: 100 } }, + }, }, simp: { display: "Simp", - autoRole: true, - type: "customText", - parameters: ["miodec", "repeat", 1000, "word", false], + isHidden: true, discordRoleId: "743854992699687023", category: "script", description: "Type miodec one thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: ["miodec", "repeat", 1000, "word", false], + }, }, simpLord: { display: "Simp Lord", - // false for now - autoRole: false, - type: "customText", - // this would be 100k times: - parameters: ["miodec", "repeat", 100000, "word", false], + isHidden: true, discordRoleId: "984911956949479445", category: "script", description: "Type miodec one hundred thousand times.", + settings: { + autoRole: false, + type: "customText", + parameters: ["miodec", "repeat", 100000, "word", false], + }, }, antidiseWhat: { display: "Antidise-what?", - autoRole: true, - type: "customText", - parameters: ["antidisestablishmentarianism", "repeat", 1, "word", false], - requirements: { - wpm: { min: 200 }, - }, + isHidden: true, discordRoleId: "782006507360616449", category: "script", description: "Get at least 200 wpm typing antidisestablishmentarianism.", + settings: { + autoRole: true, + type: "customText", + parameters: ["antidisestablishmentarianism", "repeat", 1, "word", false], + requirements: { wpm: { min: 200 } }, + }, }, whatsThisWebsiteCalledAgain: { display: "What's this website called again?", - autoRole: true, - type: "customText", - parameters: ["monkeytype", "repeat", 1000, "word", false], + isHidden: true, discordRoleId: "739276161603076116", category: "script", description: "Type monkeytype one thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: ["monkeytype", "repeat", 1000, "word", false], + }, }, developd: { display: "Develop'd", - autoRole: true, - type: "customText", - parameters: ["develop", "repeat", 1000, "word", false], + isHidden: true, discordRoleId: "735964917877964932", category: "script", description: "Type develop one thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: ["develop", "repeat", 1000, "word", false], + }, }, slowAndSteady: { display: "Slow and Steady", - autoRole: true, - type: "customTime", - parameters: [300], - requirements: { - wpm: { exact: 60 }, - config: { liveSpeedStyle: "off", paceCaret: "off" }, - }, + isHidden: true, discordRoleId: "782005061935956008", category: "speed", description: "Complete a 5-minute test with exactly 60 WPM without using the live WPM or pace caret.", + settings: { + autoRole: true, + type: "customTime", + parameters: [300], + requirements: { + wpm: { exact: 60 }, + config: { liveSpeedStyle: "off", paceCaret: "off" }, + }, + }, }, speedSpacer: { display: "Speed Spacer", - autoRole: true, - type: "customText", - parameters: [ - "a b c d e f g h i j k l m n o p q r s t u v w x y z", - "random", - 100, - "word", - false, - ], - requirements: { - wpm: { min: 100 }, - }, + isHidden: true, discordRoleId: "755244049446731856", category: "speed", description: "Get 100 wpm on a randomised custom test with the input: a b c d e f g h i j k l m n o p q r s t u v w x y z (the alphabet) and a word count of 100.", + settings: { + autoRole: true, + type: "customText", + parameters: [ + "a b c d e f g h i j k l m n o p q r s t u v w x y z", + "random", + 100, + "word", + false, + ], + requirements: { wpm: { min: 100 } }, + }, }, iveGotThePower: { display: "I've got the POWER", - autoRole: true, - type: "customText", - parameters: ["power", "repeat", 10, "word", false], - requirements: { - wpm: { min: 400 }, - }, + isHidden: true, discordRoleId: "764879734873915402", category: "speed", description: "Get 400 WPM while typing power 10 times.", + settings: { + autoRole: true, + type: "customText", + parameters: ["power", "repeat", 10, "word", false], + requirements: { wpm: { min: 400 } }, + }, }, accuracyExpert: { display: "Accuracy Expert", - autoRole: true, - type: "accuracy", - parameters: [], - message: "Minimum 60wpm and 100% accuracy required.", - requirements: { - wpm: { min: 60 }, - acc: { exact: 100 }, - afk: { max: 5 }, - time: { min: 600 }, - }, + isHidden: true, discordRoleId: "751168451263070259", category: "accuracy", description: "Complete a 10-minute Master mode test.", + settings: { + autoRole: true, + type: "accuracy", + message: "Minimum 60wpm and 100% accuracy required.", + parameters: [], + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 600 }, + }, + }, }, accuracyMaster: { display: "Accuracy Master", - autoRole: true, - type: "accuracy", - parameters: [], - message: "Minimum 60wpm and 100% accuracy required.", - requirements: { - wpm: { min: 60 }, - acc: { exact: 100 }, - afk: { max: 5 }, - time: { min: 1200 }, - }, + isHidden: true, discordRoleId: "751168567432708239", category: "accuracy", description: "Complete a 20-minute Master mode test.", + settings: { + autoRole: true, + type: "accuracy", + message: "Minimum 60wpm and 100% accuracy required.", + parameters: [], + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 1200 }, + }, + }, }, accuracyGod: { display: "Accuracy God", - autoRole: true, - type: "accuracy", - parameters: [], - message: "Minimum 60wpm and 100% accuracy required.", - requirements: { - wpm: { min: 60 }, - acc: { exact: 100 }, - afk: { max: 5 }, - time: { min: 1800 }, - }, + isHidden: true, discordRoleId: "751168657626890361", category: "accuracy", description: "Complete a 30-minute Master mode test.", + settings: { + autoRole: true, + type: "accuracy", + message: "Minimum 60wpm and 100% accuracy required.", + parameters: [], + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 1800 }, + }, + }, }, inAGalaxyFarFarAway: { display: "In a galaxy far, far away", - type: "script", - parameters: ["episode4.txt", null, ["space_balls"]], - requirements: { - config: { tapeMode: "off" }, - }, + isHidden: true, discordRoleId: "740004324301602907", category: "script", description: "Type out the entire Star Wars Episode 4 script with punctuation while watching the movie simultaneously.", + settings: { + type: "script", + parameters: ["episode4.txt", null, ["space_balls"]], + requirements: { config: { tapeMode: "off" } }, + }, }, beepBoop: { display: "Beep Boop", - type: "script", - parameters: ["beepboop.txt", null, ["nospace"]], - message: "Mininum 45 WPM and 100% accuracy required.", - requirements: { - wpm: { min: 45 }, - acc: { min: 100 }, - funbox: { exact: ["nospace"] }, - }, + isHidden: true, discordRoleId: "813076265145729024", category: "script", description: "Type the beepboop script with 100% accuracy and at least 45 WPM.", + settings: { + type: "script", + message: "Mininum 45 WPM and 100% accuracy required.", + parameters: ["beepboop.txt", null, ["nospace"]], + requirements: { + wpm: { min: 45 }, + acc: { min: 100 }, + funbox: { exact: ["nospace"] }, + }, + }, }, whosYourDaddy: { display: "Who's your daddy", - type: "script", - parameters: ["episode5.txt", null, ["space_balls"]], - requirements: { - config: { tapeMode: "off" }, - }, + isHidden: true, discordRoleId: "742171915405361204", category: "script", description: "Type out the entire Star Wars Episode 5 script with punctuation while watching the movie simultaneously.", + settings: { + type: "script", + parameters: ["episode5.txt", null, ["space_balls"]], + requirements: { config: { tapeMode: "off" } }, + }, }, itsATrap: { display: "It's a trap!!", - type: "script", - parameters: ["episode6.txt", null, ["space_balls"]], - requirements: { - config: { tapeMode: "off" }, - }, + isHidden: true, discordRoleId: "744325174668820550", category: "script", description: "Type out the entire Star Wars Episode 6 script with punctuation while watching the movie simultaneously.", + settings: { + type: "script", + parameters: ["episode6.txt", null, ["space_balls"]], + requirements: { config: { tapeMode: "off" } }, + }, }, jolly: { display: "Jolly", - autoRole: true, - type: "script", - parameters: ["jolly.txt", null, null], - message: "Minimum 70wpm required.", - requirements: { - wpm: { min: 70 }, - }, + isHidden: true, discordRoleId: "768497412548329563", category: "script", description: "Type the Jolly script with a minimum of 70 wpm.", + settings: { + autoRole: true, + type: "script", + message: "Minimum 70wpm required.", + parameters: ["jolly.txt", null, null], + requirements: { wpm: { min: 70 } }, + }, }, gottaCatchEmAll: { display: "Gotta Catch 'Em All", - autoRole: true, - type: "script", - parameters: ["pokemon.txt", null, null], + isHidden: true, discordRoleId: "767069340599975998", category: "script", description: "Type out the names of all Pokemon.", + settings: { + autoRole: true, + type: "script", + parameters: ["pokemon.txt", null, null], + }, }, rapGod: { display: "Rap God", - autoRole: true, - type: "script", - parameters: ["rapgod.txt", null, null], - message: "Minimum 85wpm and 90% accuracy required.", - requirements: { - wpm: { min: 85 }, - acc: { min: 90 }, - afk: { max: 5 }, - }, + isHidden: true, discordRoleId: "743844891045396603", category: "script", description: "Type out the lyrics of Eminem's Rap God at a minimum of 85 WPM and 90% accuracy, including punctuation.", + settings: { + autoRole: true, + type: "script", + message: "Minimum 85wpm and 90% accuracy required.", + parameters: ["rapgod.txt", null, null], + requirements: { wpm: { min: 85 }, acc: { min: 90 }, afk: { max: 5 } }, + }, }, navySeal: { display: "Navy Seal", - autoRole: true, - type: "script", - parameters: ["navyseal.txt", null, null], - message: "Minimum 60wpm and 100% accuracy required.", - requirements: { - wpm: { min: 60 }, - acc: { exact: 100 }, - afk: { max: 5 }, - }, + isHidden: true, discordRoleId: "762345535969165342", category: "script", description: "Type out the Navy Seal copy pasta with 100% accuracy and minimum 60 WPM.", + settings: { + autoRole: true, + type: "script", + message: "Minimum 60wpm and 100% accuracy required.", + parameters: ["navyseal.txt", null, null], + requirements: { wpm: { min: 60 }, acc: { exact: 100 }, afk: { max: 5 } }, + }, }, littleChef: { display: "Little Chef", - type: "script", - parameters: ["littlechef.txt", null, null], + isHidden: true, discordRoleId: "763544714028122153", category: "script", description: "Type out the entire Ratatouille script while watching the movie simultaneously.", + settings: { type: "script", parameters: ["littlechef.txt", null, null] }, }, crosstalk: { display: "(CROSSTALK)", - type: "script", - parameters: ["crosstalk.txt", null, null], + isHidden: true, discordRoleId: "761276009664217129", category: "script", description: "Type out the entire transcript of the first 2020 Presidential Debate.", + settings: { type: "script", parameters: ["crosstalk.txt", null, null] }, }, bees: { display: "Bees!!!", - type: "script", - parameters: ["bees.txt", null, null], + isHidden: true, discordRoleId: "739636003182084307", category: "script", description: "Type out the entire Bee Movie script while watching the movie simultaneously.", + settings: { type: "script", parameters: ["bees.txt", null, null] }, }, getOffMySwamp: { display: "Get Off My Swamp", - type: "script", - parameters: ["shrek.txt", null, null], + isHidden: true, discordRoleId: "757346966987342026", category: "script", description: "Type out the entire Shrek script with punctuation while watching the movie simultaneously.", + settings: { type: "script", parameters: ["shrek.txt", null, null] }, }, fiftyShadesOfHell: { display: "50 Shades of Hell", - type: "script", - parameters: [], + isHidden: true, discordRoleId: "751802155119280128", category: "script", description: "Type out your favourite chapter from 50 Shades of Gray.", + settings: { type: "script", parameters: [] }, }, lookAtMeIAmTheDeveloperNow: { display: "Look at me. I am the developer now.", - autoRole: true, - type: "script", - parameters: ["sourcecode.txt", null, null], + isHidden: true, discordRoleId: "937358772635074600", category: "script", description: - "Type out the entire source code of Monkeytype, as it was in February 2022.", + "Type out the entire source code ofMonkeytype, as it was in February 2022.", + settings: { + autoRole: true, + type: "script", + parameters: ["sourcecode.txt", null, null], + }, }, beLikeWater: { display: "Be Like Water", - type: "funbox", - parameters: [["layoutfluid"], "time", 60], - message: "Remember: You need to achieve at least 50 wpm in each layout.", + isHidden: true, discordRoleId: "740568679485276201", category: "funbox", description: "Achieve at least 50 WPM in all three layouts in a 60-second time test using the layoutfluid mode. Layouts must be unique (e.g., QWERTY, Colemak, Dvorak).", + settings: { + type: "funbox", + message: "Remember: You need to achieve at least 50 wpm in each layout.", + parameters: [["layoutfluid"], "time", 60], + }, }, rollercoaster: { display: "Rollercoaster", - autoRole: true, - type: "funbox", - parameters: [["round_round_baby"], "time", 3600], - requirements: { - time: { min: 3600 }, - funbox: { exact: ["round_round_baby"] }, - }, + isHidden: true, discordRoleId: "736032495526740001", category: "funbox", description: - "Complete at least a one-hour test using the round round baby mode.", + "Complete at least an one-hour test using the round round baby mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["round_round_baby"], "time", 3600], + requirements: { + time: { min: 3600 }, + funbox: { exact: ["round_round_baby"] }, + }, + }, }, oneHourMirror: { display: "ɿoɿɿim ɿυoʜ ɘno", - autoRole: true, - type: "funbox", - parameters: [["mirror"], "time", 3600], - requirements: { - time: { min: 3600 }, - funbox: { exact: ["mirror"] }, - }, + isHidden: true, discordRoleId: "737385182998429757", category: "funbox", - description: "Complete at least a one-hour test using the mirror mode.", + description: "Complete at least an one-hour test using the mirror mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["mirror"], "time", 3600], + requirements: { time: { min: 3600 }, funbox: { exact: ["mirror"] } }, + }, }, chooChoo: { display: "Choo choo", - autoRole: true, - type: "funbox", - parameters: [["choo_choo"], "time", 3600], - requirements: { - time: { min: 3600 }, - funbox: { exact: ["choo_choo"] }, - }, + isHidden: true, discordRoleId: "739306439574683710", category: "funbox", - description: "Complete at least a one-hour test using choo choo mode.", + description: "Complete at least an one-hour test using choo choomode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["choo_choo"], "time", 3600], + requirements: { time: { min: 3600 }, funbox: { exact: ["choo_choo"] } }, + }, }, mnemonist: { display: "Mnemonist", - type: "funbox", - parameters: [["memory"], "words", 25, "master"], - requirements: { - config: { tapeMode: "off" }, - }, + isHidden: true, discordRoleId: "782005606852067328", category: "funbox", description: "Achieve 100+ WPM with 100% accuracy on a 25-word test using the memory funbox.", + settings: { + type: "funbox", + parameters: [["memory"], "words", 25, "master"], + requirements: { config: { tapeMode: "off" } }, + }, }, earfquake: { display: "Earfquake", - autoRole: true, - type: "funbox", - parameters: [["earthquake"], "time", 3600], - requirements: { - time: { min: 3600 }, - funbox: { exact: ["earthquake"] }, - }, + isHidden: true, discordRoleId: "740730587429601291", category: "funbox", description: - "Complete at least a one-hour test using the earthquake funbox mode.", + "Complete at least an one-hour test using the earthquake funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["earthquake"], "time", 3600], + requirements: { time: { min: 3600 }, funbox: { exact: ["earthquake"] } }, + }, }, simonSez: { display: "Simon Sez", - autoRole: true, - type: "funbox", - parameters: [["simon_says"], "time", 3600], - requirements: { - time: { min: 3600 }, - funbox: { exact: ["simon_says"] }, - }, + isHidden: true, discordRoleId: "742128871825997914", category: "funbox", description: - "Complete at least a one-hour test using the simon says funbox mode.", + "Complete at least an one-hour test using the simon says funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["simon_says"], "time", 3600], + requirements: { time: { min: 3600 }, funbox: { exact: ["simon_says"] } }, + }, }, accountant: { display: "Accountant", - autoRole: true, - type: "funbox", - parameters: [["58008"], "time", 3600], - requirements: { - time: { min: 3600 }, - funbox: { exact: ["58008"] }, - }, + isHidden: true, discordRoleId: "743962178821816391", category: "funbox", description: - "Complete at least a one-hour test using the 58008 funbox mode.", + "Complete at least an one-hour test using the 58008 funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["58008"], "time", 3600], + requirements: { time: { min: 3600 }, funbox: { exact: ["58008"] } }, + }, }, hidden: { display: "Hidden", - autoRole: true, - type: "funbox", - parameters: [["read_ahead"], "time", 60], - requirements: { - wpm: { min: 100 }, - time: { min: 60 }, - funbox: { exact: ["read_ahead"] }, - config: { tapeMode: "off" }, - }, + isHidden: true, discordRoleId: "782006137742557194", category: "funbox", description: "Achieve 100+ WPM using the read ahead funbox on a 60-second test.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["read_ahead"], "time", 60], + requirements: { + wpm: { min: 100 }, + time: { min: 60 }, + funbox: { exact: ["read_ahead"] }, + config: { tapeMode: "off" }, + }, + }, }, iCanSeeTheFuture: { display: "I can see the future", - autoRole: true, - type: "funbox", - parameters: [["read_ahead_hard"], "time", 60], - requirements: { - wpm: { min: 100 }, - time: { min: 60 }, - funbox: { exact: ["read_ahead_hard"] }, - config: { tapeMode: "off" }, - }, + isHidden: true, discordRoleId: "814877508008411226", category: "funbox", description: "Achieve 100+ WPM using the read ahead hard funbox on a 60-second test.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["read_ahead_hard"], "time", 60], + requirements: { + wpm: { min: 100 }, + time: { min: 60 }, + funbox: { exact: ["read_ahead_hard"] }, + config: { tapeMode: "off" }, + }, + }, }, whatAreWordsAtThisPoint: { display: "What are words at this point", - autoRole: true, - type: "funbox", - parameters: [["gibberish"], "time", 3600], - requirements: { - time: { min: 60 }, - funbox: { exact: ["gibberish"] }, - }, + isHidden: true, discordRoleId: "744209241396740176", category: "funbox", description: - "Complete at least a one-hour test using the gibberish funbox mode.", + "Complete at least an one-hour test using the gibberish funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["gibberish"], "time", 3600], + requirements: { time: { min: 60 }, funbox: { exact: ["gibberish"] } }, + }, }, specials: { display: "Specials", - autoRole: true, - type: "funbox", - parameters: [["specials"], "time", 3600], - requirements: { - time: { min: 60 }, - funbox: { exact: ["specials"] }, - }, + isHidden: true, discordRoleId: "744209452714033162", category: "funbox", description: - "Complete at least a one-hour test using the specials funbox mode.", + "Complete at least an one-hour test using the specials funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["specials"], "time", 3600], + requirements: { time: { min: 60 }, funbox: { exact: ["specials"] } }, + }, }, aeiou: { display: "Aeiou.", - autoRole: true, - type: "funbox", - parameters: [["tts"], "time", 3600], - requirements: { - time: { min: 60 }, - funbox: { exact: ["tts"] }, - }, + isHidden: true, discordRoleId: "744318102766092362", category: "funbox", - description: "Complete at least a one-hour test using the tts funbox mode.", + description: + "Complete at least an one-hour test using the tts funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["tts"], "time", 3600], + requirements: { time: { min: 60 }, funbox: { exact: ["tts"] } }, + }, }, asciiWarrior: { display: "ASCII warrior", - autoRole: true, - type: "funbox", - parameters: [["ascii"], "time", 3600], - requirements: { - time: { min: 60 }, - funbox: { exact: ["ascii"] }, - }, + isHidden: true, discordRoleId: "746142791326760980", category: "funbox", description: - "Complete at least a one-hour test using the ascii funbox mode.", + "Complete at least an one-hour test using the ascii funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["ascii"], "time", 3600], + requirements: { time: { min: 60 }, funbox: { exact: ["ascii"] } }, + }, }, iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS: { display: "i KINda LikE HoW inEFFICIeNt QwErtY Is.", - autoRole: true, - type: "funbox", - parameters: [["sPoNgEcAsE"], "time", 3600], - requirements: { - time: { min: 60 }, - funbox: { exact: ["sPoNgEcAsE"] }, - }, + isHidden: true, discordRoleId: "760999194525171724", category: "funbox", description: - "Complete at least a one-hour test using the randomcase funbox mode.", + "Complete at least an one-hour test using the randomcase funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["sPoNgEcAsE"], "time", 3600], + requirements: { time: { min: 60 }, funbox: { exact: ["sPoNgEcAsE"] } }, + }, }, oneNauseousMonkey: { display: "One Nauseous Monkey", - autoRole: true, - type: "funbox", - parameters: [["nausea"], "time", 3600], - requirements: { - time: { min: 60 }, - funbox: { exact: ["nausea"] }, - }, + isHidden: true, discordRoleId: "760930262740631633", category: "funbox", description: - "Complete at least a one-hour test using the nausea funbox mode.", + "Complete at least an one-hour test using the nausea funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: [["nausea"], "time", 3600], + requirements: { time: { min: 60 }, funbox: { exact: ["nausea"] } }, + }, }, thumbWarrior: { display: "Thumb Warrior", - type: "customTime", - parameters: [3600], + isHidden: true, discordRoleId: "761794585109200906", category: "other", - description: "Complete a one-hour test using only your thumbs.", + description: "Complete an one-hour test using only your thumbs.", + settings: { type: "customTime", parameters: [3600] }, }, mouseWarrior: { display: "Mouse warrior", - type: "customTime", - parameters: [3600], + isHidden: true, discordRoleId: "744580294442614790", category: "other", description: - "Complete a one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", + "Complete an one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", + settings: { type: "customTime", parameters: [3600] }, }, mobileWarrior: { display: "Mobile warrior", - type: "customTime", - parameters: [3600], + isHidden: true, discordRoleId: "744723801526370407", category: "other", - description: "Complete a one-hour test on mobile.", + description: "Complete an one-hour test on mobile.", + settings: { type: "customTime", parameters: [3600] }, }, upsideDown: { display: "uʍop ǝpᴉsdn", - type: "customTime", - parameters: [60], + isHidden: true, discordRoleId: "782725716114014237", category: "other", description: - "Achieve at least 60 WPM on a one-minute test with your keyboard upside down.", + "Achieve at least 60 WPM on an one-minute test with your keyboard upside down.", + settings: { type: "customTime", parameters: [60] }, }, oneArmedBandit: { display: "One armed bandit", - type: "customWords", - parameters: [10000], + isHidden: true, discordRoleId: "765919192557682708", category: "other", description: - "Complete a one-hour or 10k words test (whichever comes sooner, using an external timer) using a one-handed words list (either left or right) for your layout.", + "Complete an one-hour or 10k words test (whichever comes sooner, using an external timer) using an one-handed words list (either left or right) for your layout.", + settings: { type: "customWords", parameters: [10000] }, }, englishMaster: { display: "English master", - autoRole: true, - type: "customTime", - parameters: [3600], - requirements: { - time: { min: 3600 }, - config: { language: "english_10k", punctuation: true, numbers: true }, - }, + isHidden: true, discordRoleId: "751166528824672396", category: "other", description: - "Complete a one-hour test using English 10k language with punctuation and numbers enabled.", + "Complete an one-hour test using English 10k language with punctuation and numbers enabled.", + settings: { + autoRole: true, + type: "customTime", + parameters: [3600], + requirements: { + time: { min: 3600 }, + config: { language: "english_10k", punctuation: true, numbers: true }, + }, + }, }, feetWarrior: { display: "Foot Warrior", - type: "customTime", - parameters: [3600], + isHidden: true, discordRoleId: "751953592860147822", category: "other", - description: "Complete a one-hour test using your feet. Don't ask me why.", + description: "Complete an one-hour test using your feet. Don't ask me why.", + settings: { type: "customTime", parameters: [3600] }, }, wingdings: { display: "Ten Words of Pain", - type: "other", - parameters: [], - message: - "Complete a 10-word Master mode test using the Wingdings custom font. No keymap allowed. Minimum 60 WPM and 100% accuracy required.", - requirements: { - acc: { exact: 100 }, - }, + isHidden: true, discordRoleId: "863192575984140338", category: "other", description: "Complete a 10-word Master mode test using the Wingdings custom font.", + settings: { + type: "other", + message: + "Complete a 10-word Master mode test using the Wingdings custom font. No keymap allowed. Minimum 60 WPM and 100% accuracy required.", + parameters: [], + requirements: { acc: { exact: 100 } }, + }, }, ultimateMonkeyFlex: { display: "Ultimate Monkey Flex", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "768497815496032266", category: "champions", description: "Have the most champion roles in the server.", }, oneRoleToRuleThemAll: { display: "One role to rule them all", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "758784729151176755", category: "champions", description: "Have the most challenge roles in the server.", }, doYouKnowTheDefinitionOfInsanity: { display: "Do You Know The Definition Of Insanity", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "736527448757370880", category: "champions", description: "Complete the longest typing session in Monkeytype history.", }, oneHourChampion: { display: "One Hour Champion", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "728650773503934464", category: "champions", - description: "Achieve the highest WPM in a one-hour test.", + description: "Achieve the highest WPM in an one-hour test.", }, fluidChampion: { display: "Fluid Champion", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "740568718719058041", category: "champions", description: "Achieve the highest WPM in a 60-second layoutfluid test.", }, accuracyChampion: { display: "Accuracy Champion", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "768499906511110235", category: "champions", description: "Achieve the longest Master mode test.", }, literallyTheFastestPersonHere: { display: "Literally The Fastest Person Here", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "984922187385405460", category: "champions", description: "Achieve 1st place on the time 60 English all-time leaderboard.", }, - // fehmer suggested putting it here under the champions section (as its a role obtainable by anyone) bananaHoarder: { display: "Banana Hoarder", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "773590599227932754", category: "champions", description: "Achieve 1st place on the banana leaderboard.", }, alpha: { display: "A l p h a", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "773590612762034176", category: "speed", description: - "Type the alphabet, with each letter separated by a space and in alphabetical order a b c d e f g h i j k l m n o p q r s t u v w x y z in LESS than 3.37 seconds.", + "Type a b c d e f g h i j k l m n o p q r s t u v w x y z in LESS than 3.37 seconds.", }, blazeIt: { display: "Blaze It", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "803650889461006346", category: "speed", description: "Achieve 420 WPM (can be rounded) by typing weed.", }, burstMaster: { display: "Burst Master", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "757330922726096917", category: "speed", description: "Achieve 200+ WPM on the words 10 mode.", }, burstGod: { display: "Burst God", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "757330992821305366", category: "speed", description: "Achieve 250+ WPM on the words 10 mode.", }, shotgun: { display: "Shotgun", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "757331084366184539", category: "speed", description: "Achieve 300+ WPM on the words 10 mode.", }, nuke: { display: "Nuke", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "912522664604758016", category: "speed", description: "Achieve 350+ WPM on the words 10 mode.", }, orbitalCannon: { display: "Orbital Cannon", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "1084094136199684196", category: "speed", description: "Achieve 400+ WPM on the words 10 mode.", }, marathonSprinter: { display: "Marathon Sprinter", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "878715678830510111", category: "speed", - description: "Achieve 200+ WPM on a one-hour test.", + description: "Achieve 200+ WPM on an one-hour test.", }, flawless: { display: "Flawless", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "767070815987695637", category: "accuracy", description: @@ -906,8 +991,7 @@ const challenges: Record> = { }, hesBeginningToBelieve: { display: "He's beginning to believe", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "979729541096431688", category: "accuracy", description: @@ -915,16 +999,14 @@ const challenges: Record> = { }, goldenHands: { display: "Golden Hands", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "851096860969795684", category: "accuracy", description: "Complete a 1-hour Master mode test.", }, fingerBlaster: { display: "Finger Blaster", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "787509606992969728", category: "other", description: @@ -932,25 +1014,22 @@ const challenges: Record> = { }, whyAreTheWallsMoving: { display: "Why are the walls moving?", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "910078947302191114", category: "other", - description: "Complete a one-hour test using tape mode and letter mode.", + description: "Complete an one-hour test using tape mode and letter mode.", }, stickman: { display: "stickman", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "788107449151651890", category: "other", description: - "Complete a one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", + "Complete an one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", }, waveDynamics: { display: "Wave Dynamics", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "1443311363794407586", category: "other", description: @@ -958,17 +1037,15 @@ const challenges: Record> = { }, apesTogetherStrong: { display: "Apes Together Strong", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "863193901153779713", category: "other", description: - "Complete a one-hour test in a Tribe lobby with at least 10 players.", + "Complete an one-hour test in a Tribe lobby with at least 10 players.", }, apesTogetherStronger: { display: "Apes Together Stronger", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "898964842726195220", category: "other", description: @@ -976,8 +1053,7 @@ const challenges: Record> = { }, apesTogetherInvincible: { display: "Apes Together Invincible", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "1367559768746758194", category: "other", description: @@ -985,32 +1061,28 @@ const challenges: Record> = { }, footBarbarian: { display: "Foot Barbarian", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "1025814170962231336", category: "other", description: "Complete a two-hour test using your feet.", }, bigFoot: { display: "Big Foot", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "1030531753082900610", category: "other", description: "Complete a three-hour test using your feet.", }, woodPecker: { display: "Wood Pecker", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "753724531666845830", category: "other", description: "Complete a 200-word test using only your nose.", }, mrWorldwide: { display: "Mr Worldwide", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "762345904279519292", category: "other", description: @@ -1018,8 +1090,7 @@ const challenges: Record> = { }, internalMetronome: { display: "Internal Metronome", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "934067904884916234", category: "other", description: @@ -1027,56 +1098,49 @@ const challenges: Record> = { }, roleCollector: { display: "Role Collector", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "739306809554108520", category: "roleCount", description: "Collect 10 roles.", }, roleEnthusiast: { display: "Role Enthusiast", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "753360663656529931", category: "roleCount", description: "Collect 20 roles.", }, roleAddict: { display: "Role Addict", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "758783172833443850", category: "roleCount", description: "Collect 30 roles.", }, roleOverdose: { display: "Role Overdose", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "758783365930811423", category: "roleCount", description: "Collect 40 roles.", }, roleZombie: { display: "Role Zombie", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "762701731993616405", category: "roleCount", description: "Collect 50 roles.", }, roleOverlord: { display: "Role Overlord", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "805519411502514187", category: "roleCount", description: "Collect 60 roles.", }, roleImp: { display: "Role Imp", - type: "hidden", - parameters: [], + isHidden: true, discordRoleId: "906565521271558214", category: "roleCount", description: "Collect 70 roles.", @@ -1088,7 +1152,7 @@ const map: Record = Object.fromEntries( ) as Record; const list: Challenge[] = Object.values(map); -const regular: Challenge[] = list.filter((it) => it.type !== "hidden"); +const regular: Challenge[] = list.filter((it) => it.isHidden !== true); export function getChallenges(): Challenge[] { return list; diff --git a/packages/schemas/src/challenges.ts b/packages/schemas/src/challenges.ts index 525f3c5a1cea..cc7ad109d6d6 100644 --- a/packages/schemas/src/challenges.ts +++ b/packages/schemas/src/challenges.ts @@ -1,10 +1,4 @@ import { z } from "zod"; -import { FunboxNameSchema, PartialConfigSchema } from "./configs"; - -const MinRequiredNumber = z.object({ min: z.number() }).strict(); -const MaxRequiredNumber = z.object({ max: z.number() }).strict(); -const ExactRequiredNumber = z.object({ exact: z.number() }).strict(); - import { customEnumErrorHandler } from "./util"; export const ChallengeNameSchema = z.enum( @@ -117,63 +111,3 @@ export const ChallengeNameSchema = z.enum( ); export type ChallengeName = z.infer; - -export const ChallengeSchema = z - .object({ - name: ChallengeNameSchema, - display: z.string(), - autoRole: z.boolean().optional(), - discordRoleId: z.string(), - type: z.enum([ - "customTime", - "customWords", - "customText", - "script", - "accuracy", - "funbox", - "other", - "hidden", - ]), - message: z.string().optional(), - parameters: z.array( - z - .string() - .or(z.null()) - .or(z.number()) - .or(z.boolean()) - .or(z.array(FunboxNameSchema)), - ), - requirements: z - .object({ - wpm: ExactRequiredNumber.or(MinRequiredNumber), - acc: ExactRequiredNumber.or(MinRequiredNumber), - afk: MaxRequiredNumber, - time: MinRequiredNumber, - funbox: z - .object({ - exact: z.array(FunboxNameSchema), - }) - .partial(), - raw: ExactRequiredNumber, - con: ExactRequiredNumber, - config: PartialConfigSchema, - }) - .partial() - .strict() - .optional(), - - category: z.enum([ - "other", - "endurance", - "script", - "speed", - "accuracy", - "funbox", - "champions", - "roleCount", - ]), - description: z.string(), - }) - .strict(); - -export type Challenge = z.infer; From 3168359609866f71cec57e7aea6714b440ae4200 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 25 Jun 2026 10:19:20 +0200 Subject: [PATCH 10/12] types for parameters, typos --- .../ts/controllers/challenge-controller.ts | 45 ++- packages/challenges/src/index.ts | 321 +++++++++++------- 2 files changed, 219 insertions(+), 147 deletions(-) diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 915251bfa352..f963351068ab 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -13,10 +13,7 @@ import * as TestState from "../test/test-state"; import { ChallengeSettings, getChallenge } from "@monkeytype/challenges"; import { ChallengeName } from "@monkeytype/schemas/challenges"; -import { Difficulty, FunboxName, ThemeName } from "@monkeytype/schemas/configs"; import { CompletedEvent } from "@monkeytype/schemas/results"; -import { Mode } from "@monkeytype/schemas/shared"; -import { CustomTextLimitMode, CustomTextMode } from "@monkeytype/schemas/util"; import { typedKeys } from "@monkeytype/util/objects"; import { hideLoaderBar, showLoaderBar } from "../states/loader-bar"; import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; @@ -220,7 +217,7 @@ export async function setup(challengeName: ChallengeName): Promise { return false; } if (settings.type === "customTime") { - setConfig("time", settings.parameters[0] as number, { + setConfig("time", settings.parameters.time, { nosave: true, }); setConfig("mode", "time", { @@ -241,7 +238,7 @@ export async function setup(challengeName: ChallengeName): Promise { }); } } else if (settings.type === "customWords") { - setConfig("words", settings.parameters[0] as number, { + setConfig("words", settings.parameters.words, { nosave: true, }); setConfig("mode", "words", { @@ -251,11 +248,11 @@ export async function setup(challengeName: ChallengeName): Promise { nosave: true, }); } else if (settings.type === "customText") { - CustomText.setText((settings.parameters[0] as string).split(" ")); - CustomText.setMode(settings.parameters[1] as CustomTextMode); - CustomText.setLimitValue(settings.parameters[2] as number); - CustomText.setLimitMode(settings.parameters[3] as CustomTextLimitMode); - CustomText.setPipeDelimiter(settings.parameters[4] as boolean); + CustomText.setText(settings.parameters.text.split(" ")); + CustomText.setMode(settings.parameters.mode); + CustomText.setLimitValue(settings.parameters.limit); + CustomText.setLimitMode(settings.parameters.limitMode); + CustomText.setPipeDelimiter(settings.parameters.isPipeDelimiter); setConfig("mode", "custom", { nosave: true, }); @@ -264,9 +261,7 @@ export async function setup(challengeName: ChallengeName): Promise { }); } else if (settings.type === "script") { showLoaderBar(); - const response = await fetch( - `/challenges/${settings.parameters[0] as string}`, - ); + const response = await fetch(`/challenges/${settings.parameters.script}`); hideLoaderBar(); if (response.status !== 200) { throw new Error(`${response.status} ${response.statusText}`); @@ -285,11 +280,11 @@ export async function setup(challengeName: ChallengeName): Promise { setConfig("difficulty", "normal", { nosave: true, }); - if (settings.parameters[1] !== null) { - setConfig("theme", settings.parameters[1] as ThemeName); + if (settings.parameters.theme !== undefined) { + setConfig("theme", settings.parameters.theme); } - if (settings.parameters[2] !== null) { - void Funbox.activate(settings.parameters[2] as FunboxName[]); + if (settings.parameters.funboxes !== undefined) { + void Funbox.activate(settings.parameters.funboxes); } } else if (settings.type === "accuracy") { setConfig("time", 0, { @@ -305,26 +300,26 @@ export async function setup(challengeName: ChallengeName): Promise { setConfig("difficulty", "normal", { nosave: true, }); - if (settings.parameters[1] === "words") { - setConfig("words", settings.parameters[2] as number, { + if (settings.parameters.mode === "words") { + setConfig("words", settings.parameters.mode2, { nosave: true, }); - } else if (settings.parameters[1] === "time") { - setConfig("time", settings.parameters[2] as number, { + } else if (settings.parameters.mode === "time") { + setConfig("time", settings.parameters.mode2, { nosave: true, }); } - setConfig("mode", settings.parameters[1] as Mode, { + setConfig("mode", settings.parameters.mode, { nosave: true, }); - if (settings.parameters[3] !== undefined) { - setConfig("difficulty", settings.parameters[3] as Difficulty, { + if (settings.parameters.difficulty !== undefined) { + setConfig("difficulty", settings.parameters.difficulty, { nosave: true, }); } if ( - !setConfig("funbox", settings.parameters[0] as FunboxName[], { + !setConfig("funbox", [settings.parameters.funbox], { nosave: true, }) ) { diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index 44adb33c24f3..2c05f2cd18de 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -1,29 +1,12 @@ import { ChallengeName } from "@monkeytype/schemas/challenges"; -import { Config, FunboxName } from "@monkeytype/schemas/configs"; - -export type ChallengeSettings = { - autoRole?: boolean; - type: - | "customTime" - | "customWords" - | "customText" - | "script" - | "accuracy" - | "funbox" - | "other"; - message?: string; - requirements?: { - wpm?: { min: number } | { exact: number }; - acc?: { min: number } | { exact: number }; - raw?: { exact: number }; - con?: { exact: number }; - afk?: { max: number }; - time?: { min: number }; - funbox?: { exact: FunboxName[] }; - config?: Partial; - }; - parameters: (string | null | number | boolean | FunboxName[])[]; -}; +import { + Config, + Difficulty, + FunboxName, + ThemeName, +} from "@monkeytype/schemas/configs"; +import { Mode } from "@monkeytype/schemas/shared"; +import { CustomTextLimitMode, CustomTextMode } from "@monkeytype/schemas/util"; export type Challenge = { name: ChallengeName; @@ -43,6 +26,58 @@ export type Challenge = { settings?: ChallengeSettings; }; +type ChallengeParameter = + | { + type: "customTime"; + parameters: { time: number }; + } + | { type: "customWords"; parameters: { words: number } } + | { + type: "customText"; + parameters: { + text: string; + mode: CustomTextMode; + limit: number; + limitMode: CustomTextLimitMode; + isPipeDelimiter: boolean; + }; + } + | { + type: "script"; + parameters: { + script: string; + theme?: ThemeName; + funboxes?: FunboxName[]; + }; + } + | { type: "accuracy" } + | { + type: "funbox"; + parameters: { + funbox: FunboxName; + difficulty?: Difficulty; + } & ( + | { mode: "time" | "words"; mode2: number } + | { mode: Exclude } + ); + } + | { type: "other" }; + +export type ChallengeSettings = { + autoRole?: boolean; + message?: string; + requirements?: { + wpm?: { min: number } | { exact: number }; + acc?: { min: number } | { exact: number }; + raw?: { exact: number }; + con?: { exact: number }; + afk?: { max: number }; + time?: { min: number }; + funbox?: { exact: FunboxName[] }; + config?: Partial; + }; +} & ChallengeParameter; + const challenges: Record> = { "69": { display: "6969696969", @@ -56,7 +91,7 @@ const challenges: Record> = { type: "customTime", message: "You need to achieve 69 wpm, 69 raw, 69% accuracy and 69% consistency.", - parameters: [69], + parameters: { time: 69 }, requirements: { wpm: { exact: 69 }, raw: { exact: 69 }, @@ -98,11 +133,11 @@ const challenges: Record> = { isHidden: true, discordRoleId: "728371749737201855", category: "endurance", - description: "Complete an one-hour test.", + description: "Complete a one-hour test.", settings: { autoRole: true, type: "customTime", - parameters: [3600], + parameters: { time: 3600 }, requirements: { time: { min: 3600 } }, }, }, @@ -115,7 +150,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "customTime", - parameters: [7200], + parameters: { time: 7200 }, requirements: { time: { min: 7200 } }, }, }, @@ -128,7 +163,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "customTime", - parameters: [10800], + parameters: { time: 10800 }, requirements: { time: { min: 10800 } }, }, }, @@ -141,7 +176,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "customTime", - parameters: [14400], + parameters: { time: 14400 }, requirements: { time: { min: 14400 } }, }, }, @@ -153,7 +188,7 @@ const challenges: Record> = { description: "Complete an eight-hour test.", settings: { type: "customTime", - parameters: [28800], + parameters: { time: 28800 }, requirements: { time: { min: 28800 } }, }, }, @@ -165,7 +200,7 @@ const challenges: Record> = { description: "Complete a twelve-hour test.", settings: { type: "customTime", - parameters: [43200], + parameters: { time: 43200 }, requirements: { time: { min: 43200 } }, }, }, @@ -177,7 +212,7 @@ const challenges: Record> = { description: "Complete a twenty-four-hour test.", settings: { type: "customTime", - parameters: [86400], + parameters: { time: 86400 }, requirements: { time: { min: 86400 } }, }, }, @@ -190,7 +225,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: ["miodec", "repeat", 10000, "word", false], + parameters: { + text: "miodec", + mode: "repeat", + limit: 10000, + limitMode: "word", + isPipeDelimiter: false, + }, }, }, bigramSalad: { @@ -203,13 +244,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: [ - "to of in it is as at be we he so on an or do if up by my go", - "random", - 100, - "word", - false, - ], + parameters: { + text: "to of in it is as at be we he so on an or do if up by my go", + mode: "random", + limit: 100, + limitMode: "word", + isPipeDelimiter: false, + }, requirements: { wpm: { min: 100 } }, }, }, @@ -222,7 +263,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: ["miodec", "repeat", 1000, "word", false], + parameters: { + text: "miodec", + mode: "repeat", + limit: 1000, + limitMode: "word", + isPipeDelimiter: false, + }, }, }, simpLord: { @@ -234,7 +281,13 @@ const challenges: Record> = { settings: { autoRole: false, type: "customText", - parameters: ["miodec", "repeat", 100000, "word", false], + parameters: { + text: "miodec", + mode: "repeat", + limit: 100000, + limitMode: "word", + isPipeDelimiter: false, + }, }, }, antidiseWhat: { @@ -246,7 +299,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: ["antidisestablishmentarianism", "repeat", 1, "word", false], + parameters: { + text: "antidisestablishmentarianism", + mode: "repeat", + limit: 1, + limitMode: "word", + isPipeDelimiter: false, + }, requirements: { wpm: { min: 200 } }, }, }, @@ -259,7 +318,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: ["monkeytype", "repeat", 1000, "word", false], + parameters: { + text: "monkeytype", + mode: "repeat", + limit: 1000, + limitMode: "word", + isPipeDelimiter: false, + }, }, }, developd: { @@ -271,7 +336,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: ["develop", "repeat", 1000, "word", false], + parameters: { + text: "develop", + mode: "repeat", + limit: 1000, + limitMode: "word", + isPipeDelimiter: false, + }, }, }, slowAndSteady: { @@ -284,7 +355,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "customTime", - parameters: [300], + parameters: { time: 300 }, requirements: { wpm: { exact: 60 }, config: { liveSpeedStyle: "off", paceCaret: "off" }, @@ -301,13 +372,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: [ - "a b c d e f g h i j k l m n o p q r s t u v w x y z", - "random", - 100, - "word", - false, - ], + parameters: { + text: "a b c d e f g h i j k l m n o p q r s t u v w x y z", + mode: "random", + limit: 100, + limitMode: "word", + isPipeDelimiter: false, + }, requirements: { wpm: { min: 100 } }, }, }, @@ -320,7 +391,13 @@ const challenges: Record> = { settings: { autoRole: true, type: "customText", - parameters: ["power", "repeat", 10, "word", false], + parameters: { + text: "power", + mode: "repeat", + limit: 10, + limitMode: "word", + isPipeDelimiter: false, + }, requirements: { wpm: { min: 400 } }, }, }, @@ -334,7 +411,6 @@ const challenges: Record> = { autoRole: true, type: "accuracy", message: "Minimum 60wpm and 100% accuracy required.", - parameters: [], requirements: { wpm: { min: 60 }, acc: { exact: 100 }, @@ -353,7 +429,6 @@ const challenges: Record> = { autoRole: true, type: "accuracy", message: "Minimum 60wpm and 100% accuracy required.", - parameters: [], requirements: { wpm: { min: 60 }, acc: { exact: 100 }, @@ -372,7 +447,6 @@ const challenges: Record> = { autoRole: true, type: "accuracy", message: "Minimum 60wpm and 100% accuracy required.", - parameters: [], requirements: { wpm: { min: 60 }, acc: { exact: 100 }, @@ -390,7 +464,7 @@ const challenges: Record> = { "Type out the entire Star Wars Episode 4 script with punctuation while watching the movie simultaneously.", settings: { type: "script", - parameters: ["episode4.txt", null, ["space_balls"]], + parameters: { script: "episode4.txt", funboxes: ["space_balls"] }, requirements: { config: { tapeMode: "off" } }, }, }, @@ -404,7 +478,7 @@ const challenges: Record> = { settings: { type: "script", message: "Mininum 45 WPM and 100% accuracy required.", - parameters: ["beepboop.txt", null, ["nospace"]], + parameters: { script: "beepboop.txt", funboxes: ["nospace"] }, requirements: { wpm: { min: 45 }, acc: { min: 100 }, @@ -421,7 +495,7 @@ const challenges: Record> = { "Type out the entire Star Wars Episode 5 script with punctuation while watching the movie simultaneously.", settings: { type: "script", - parameters: ["episode5.txt", null, ["space_balls"]], + parameters: { script: "episode5.txt", funboxes: ["space_balls"] }, requirements: { config: { tapeMode: "off" } }, }, }, @@ -434,7 +508,7 @@ const challenges: Record> = { "Type out the entire Star Wars Episode 6 script with punctuation while watching the movie simultaneously.", settings: { type: "script", - parameters: ["episode6.txt", null, ["space_balls"]], + parameters: { script: "episode6.txt", funboxes: ["space_balls"] }, requirements: { config: { tapeMode: "off" } }, }, }, @@ -448,7 +522,7 @@ const challenges: Record> = { autoRole: true, type: "script", message: "Minimum 70wpm required.", - parameters: ["jolly.txt", null, null], + parameters: { script: "jolly.txt" }, requirements: { wpm: { min: 70 } }, }, }, @@ -461,7 +535,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "script", - parameters: ["pokemon.txt", null, null], + parameters: { script: "pokemon.txt" }, }, }, rapGod: { @@ -475,7 +549,7 @@ const challenges: Record> = { autoRole: true, type: "script", message: "Minimum 85wpm and 90% accuracy required.", - parameters: ["rapgod.txt", null, null], + parameters: { script: "rapgod.txt" }, requirements: { wpm: { min: 85 }, acc: { min: 90 }, afk: { max: 5 } }, }, }, @@ -490,7 +564,7 @@ const challenges: Record> = { autoRole: true, type: "script", message: "Minimum 60wpm and 100% accuracy required.", - parameters: ["navyseal.txt", null, null], + parameters: { script: "navyseal.txt" }, requirements: { wpm: { min: 60 }, acc: { exact: 100 }, afk: { max: 5 } }, }, }, @@ -501,7 +575,7 @@ const challenges: Record> = { category: "script", description: "Type out the entire Ratatouille script while watching the movie simultaneously.", - settings: { type: "script", parameters: ["littlechef.txt", null, null] }, + settings: { type: "script", parameters: { script: "littlechef.txt" } }, }, crosstalk: { display: "(CROSSTALK)", @@ -510,7 +584,7 @@ const challenges: Record> = { category: "script", description: "Type out the entire transcript of the first 2020 Presidential Debate.", - settings: { type: "script", parameters: ["crosstalk.txt", null, null] }, + settings: { type: "script", parameters: { script: "crosstalk.txt" } }, }, bees: { display: "Bees!!!", @@ -519,7 +593,7 @@ const challenges: Record> = { category: "script", description: "Type out the entire Bee Movie script while watching the movie simultaneously.", - settings: { type: "script", parameters: ["bees.txt", null, null] }, + settings: { type: "script", parameters: { script: "bees.txt" } }, }, getOffMySwamp: { display: "Get Off My Swamp", @@ -528,7 +602,7 @@ const challenges: Record> = { category: "script", description: "Type out the entire Shrek script with punctuation while watching the movie simultaneously.", - settings: { type: "script", parameters: ["shrek.txt", null, null] }, + settings: { type: "script", parameters: { script: "shrek.txt" } }, }, fiftyShadesOfHell: { display: "50 Shades of Hell", @@ -536,7 +610,7 @@ const challenges: Record> = { discordRoleId: "751802155119280128", category: "script", description: "Type out your favourite chapter from 50 Shades of Gray.", - settings: { type: "script", parameters: [] }, + settings: { type: "other" }, }, lookAtMeIAmTheDeveloperNow: { display: "Look at me. I am the developer now.", @@ -548,7 +622,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "script", - parameters: ["sourcecode.txt", null, null], + parameters: { script: "sourcecode.txt" }, }, }, beLikeWater: { @@ -561,7 +635,7 @@ const challenges: Record> = { settings: { type: "funbox", message: "Remember: You need to achieve at least 50 wpm in each layout.", - parameters: [["layoutfluid"], "time", 60], + parameters: { funbox: "layoutfluid", mode: "time", mode2: 60 }, }, }, rollercoaster: { @@ -570,11 +644,11 @@ const challenges: Record> = { discordRoleId: "736032495526740001", category: "funbox", description: - "Complete at least an one-hour test using the round round baby mode.", + "Complete at least a one-hour test using the round round baby mode.", settings: { autoRole: true, type: "funbox", - parameters: [["round_round_baby"], "time", 3600], + parameters: { funbox: "round_round_baby", mode: "time", mode2: 3600 }, requirements: { time: { min: 3600 }, funbox: { exact: ["round_round_baby"] }, @@ -586,11 +660,11 @@ const challenges: Record> = { isHidden: true, discordRoleId: "737385182998429757", category: "funbox", - description: "Complete at least an one-hour test using the mirror mode.", + description: "Complete at least a one-hour test using the mirror mode.", settings: { autoRole: true, type: "funbox", - parameters: [["mirror"], "time", 3600], + parameters: { funbox: "mirror", mode: "time", mode2: 3600 }, requirements: { time: { min: 3600 }, funbox: { exact: ["mirror"] } }, }, }, @@ -599,11 +673,11 @@ const challenges: Record> = { isHidden: true, discordRoleId: "739306439574683710", category: "funbox", - description: "Complete at least an one-hour test using choo choomode.", + description: "Complete at least a one-hour test using choo choo mode.", settings: { autoRole: true, type: "funbox", - parameters: [["choo_choo"], "time", 3600], + parameters: { funbox: "choo_choo", mode: "time", mode2: 3600 }, requirements: { time: { min: 3600 }, funbox: { exact: ["choo_choo"] } }, }, }, @@ -616,7 +690,12 @@ const challenges: Record> = { "Achieve 100+ WPM with 100% accuracy on a 25-word test using the memory funbox.", settings: { type: "funbox", - parameters: [["memory"], "words", 25, "master"], + parameters: { + funbox: "memory", + mode: "words", + mode2: 25, + difficulty: "master", + }, requirements: { config: { tapeMode: "off" } }, }, }, @@ -626,11 +705,11 @@ const challenges: Record> = { discordRoleId: "740730587429601291", category: "funbox", description: - "Complete at least an one-hour test using the earthquake funbox mode.", + "Complete at least a one-hour test using the earthquake funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["earthquake"], "time", 3600], + parameters: { funbox: "earthquake", mode: "time", mode2: 3600 }, requirements: { time: { min: 3600 }, funbox: { exact: ["earthquake"] } }, }, }, @@ -640,11 +719,11 @@ const challenges: Record> = { discordRoleId: "742128871825997914", category: "funbox", description: - "Complete at least an one-hour test using the simon says funbox mode.", + "Complete at least a one-hour test using the simon says funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["simon_says"], "time", 3600], + parameters: { funbox: "simon_says", mode: "time", mode2: 3600 }, requirements: { time: { min: 3600 }, funbox: { exact: ["simon_says"] } }, }, }, @@ -654,11 +733,11 @@ const challenges: Record> = { discordRoleId: "743962178821816391", category: "funbox", description: - "Complete at least an one-hour test using the 58008 funbox mode.", + "Complete at least a one-hour test using the 58008 funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["58008"], "time", 3600], + parameters: { funbox: "58008", mode: "time", mode2: 3600 }, requirements: { time: { min: 3600 }, funbox: { exact: ["58008"] } }, }, }, @@ -672,7 +751,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "funbox", - parameters: [["read_ahead"], "time", 60], + parameters: { funbox: "read_ahead", mode: "time", mode2: 60 }, requirements: { wpm: { min: 100 }, time: { min: 60 }, @@ -691,7 +770,7 @@ const challenges: Record> = { settings: { autoRole: true, type: "funbox", - parameters: [["read_ahead_hard"], "time", 60], + parameters: { funbox: "read_ahead_hard", mode: "time", mode2: 60 }, requirements: { wpm: { min: 100 }, time: { min: 60 }, @@ -706,11 +785,11 @@ const challenges: Record> = { discordRoleId: "744209241396740176", category: "funbox", description: - "Complete at least an one-hour test using the gibberish funbox mode.", + "Complete at least a one-hour test using the gibberish funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["gibberish"], "time", 3600], + parameters: { funbox: "gibberish", mode: "time", mode2: 3600 }, requirements: { time: { min: 60 }, funbox: { exact: ["gibberish"] } }, }, }, @@ -720,11 +799,11 @@ const challenges: Record> = { discordRoleId: "744209452714033162", category: "funbox", description: - "Complete at least an one-hour test using the specials funbox mode.", + "Complete at least a one-hour test using the specials funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["specials"], "time", 3600], + parameters: { funbox: "specials", mode: "time", mode2: 3600 }, requirements: { time: { min: 60 }, funbox: { exact: ["specials"] } }, }, }, @@ -733,12 +812,11 @@ const challenges: Record> = { isHidden: true, discordRoleId: "744318102766092362", category: "funbox", - description: - "Complete at least an one-hour test using the tts funbox mode.", + description: "Complete at least a one-hour test using the tts funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["tts"], "time", 3600], + parameters: { funbox: "tts", mode: "time", mode2: 3600 }, requirements: { time: { min: 60 }, funbox: { exact: ["tts"] } }, }, }, @@ -748,11 +826,11 @@ const challenges: Record> = { discordRoleId: "746142791326760980", category: "funbox", description: - "Complete at least an one-hour test using the ascii funbox mode.", + "Complete at least a one-hour test using the ascii funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["ascii"], "time", 3600], + parameters: { funbox: "ascii", mode: "time", mode2: 3600 }, requirements: { time: { min: 60 }, funbox: { exact: ["ascii"] } }, }, }, @@ -762,11 +840,11 @@ const challenges: Record> = { discordRoleId: "760999194525171724", category: "funbox", description: - "Complete at least an one-hour test using the randomcase funbox mode.", + "Complete at least a one-hour test using the randomcase funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["sPoNgEcAsE"], "time", 3600], + parameters: { funbox: "sPoNgEcAsE", mode: "time", mode2: 3600 }, requirements: { time: { min: 60 }, funbox: { exact: ["sPoNgEcAsE"] } }, }, }, @@ -776,11 +854,11 @@ const challenges: Record> = { discordRoleId: "760930262740631633", category: "funbox", description: - "Complete at least an one-hour test using the nausea funbox mode.", + "Complete at least a one-hour test using the nausea funbox mode.", settings: { autoRole: true, type: "funbox", - parameters: [["nausea"], "time", 3600], + parameters: { funbox: "nausea", mode: "time", mode2: 3600 }, requirements: { time: { min: 60 }, funbox: { exact: ["nausea"] } }, }, }, @@ -789,8 +867,8 @@ const challenges: Record> = { isHidden: true, discordRoleId: "761794585109200906", category: "other", - description: "Complete an one-hour test using only your thumbs.", - settings: { type: "customTime", parameters: [3600] }, + description: "Complete a one-hour test using only your thumbs.", + settings: { type: "customTime", parameters: { time: 3600 } }, }, mouseWarrior: { display: "Mouse warrior", @@ -798,16 +876,16 @@ const challenges: Record> = { discordRoleId: "744580294442614790", category: "other", description: - "Complete an one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", - settings: { type: "customTime", parameters: [3600] }, + "Complete a one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", + settings: { type: "customTime", parameters: { time: 3600 } }, }, mobileWarrior: { display: "Mobile warrior", isHidden: true, discordRoleId: "744723801526370407", category: "other", - description: "Complete an one-hour test on mobile.", - settings: { type: "customTime", parameters: [3600] }, + description: "Complete a one-hour test on mobile.", + settings: { type: "customTime", parameters: { time: 3600 } }, }, upsideDown: { display: "uʍop ǝpᴉsdn", @@ -815,8 +893,8 @@ const challenges: Record> = { discordRoleId: "782725716114014237", category: "other", description: - "Achieve at least 60 WPM on an one-minute test with your keyboard upside down.", - settings: { type: "customTime", parameters: [60] }, + "Achieve at least 60 WPM on a one-minute test with your keyboard upside down.", + settings: { type: "customTime", parameters: { time: 60 } }, }, oneArmedBandit: { display: "One armed bandit", @@ -824,8 +902,8 @@ const challenges: Record> = { discordRoleId: "765919192557682708", category: "other", description: - "Complete an one-hour or 10k words test (whichever comes sooner, using an external timer) using an one-handed words list (either left or right) for your layout.", - settings: { type: "customWords", parameters: [10000] }, + "Complete a one-hour or 10k words test (whichever comes sooner, using an external timer) using a one-handed words list (either left or right) for your layout.", + settings: { type: "customWords", parameters: { words: 10000 } }, }, englishMaster: { display: "English master", @@ -833,11 +911,11 @@ const challenges: Record> = { discordRoleId: "751166528824672396", category: "other", description: - "Complete an one-hour test using English 10k language with punctuation and numbers enabled.", + "Complete a one-hour test using English 10k language with punctuation and numbers enabled.", settings: { autoRole: true, type: "customTime", - parameters: [3600], + parameters: { time: 3600 }, requirements: { time: { min: 3600 }, config: { language: "english_10k", punctuation: true, numbers: true }, @@ -849,8 +927,8 @@ const challenges: Record> = { isHidden: true, discordRoleId: "751953592860147822", category: "other", - description: "Complete an one-hour test using your feet. Don't ask me why.", - settings: { type: "customTime", parameters: [3600] }, + description: "Complete a one-hour test using your feet. Don't ask me why.", + settings: { type: "customTime", parameters: { time: 3600 } }, }, wingdings: { display: "Ten Words of Pain", @@ -863,7 +941,6 @@ const challenges: Record> = { type: "other", message: "Complete a 10-word Master mode test using the Wingdings custom font. No keymap allowed. Minimum 60 WPM and 100% accuracy required.", - parameters: [], requirements: { acc: { exact: 100 } }, }, }, @@ -893,7 +970,7 @@ const challenges: Record> = { isHidden: true, discordRoleId: "728650773503934464", category: "champions", - description: "Achieve the highest WPM in an one-hour test.", + description: "Achieve the highest WPM in a one-hour test.", }, fluidChampion: { display: "Fluid Champion", @@ -979,7 +1056,7 @@ const challenges: Record> = { isHidden: true, discordRoleId: "878715678830510111", category: "speed", - description: "Achieve 200+ WPM on an one-hour test.", + description: "Achieve 200+ WPM on a one-hour test.", }, flawless: { display: "Flawless", @@ -1017,7 +1094,7 @@ const challenges: Record> = { isHidden: true, discordRoleId: "910078947302191114", category: "other", - description: "Complete an one-hour test using tape mode and letter mode.", + description: "Complete a one-hour test using tape mode and letter mode.", }, stickman: { display: "stickman", @@ -1025,7 +1102,7 @@ const challenges: Record> = { discordRoleId: "788107449151651890", category: "other", description: - "Complete an one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", + "Complete a one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", }, waveDynamics: { display: "Wave Dynamics", @@ -1041,7 +1118,7 @@ const challenges: Record> = { discordRoleId: "863193901153779713", category: "other", description: - "Complete an one-hour test in a Tribe lobby with at least 10 players.", + "Complete a one-hour test in a Tribe lobby with at least 10 players.", }, apesTogetherStronger: { display: "Apes Together Stronger", From 9252e0f7b5d39b608f4aec04540460dcd2f79c4d Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 25 Jun 2026 10:51:39 +0200 Subject: [PATCH 11/12] remove all non-refactoring --- .../__integration__/dal/user.spec.ts | 13 +- .../__tests__/api/controllers/user.spec.ts | 41 +-- backend/src/api/controllers/user.ts | 42 +-- backend/src/dal/user.ts | 7 +- backend/src/utils/discord.ts | 63 +--- .../ts/components/modals/EditProfileModal.tsx | 17 +- .../components/pages/profile/Challenges.tsx | 123 ------- .../components/pages/profile/UserDetails.tsx | 20 +- .../components/pages/profile/UserProfile.tsx | 6 - frontend/src/ts/controllers/url-handler.tsx | 3 +- frontend/src/ts/db.ts | 1 - frontend/src/ts/pages/account-settings.ts | 21 +- packages/challenges/src/index.ts | 316 +----------------- packages/contracts/src/users.ts | 10 - packages/schemas/src/challenges.ts | 45 +-- packages/schemas/src/users.ts | 12 - 16 files changed, 39 insertions(+), 701 deletions(-) delete mode 100644 frontend/src/ts/components/pages/profile/Challenges.tsx diff --git a/backend/__tests__/__integration__/dal/user.spec.ts b/backend/__tests__/__integration__/dal/user.spec.ts index 1126650fefef..5f44cffe1d41 100644 --- a/backend/__tests__/__integration__/dal/user.spec.ts +++ b/backend/__tests__/__integration__/dal/user.spec.ts @@ -1271,7 +1271,7 @@ describe("UserDal", () => { describe("linkDiscord", () => { it("throws for nonexisting user", async () => { await expect(async () => - UserDAL.linkDiscord("unknown", "", "", {}), + UserDAL.linkDiscord("unknown", "", ""), ).rejects.toThrow("User not found\nStack: link discord"); }); it("should update", async () => { @@ -1279,18 +1279,14 @@ describe("UserDal", () => { const { uid } = await UserTestData.createUser({ discordId: "discordId", discordAvatar: "discordAvatar", - challenges: { - "100hours": {}, - }, }); //when - await UserDAL.linkDiscord(uid, "newId", "newAvatar", { "250hours": {} }); + await UserDAL.linkDiscord(uid, "newId", "newAvatar"); //then const read = await UserDAL.getUser(uid, "read"); expect(read.discordId).toEqual("newId"); expect(read.discordAvatar).toEqual("newAvatar"); - expect(read.challenges).toEqual({ "250hours": {} }); }); it("should update without avatar", async () => { //given @@ -1319,10 +1315,6 @@ describe("UserDal", () => { const { uid } = await UserTestData.createUser({ discordId: "discordId", discordAvatar: "discordAvatar", - challenges: { - "100hours": {}, - "250hours": { addedAt: Date.now() }, - }, }); //when @@ -1332,7 +1324,6 @@ describe("UserDal", () => { const read = await UserDAL.getUser(uid, "read"); expect(read.discordId).toBeUndefined(); expect(read.discordAvatar).toBeUndefined(); - expect(read.challenges).toBeUndefined(); }); }); describe("updateInbox", () => { diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index baf7a0e52feb..867f050cbfa2 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -37,7 +37,6 @@ import * as WeeklyXpLeaderboard from "../../../src/services/weekly-xp-leaderboar import * as ConnectionsDal from "../../../src/dal/connections"; import { pb } from "../../__testData__/users"; import Test from "supertest/lib/test"; -import { getChallenge } from "@monkeytype/challenges"; const { mockApp, uid, mockAuth } = setup(); const configuration = Configuration.getCachedConfiguration(); @@ -1553,7 +1552,7 @@ describe("user controller test", () => { it("should get oauth link", async () => { //WHEN const { body } = await mockApp - .get("/users/discord/oauth?includeRoles=true") + .get("/users/discord/oauth") .set("Authorization", `Bearer ${uid}`) .expect(200); @@ -1562,9 +1561,7 @@ describe("user controller test", () => { message: "Discord oauth link generated", data: { url }, }); - expect(getOauthLinkMock).toHaveBeenCalledWith(uid, { - includeRoles: true, - }); + expect(getOauthLinkMock).toHaveBeenCalledWith(uid); }); it("should fail if feature is not enabled", async () => { //GIVEN @@ -1590,7 +1587,6 @@ describe("user controller test", () => { "iStateValidForUser", ); const getDiscordUserMock = vi.spyOn(DiscordUtils, "getDiscordUser"); - const getDiscordRoleIdsMock = vi.spyOn(DiscordUtils, "getDiscordRoleIds"); const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains"); const userLinkDiscordMock = vi.spyOn(UserDal, "linkDiscord"); const georgeLinkDiscordMock = vi.spyOn(GeorgeQueue, "linkDiscord"); @@ -1603,9 +1599,6 @@ describe("user controller test", () => { id: "discordUserId", avatar: "discordUserAvatar", }); - getDiscordRoleIdsMock.mockResolvedValue([ - getChallenge("100hours").discordRoleId, - ]); isDiscordIdAvailableMock.mockResolvedValue(true); blocklistContainsMock.mockResolvedValue(false); userLinkDiscordMock.mockResolvedValue(); @@ -1617,7 +1610,6 @@ describe("user controller test", () => { isStateValidForUserMock, isDiscordIdAvailableMock, getDiscordUserMock, - getDiscordRoleIdsMock, blocklistContainsMock, userLinkDiscordMock, georgeLinkDiscordMock, @@ -1637,7 +1629,6 @@ describe("user controller test", () => { tokenType: "tokenType", accessToken: "accessToken", state: "statestatestatestate", - scope: ["scopeOne", "scopeTwo"], }) .expect(200); @@ -1662,11 +1653,6 @@ describe("user controller test", () => { "tokenType", "accessToken", ); - expect(getDiscordRoleIdsMock).toHaveBeenCalledWith( - "tokenType", - "accessToken", - ["scopeOne", "scopeTwo"], - ); expect(isDiscordIdAvailableMock).toHaveBeenCalledWith("discordUserId"); expect(blocklistContainsMock).toHaveBeenCalledWith({ discordId: "discordUserId", @@ -1675,9 +1661,6 @@ describe("user controller test", () => { uid, "discordUserId", "discordUserAvatar", - { - "100hours": {}, - }, ); expect(georgeLinkDiscordMock).toHaveBeenCalledWith( "discordUserId", @@ -2979,9 +2962,6 @@ describe("user controller test", () => { testActivity: { "2024": fillYearWithDay(94), }, - challenges: { - "100hours": {}, - }, }; beforeEach(async () => { @@ -3053,15 +3033,12 @@ describe("user controller test", () => { expect(getUserByNameMock).toHaveBeenCalledWith("bob", "get user profile"); expect(getUserMock).not.toHaveBeenCalled(); }); - it("should get testActivity/challenges if enabled", async () => { + it("should get testActivity if enabled", async () => { //GIVEN vi.useFakeTimers().setSystemTime(1712102400000); getUserByNameMock.mockResolvedValue({ ...foundUser, - profileDetails: { - showActivityOnPublicProfile: true, - showChallengesOnPublicProfile: true, - }, + profileDetails: { showActivityOnPublicProfile: true }, } as any); const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry; leaderboardGetRankMock.mockResolvedValue(rank); @@ -3077,18 +3054,13 @@ describe("user controller test", () => { testsByDays: expect.arrayContaining([]), }), ); - - expect(body.data.challenges).toEqual({ "100hours": {} }); }); it("should not get testActivity if disabled", async () => { //GIVEN vi.useFakeTimers().setSystemTime(1712102400000); getUserByNameMock.mockResolvedValue({ ...foundUser, - profileDetails: { - showActivityOnPublicProfile: false, - showChallengesOnPublicProfile: false, - }, + profileDetails: { showActivityOnPublicProfile: false }, } as any); const rank = { rank: 24 } as LeaderboardDal.DBLeaderboardEntry; leaderboardGetRankMock.mockResolvedValue(rank); @@ -3099,7 +3071,6 @@ describe("user controller test", () => { //THEN expect(body.data.testActivity).toBeUndefined(); - expect(body.data.challenges).toBeUndefined(); }); it("should get base profile for banned user", async () => { @@ -3217,7 +3188,6 @@ describe("user controller test", () => { website: "https://monkeytype.com", }, showActivityOnPublicProfile: false, - showChallengesOnPublicProfile: false, }; //WHEN @@ -3246,7 +3216,6 @@ describe("user controller test", () => { website: "https://monkeytype.com", }, showActivityOnPublicProfile: false, - showChallengesOnPublicProfile: false, }, { badges: [{ id: 4 }, { id: 2, selected: true }, { id: 3 }], diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 643423431976..b89a7874515f 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -39,7 +39,6 @@ import { CountByYearAndDay, TestActivity, UserProfileDetails, - UserChallenges, } from "@monkeytype/schemas/users"; import { addImportantLog, addLog, deleteUserLogs } from "../../dal/logs"; import { sendForgotPasswordEmail as authSendForgotPasswordEmail } from "../../utils/auth"; @@ -60,7 +59,6 @@ import { ForgotPasswordEmailRequest, GetCurrentTestActivityResponse, GetCustomThemesResponse, - GetDiscordOauthLinkQuery, GetDiscordOauthLinkResponse, GetFavoriteQuotesResponse, GetFriendsResponse, @@ -96,15 +94,6 @@ import { tryCatch } from "@monkeytype/util/trycatch"; import * as ConnectionsDal from "../../dal/connections"; import { PersonalBest } from "@monkeytype/schemas/shared"; -import { ChallengeName } from "@monkeytype/schemas/challenges"; -import { getChallenges } from "@monkeytype/challenges"; - -const challengeNameByRoleId: Record = Object.fromEntries( - getChallenges() - .filter((it) => it.discordRoleId !== undefined) - .map((it) => [it.discordRoleId, it.name]), -); - async function verifyCaptcha(captcha: string): Promise { const { data: verified, error } = await tryCatch(verify(captcha)); if (error) { @@ -640,13 +629,12 @@ export async function getUser(req: MonkeyRequest): Promise { } export async function getOauthLink( - req: MonkeyRequest, + req: MonkeyRequest, ): Promise { const { uid } = req.ctx.decodedToken; - const { includeRoles } = req.query; //build the url - const url = await DiscordUtils.getOauthLink(uid, { includeRoles }); + const url = await DiscordUtils.getOauthLink(uid); //return return new MonkeyResponse("Discord oauth link generated", { @@ -658,7 +646,7 @@ export async function linkDiscord( req: MonkeyRequest, ): Promise { const { uid } = req.ctx.decodedToken; - const { tokenType, accessToken, state, scope } = req.body; + const { tokenType, accessToken, state } = req.body; if (!(await DiscordUtils.iStateValidForUser(state, uid))) { throw new MonkeyError(403, "Invalid user token"); @@ -704,20 +692,7 @@ export async function linkDiscord( throw new MonkeyError(409, "The Discord account is blocked"); } - let roles = await DiscordUtils.getDiscordRoleIds( - tokenType, - accessToken, - scope, - ); - - const challenges: UserChallenges = Object.fromEntries( - roles - .map((roleId) => challengeNameByRoleId[roleId]) - .filter((it) => it !== undefined) - .map((it) => [it, {}]), - ); - - await UserDAL.linkDiscord(uid, discordId, discordAvatar, challenges); + await UserDAL.linkDiscord(uid, discordId, discordAvatar); await GeorgeQueue.linkDiscord(discordId, uid, userInfo.lbOptOut ?? false); void addImportantLog("user_discord_link", `linked to ${discordId}`, uid); @@ -1031,13 +1006,6 @@ export async function getProfile( } else { delete profileData.testActivity; } - - if (user.profileDetails?.showChallengesOnPublicProfile) { - profileData.challenges = user.challenges; - } else { - delete profileData.challenges; - } - return new MonkeyResponse("Profile retrieved", profileData); } @@ -1051,7 +1019,6 @@ export async function updateProfile( socialProfiles, selectedBadgeId, showActivityOnPublicProfile, - showChallengesOnPublicProfile, } = req.body; const user = await UserDAL.getPartialUser(uid, "update user profile", [ @@ -1081,7 +1048,6 @@ export async function updateProfile( ]), ), showActivityOnPublicProfile, - showChallengesOnPublicProfile, }; await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory); diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index a862ad26e50c..ada92f0ee764 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -26,7 +26,6 @@ import { User, CountByYearAndDay, Friend, - UserChallenges, } from "@monkeytype/schemas/users"; import { Mode, @@ -614,15 +613,11 @@ export async function linkDiscord( uid: string, discordId: string, discordAvatar?: string, - challenges?: UserChallenges, ): Promise { const updates: Partial = { discordId }; if (discordAvatar !== undefined && discordAvatar !== null) { updates.discordAvatar = discordAvatar; } - if (challenges !== undefined) { - updates.challenges = challenges; - } await updateUser({ uid }, { $set: updates }, { stack: "link discord" }); } @@ -630,7 +625,7 @@ export async function linkDiscord( export async function unlinkDiscord(uid: string): Promise { await updateUser( { uid }, - { $unset: { discordId: "", discordAvatar: "", challenges: "" } }, + { $unset: { discordId: "", discordAvatar: "" } }, { stack: "unlink discord" }, ); } diff --git a/backend/src/utils/discord.ts b/backend/src/utils/discord.ts index b2533e2db581..e290c02d5f75 100644 --- a/backend/src/utils/discord.ts +++ b/backend/src/utils/discord.ts @@ -6,27 +6,16 @@ import { z } from "zod"; import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; const BASE_URL = "https://discord.com/api"; -const CLIENT_ID = "798272335035498557"; -const SERVER_ID = "713194177403420752"; -const READ_ROLE_SCOPE = "guilds.members.read"; -const DiscordIdAndAvatarSchema = z - .object({ - id: z.string(), - avatar: z - .string() - .optional() - .or(z.null().transform(() => undefined)), - }) - .strip(); +const DiscordIdAndAvatarSchema = z.object({ + id: z.string(), + avatar: z + .string() + .optional() + .or(z.null().transform(() => undefined)), +}); type DiscordIdAndAvatar = z.infer; -const DiscordGuildMemberSchema = z - .object({ - roles: z.array(z.string()), - }) - .strip(); - export async function getDiscordUser( tokenType: string, accessToken: string, @@ -45,51 +34,21 @@ export async function getDiscordUser( return parsed; } -export async function getDiscordRoleIds( - tokenType: string, - accessToken: string, - scope?: string[], -): Promise { - if (!scope?.includes(READ_ROLE_SCOPE)) return []; - - const response = await fetch( - `${BASE_URL}/users/@me/guilds/${SERVER_ID}/member`, - { - headers: { - authorization: `${tokenType} ${accessToken}`, - }, - }, - ); - - const parsed = parseJsonWithSchema( - await response.text(), - DiscordGuildMemberSchema, - ); - - return parsed.roles; -} - -export async function getOauthLink( - uid: string, - options: { includeRoles?: boolean }, -): Promise { +export async function getOauthLink(uid: string): Promise { const connection = RedisClient.getConnection(); if (!connection) { throw new MonkeyError(500, "Redis connection not found"); } const token = randomBytes(10).toString("hex"); - const scope = ["identify"]; - - if (options.includeRoles) scope.push(READ_ROLE_SCOPE); - //add the token uid pair to redis + //add the token uid pair to reids await connection.setex(`discordoauth:${uid}`, 60, token); - return `${BASE_URL}/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${ + return `${BASE_URL}/oauth2/authorize?client_id=798272335035498557&redirect_uri=${ isDevEnvironment() ? `http%3A%2F%2Flocalhost%3A3000%2Fverify` : `https%3A%2F%2Fmonkeytype.com%2Fverify` - }&response_type=token&scope=${scope.join("+")}&state=${token}`; + }&response_type=token&scope=identify&state=${token}`; } export async function iStateValidForUser( diff --git a/frontend/src/ts/components/modals/EditProfileModal.tsx b/frontend/src/ts/components/modals/EditProfileModal.tsx index 4bd2574a0cbf..088f8e5ce2de 100644 --- a/frontend/src/ts/components/modals/EditProfileModal.tsx +++ b/frontend/src/ts/components/modals/EditProfileModal.tsx @@ -40,9 +40,7 @@ export function EditProfile() { twitter: snapshot.details?.socialProfiles?.twitter ?? "", website: snapshot.details?.socialProfiles?.website ?? "", showActivityOnPublicProfile: - snapshot.details?.showActivityOnPublicProfile, - showChallengesOnPublicProfile: - snapshot.details?.showChallengesOnPublicProfile, + snapshot.details?.showActivityOnPublicProfile ?? true, badgeId: badges.find((b) => b.selected)?.id ?? -1, }, onSubmit: async ({ value }) => { @@ -55,7 +53,6 @@ export function EditProfile() { website: value.website || undefined, }, showActivityOnPublicProfile: value.showActivityOnPublicProfile, - showChallengesOnPublicProfile: value.showChallengesOnPublicProfile, }; const response = await Ape.users.updateProfile({ @@ -262,18 +259,6 @@ export function EditProfile() { -
- - - {(field) => ( - - )} - -
- save diff --git a/frontend/src/ts/components/pages/profile/Challenges.tsx b/frontend/src/ts/components/pages/profile/Challenges.tsx deleted file mode 100644 index bb7bc34c01ec..000000000000 --- a/frontend/src/ts/components/pages/profile/Challenges.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { - Challenge, - getChallenge, - getRegularChallenges, -} from "@monkeytype/challenges"; -import { ChallengeName } from "@monkeytype/schemas/challenges"; -import { UserChallenges } from "@monkeytype/schemas/users"; -import { typedEntries } from "@monkeytype/util/objects"; -import { createMemo, For, Show } from "solid-js"; - -import { FaSolidIcon } from "../../../types/font-awesome"; -import { cn } from "../../../utils/cn"; -import { Fa } from "../../common/Fa"; - -function sortNewestFirst( - a: [ChallengeName, { addedAt?: number | undefined } | undefined], - b: [ChallengeName, { addedAt?: number | undefined } | undefined], -): number { - const aHas = a[1]?.addedAt !== undefined; - const bHas = b[1]?.addedAt !== undefined; - if (aHas && !bHas) return -1; - if (!aHas && bHas) return 1; - if (aHas && bHas) return (b[1]?.addedAt ?? 0) - (a[1]?.addedAt ?? 0); - return a[0].localeCompare(b[0]); -} - -export function Challenges(props: { - isAccountPage?: true; - challenges: UserChallenges | undefined; -}) { - const completedChallenges = createMemo((): Challenge[] => - ( - typedEntries(props.challenges ?? {}) as [ - ChallengeName, - { addedAt?: number | undefined } | undefined, - ][] - ) - .sort(sortNewestFirst) - .map(([name]) => getChallenge(name)) - .filter((it) => it !== undefined), - ); - - const completedNames = createMemo( - () => new Set(completedChallenges().map((it) => it.name)), - ); - - const incompleteChallenges = createMemo((): Challenge[] => - getRegularChallenges().filter((it) => !completedNames().has(it.name)), - ); - - return ( - -
-
-

Challenges

-
- {Object.keys(props.challenges ?? {}).length} /{" "} - {getRegularChallenges().length} completed -
-
- -
- - {(challenge) => ( - - )} - - - - {(challenge) => ( - - )} - - -
-
-
- ); -} - -function ChallengeItem(props: { completed: boolean; challenge: Challenge }) { - const icon = (): FaSolidIcon => { - switch (props.challenge.category) { - case "accuracy": - return "fa-bullseye"; - case "champions": - return "fa-crown"; - case "endurance": - return "fa-running"; - case "funbox": - return "fa-gamepad"; - case "speed": - return "fa-tachometer-alt"; - case "script": - return "fa-file-alt"; - - default: - return "fa-trophy"; - } - }; - return ( -
-
- -
-
-

{props.challenge.display}

-

{props.challenge.description}

-
-
- ); -} diff --git a/frontend/src/ts/components/pages/profile/UserDetails.tsx b/frontend/src/ts/components/pages/profile/UserDetails.tsx index 8e7ad0924b64..1824eac6ed2f 100644 --- a/frontend/src/ts/components/pages/profile/UserDetails.tsx +++ b/frontend/src/ts/components/pages/profile/UserDetails.tsx @@ -1,4 +1,3 @@ -import { getRegularChallenges } from "@monkeytype/challenges"; import { TypingStats as TypingStatsType, UserProfile, @@ -10,7 +9,6 @@ import { getCurrentDayTimestamp, } from "@monkeytype/util/date-and-time"; import { isSafeNumber } from "@monkeytype/util/numbers"; -import { typedKeys } from "@monkeytype/util/objects"; import { differenceInDays } from "date-fns/differenceInDays"; import { formatDate } from "date-fns/format"; import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict"; @@ -82,7 +80,6 @@ export function UserDetails(props: {
@@ -412,7 +409,6 @@ function BioAndKeyboard(props: { function TypingStats(props: { typingStats: TypingStatsType; - completedChallenges: number | undefined; variant: Variant; }): JSXElement { const stats = () => formatTypingStatsRatio(props.typingStats); @@ -433,13 +429,13 @@ function TypingStats(props: { class={cn( "grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-2", props.variant === "basic" && - "sm:grid-cols-3 md:grid-cols-1 lg:grid-cols-4 lg:text-[1.25rem]", + "sm:grid-cols-3 md:grid-cols-1 lg:grid-cols-3 lg:text-[1.25rem]", props.variant === "hasBioOrKeyboard" && "sm:col-span-2 md:order-2 md:col-span-1 md:grid-cols-1", props.variant === "hasSocials" && - "sm:col-span-2 sm:grid-cols-4 md:col-span-1 md:grid-cols-1 lg:grid-cols-4 xl:text-[1.25rem]", + "sm:col-span-2 sm:grid-cols-3 md:col-span-1 md:grid-cols-1 lg:grid-cols-3 xl:text-[1.25rem]", props.variant === "full" && - "sm:col-span-2 sm:grid-cols-4 md:col-span-3 md:grid-cols-4 lg:order-2 lg:col-span-1 lg:grid-cols-1", + "sm:col-span-2 sm:grid-cols-3 md:col-span-3 md:grid-cols-3 lg:order-2 lg:col-span-1 lg:grid-cols-1", )} >
@@ -471,16 +467,6 @@ function TypingStats(props: { )}
- - -
-
challenges
-
- {props.completedChallenges}{" "} - / {getRegularChallenges().length} -
-
-
); diff --git a/frontend/src/ts/components/pages/profile/UserProfile.tsx b/frontend/src/ts/components/pages/profile/UserProfile.tsx index 58423fd92dc9..4e8906a311da 100644 --- a/frontend/src/ts/components/pages/profile/UserProfile.tsx +++ b/frontend/src/ts/components/pages/profile/UserProfile.tsx @@ -11,7 +11,6 @@ import { getFormatting } from "../../../states/core"; import { formatTopPercentage } from "../../../utils/misc"; import { Button } from "../../common/Button"; import { ActivityCalendar } from "./ActivityCalendar"; -import { Challenges } from "./Challenges"; import { UserDetails } from "./UserDetails"; export function UserProfile(props: { @@ -56,11 +55,6 @@ export function UserProfile(props: { testActivity={props.profile.testActivity} isAccountPage={props.isAccountPage} /> - - ); } diff --git a/frontend/src/ts/controllers/url-handler.tsx b/frontend/src/ts/controllers/url-handler.tsx index 9abe54e075ef..3b5c326f7258 100644 --- a/frontend/src/ts/controllers/url-handler.tsx +++ b/frontend/src/ts/controllers/url-handler.tsx @@ -46,11 +46,10 @@ export async function linkDiscord(hashOverride: string): Promise { const accessToken = fragment.get("access_token") as string; const tokenType = fragment.get("token_type") as string; const state = fragment.get("state") as string; - const scope = fragment.get("scope"); showLoaderBar(); const response = await Ape.users.linkDiscord({ - body: { tokenType, accessToken, state, scope: scope?.split(" ") }, + body: { tokenType, accessToken, state }, }); hideLoaderBar(); diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 3cbfb7ea7a9c..cb17d5f81dc3 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -138,7 +138,6 @@ export async function initSnapshot(): Promise { firstDayOfTheWeek, ); } - snap.challenges = userData.challenges; const hourOffset = userData?.streak?.hourOffset; snap.streakHourOffset = hourOffset ?? undefined; diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts index 28183253306c..6452541bc1f4 100644 --- a/frontend/src/ts/pages/account-settings.ts +++ b/frontend/src/ts/pages/account-settings.ts @@ -181,18 +181,15 @@ qsa( ".page.pageAccountSettings .section.discordIntegration .getLinkAndGoToOauth", )?.on("click", () => { showLoaderBar(); - - void Ape.users - .getDiscordOAuth({ query: { includeRoles: true } }) - .then((response) => { - if (response.status === 200) { - window.open(response.body.data.url, "_self"); - } else { - showErrorNotification( - `Failed to get OAuth from discord: ${response.body.message}`, - ); - } - }); + void Ape.users.getDiscordOAuth().then((response) => { + if (response.status === 200) { + window.open(response.body.data.url, "_self"); + } else { + showErrorNotification( + `Failed to get OAuth from discord: ${response.body.message}`, + ); + } + }); }); qs(".page.pageAccountSettings #setStreakHourOffset")?.on("click", () => { diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index 2c05f2cd18de..255bc9ec2499 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -23,7 +23,7 @@ export type Challenge = { | "funbox" | "champions" | "roleCount"; - settings?: ChallengeSettings; + settings: ChallengeSettings; }; type ChallengeParameter = @@ -100,34 +100,6 @@ const challenges: Record> = { }, }, }, - "100hours": { - display: "100 hours", - isHidden: true, - discordRoleId: "761766710704603166", - category: "other", - description: "Achieve 100 hours of typing.", - }, - "250hours": { - display: "250 hours", - isHidden: true, - discordRoleId: "799825381733433344", - category: "other", - description: "Achieve 250 hours of typing.", - }, - "500hours": { - display: "500 hours", - isHidden: true, - discordRoleId: "951861792622125106", - category: "other", - description: "Achieve 500 hours of typing.", - }, - "1000hours": { - display: "1000 hours", - isHidden: true, - discordRoleId: "1262175323588395100", - category: "other", - description: "Achieve 1000 hours of typing.", - }, oneHourWarrior: { display: "One Hour Warrior", isHidden: true, @@ -604,14 +576,6 @@ const challenges: Record> = { "Type out the entire Shrek script with punctuation while watching the movie simultaneously.", settings: { type: "script", parameters: { script: "shrek.txt" } }, }, - fiftyShadesOfHell: { - display: "50 Shades of Hell", - isHidden: true, - discordRoleId: "751802155119280128", - category: "script", - description: "Type out your favourite chapter from 50 Shades of Gray.", - settings: { type: "other" }, - }, lookAtMeIAmTheDeveloperNow: { display: "Look at me. I am the developer now.", isHidden: true, @@ -944,284 +908,6 @@ const challenges: Record> = { requirements: { acc: { exact: 100 } }, }, }, - ultimateMonkeyFlex: { - display: "Ultimate Monkey Flex", - isHidden: true, - discordRoleId: "768497815496032266", - category: "champions", - description: "Have the most champion roles in the server.", - }, - oneRoleToRuleThemAll: { - display: "One role to rule them all", - isHidden: true, - discordRoleId: "758784729151176755", - category: "champions", - description: "Have the most challenge roles in the server.", - }, - doYouKnowTheDefinitionOfInsanity: { - display: "Do You Know The Definition Of Insanity", - isHidden: true, - discordRoleId: "736527448757370880", - category: "champions", - description: "Complete the longest typing session in Monkeytype history.", - }, - oneHourChampion: { - display: "One Hour Champion", - isHidden: true, - discordRoleId: "728650773503934464", - category: "champions", - description: "Achieve the highest WPM in a one-hour test.", - }, - fluidChampion: { - display: "Fluid Champion", - isHidden: true, - discordRoleId: "740568718719058041", - category: "champions", - description: "Achieve the highest WPM in a 60-second layoutfluid test.", - }, - accuracyChampion: { - display: "Accuracy Champion", - isHidden: true, - discordRoleId: "768499906511110235", - category: "champions", - description: "Achieve the longest Master mode test.", - }, - literallyTheFastestPersonHere: { - display: "Literally The Fastest Person Here", - isHidden: true, - discordRoleId: "984922187385405460", - category: "champions", - description: - "Achieve 1st place on the time 60 English all-time leaderboard.", - }, - bananaHoarder: { - display: "Banana Hoarder", - isHidden: true, - discordRoleId: "773590599227932754", - category: "champions", - description: "Achieve 1st place on the banana leaderboard.", - }, - alpha: { - display: "A l p h a", - isHidden: true, - discordRoleId: "773590612762034176", - category: "speed", - description: - "Type a b c d e f g h i j k l m n o p q r s t u v w x y z in LESS than 3.37 seconds.", - }, - blazeIt: { - display: "Blaze It", - isHidden: true, - discordRoleId: "803650889461006346", - category: "speed", - description: "Achieve 420 WPM (can be rounded) by typing weed.", - }, - burstMaster: { - display: "Burst Master", - isHidden: true, - discordRoleId: "757330922726096917", - category: "speed", - description: "Achieve 200+ WPM on the words 10 mode.", - }, - burstGod: { - display: "Burst God", - isHidden: true, - discordRoleId: "757330992821305366", - category: "speed", - description: "Achieve 250+ WPM on the words 10 mode.", - }, - shotgun: { - display: "Shotgun", - isHidden: true, - discordRoleId: "757331084366184539", - category: "speed", - description: "Achieve 300+ WPM on the words 10 mode.", - }, - nuke: { - display: "Nuke", - isHidden: true, - discordRoleId: "912522664604758016", - category: "speed", - description: "Achieve 350+ WPM on the words 10 mode.", - }, - orbitalCannon: { - display: "Orbital Cannon", - isHidden: true, - discordRoleId: "1084094136199684196", - category: "speed", - description: "Achieve 400+ WPM on the words 10 mode.", - }, - marathonSprinter: { - display: "Marathon Sprinter", - isHidden: true, - discordRoleId: "878715678830510111", - category: "speed", - description: "Achieve 200+ WPM on a one-hour test.", - }, - flawless: { - display: "Flawless", - isHidden: true, - discordRoleId: "767070815987695637", - category: "accuracy", - description: - "Complete back-to-back tests in Master Mode: 15, 30, 60, 120 seconds and 10, 25, 50, 100 words. If you fail one, restart from the beginning. Order of modes is up to you.", - }, - hesBeginningToBelieve: { - display: "He's beginning to believe", - isHidden: true, - discordRoleId: "979729541096431688", - category: "accuracy", - description: - "Achieve 100% accuracy in a 2-minute test under specified settings.", - }, - goldenHands: { - display: "Golden Hands", - isHidden: true, - discordRoleId: "851096860969795684", - category: "accuracy", - description: "Complete a 1-hour Master mode test.", - }, - fingerBlaster: { - display: "Finger Blaster", - isHidden: true, - discordRoleId: "787509606992969728", - category: "other", - description: - "Achieve at least 60 WPM using one finger on a 60-second test.", - }, - whyAreTheWallsMoving: { - display: "Why are the walls moving?", - isHidden: true, - discordRoleId: "910078947302191114", - category: "other", - description: "Complete a one-hour test using tape mode and letter mode.", - }, - stickman: { - display: "stickman", - isHidden: true, - discordRoleId: "788107449151651890", - category: "other", - description: - "Complete a one-hour test using chopsticks/pencils/pens (you get the idea) with both hands.", - }, - waveDynamics: { - display: "Wave Dynamics", - isHidden: true, - discordRoleId: "1443311363794407586", - category: "other", - description: - "Achieve 30 wpm 100% acc on a 60 second test with the raw graph being a perfect wave (to achieve this, type 5 characters in 1 second, pause for 1 second, repeat). Must be completed with random words (time 60 mode). Must include words history in the screenshot.", - }, - apesTogetherStrong: { - display: "Apes Together Strong", - isHidden: true, - discordRoleId: "863193901153779713", - category: "other", - description: - "Complete a one-hour test in a Tribe lobby with at least 10 players.", - }, - apesTogetherStronger: { - display: "Apes Together Stronger", - isHidden: true, - discordRoleId: "898964842726195220", - category: "other", - description: - "Complete a two-hour test in a Tribe lobby with at least 10 players.", - }, - apesTogetherInvincible: { - display: "Apes Together Invincible", - isHidden: true, - discordRoleId: "1367559768746758194", - category: "other", - description: - "Complete a three-hour test in a Tribe lobby with at least 10 players.", - }, - footBarbarian: { - display: "Foot Barbarian", - isHidden: true, - discordRoleId: "1025814170962231336", - category: "other", - description: "Complete a two-hour test using your feet.", - }, - bigFoot: { - display: "Big Foot", - isHidden: true, - discordRoleId: "1030531753082900610", - category: "other", - description: "Complete a three-hour test using your feet.", - }, - woodPecker: { - display: "Wood Pecker", - isHidden: true, - discordRoleId: "753724531666845830", - category: "other", - description: "Complete a 200-word test using only your nose.", - }, - mrWorldwide: { - display: "Mr Worldwide", - isHidden: true, - discordRoleId: "762345904279519292", - category: "other", - description: - "Achieve 100 WPM on a 60-second test in 5 different languages (English, English expanded, English 10k and coding languages all count as English which is 1 language).", - }, - internalMetronome: { - display: "Internal Metronome", - isHidden: true, - discordRoleId: "934067904884916234", - category: "other", - description: - "Complete a 60-second test (standard English) with a minimum consistency of 90%, 100% accuracy and within 25% of your 60-second personal best.", - }, - roleCollector: { - display: "Role Collector", - isHidden: true, - discordRoleId: "739306809554108520", - category: "roleCount", - description: "Collect 10 roles.", - }, - roleEnthusiast: { - display: "Role Enthusiast", - isHidden: true, - discordRoleId: "753360663656529931", - category: "roleCount", - description: "Collect 20 roles.", - }, - roleAddict: { - display: "Role Addict", - isHidden: true, - discordRoleId: "758783172833443850", - category: "roleCount", - description: "Collect 30 roles.", - }, - roleOverdose: { - display: "Role Overdose", - isHidden: true, - discordRoleId: "758783365930811423", - category: "roleCount", - description: "Collect 40 roles.", - }, - roleZombie: { - display: "Role Zombie", - isHidden: true, - discordRoleId: "762701731993616405", - category: "roleCount", - description: "Collect 50 roles.", - }, - roleOverlord: { - display: "Role Overlord", - isHidden: true, - discordRoleId: "805519411502514187", - category: "roleCount", - description: "Collect 60 roles.", - }, - roleImp: { - display: "Role Imp", - isHidden: true, - discordRoleId: "906565521271558214", - category: "roleCount", - description: "Collect 70 roles.", - }, }; const map: Record = Object.fromEntries( diff --git a/packages/contracts/src/users.ts b/packages/contracts/src/users.ts index 62090ea3aabd..7a01febb6648 100644 --- a/packages/contracts/src/users.ts +++ b/packages/contracts/src/users.ts @@ -177,14 +177,6 @@ export const EditCustomThemeRequstSchema = z.object({ }); export type EditCustomThemeRequst = z.infer; -export const GetDiscordOauthLinkQuerySchema = z.object({ - includeRoles: z.boolean().optional(), -}); - -export type GetDiscordOauthLinkQuery = z.infer< - typeof GetDiscordOauthLinkQuerySchema ->; - export const GetDiscordOauthLinkResponseSchema = responseWithData( z.object({ url: z.string().url(), @@ -198,7 +190,6 @@ export const LinkDiscordRequestSchema = z.object({ tokenType: z.string(), accessToken: z.string(), state: z.string().length(20), - scope: z.array(z.string()).optional(), }); export type LinkDiscordRequest = z.infer; @@ -672,7 +663,6 @@ export const usersContract = c.router( description: "Start OAuth authentication with discord", method: "GET", path: "/discord/oauth", - query: GetDiscordOauthLinkQuerySchema.strict(), responses: { 200: GetDiscordOauthLinkResponseSchema, }, diff --git a/packages/schemas/src/challenges.ts b/packages/schemas/src/challenges.ts index cc7ad109d6d6..005ac9fe71a5 100644 --- a/packages/schemas/src/challenges.ts +++ b/packages/schemas/src/challenges.ts @@ -35,7 +35,6 @@ export const ChallengeNameSchema = z.enum( "crosstalk", "bees", "getOffMySwamp", - "fiftyShadesOfHell", "lookAtMeIAmTheDeveloperNow", "beLikeWater", "rollercoaster", @@ -48,6 +47,7 @@ export const ChallengeNameSchema = z.enum( "hidden", "iCanSeeTheFuture", "whatAreWordsAtThisPoint", + "iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS", "specials", "aeiou", "asciiWarrior", @@ -61,49 +61,6 @@ export const ChallengeNameSchema = z.enum( "englishMaster", "feetWarrior", "wingdings", - "iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS", - "100hours", - "250hours", - "500hours", - "1000hours", - "ultimateMonkeyFlex", - "oneRoleToRuleThemAll", - "doYouKnowTheDefinitionOfInsanity", - "oneHourChampion", - "fluidChampion", - "accuracyChampion", - "literallyTheFastestPersonHere", - "bananaHoarder", - "alpha", - "blazeIt", - "burstMaster", - "burstGod", - "shotgun", - "nuke", - "orbitalCannon", - "marathonSprinter", - "flawless", - "hesBeginningToBelieve", - "goldenHands", - "fingerBlaster", - "whyAreTheWallsMoving", - "stickman", - "waveDynamics", - "apesTogetherStrong", - "apesTogetherStronger", - "apesTogetherInvincible", - "footBarbarian", - "bigFoot", - "woodPecker", - "mrWorldwide", - "internalMetronome", - "roleCollector", - "roleEnthusiast", - "roleAddict", - "roleOverdose", - "roleZombie", - "roleOverlord", - "roleImp", ], { errorMap: customEnumErrorHandler("Must be a known challenge name"), diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index fec0bb676a2e..ab0e6c312115 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -14,7 +14,6 @@ import { import { CustomThemeColorsSchema, FunboxNameSchema } from "./configs"; import { doesNotContainDisallowedWords } from "./validation/validation"; import { ConnectionSchema } from "./connections"; -import { ChallengeNameSchema } from "./challenges"; export const ResultFilterPresetNameSchema = slug().max(16); @@ -118,7 +117,6 @@ export const UserProfileDetailsSchema = z .strict() .optional(), showActivityOnPublicProfile: z.boolean().optional(), - showChallengesOnPublicProfile: z.boolean().optional(), }) .strict(); export type UserProfileDetails = z.infer; @@ -251,14 +249,6 @@ export const UserNameSchema = doesNotContainDisallowedWords( UserNameWithoutFilterSchema, ); -export const UserChallengesSchema = z.record( - ChallengeNameSchema, - z.object({ - addedAt: z.number().int().nonnegative().optional(), - }), -); -export type UserChallenges = z.infer; - export const UserSchema = z.object({ name: UserNameSchema, email: UserEmailSchema, @@ -294,7 +284,6 @@ export const UserSchema = z.object({ quoteMod: QuoteModSchema.optional(), resultFilterPresets: z.array(ResultFiltersSchema).optional(), testActivity: TestActivitySchema.optional(), - challenges: UserChallengesSchema.optional(), }); export type User = z.infer; @@ -323,7 +312,6 @@ export const UserProfileSchema = UserSchema.pick({ inventory: true, allTimeLbs: true, testActivity: true, - challenges: true, }) .extend({ typingStats: TypingStatsSchema, From cb2d6bc51d15ad4c7aa3659ab6e6a41a734dfc22 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 25 Jun 2026 13:01:27 +0200 Subject: [PATCH 12/12] fixes --- packages/challenges/src/index.ts | 73 +++----------------------------- 1 file changed, 7 insertions(+), 66 deletions(-) diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts index 255bc9ec2499..1c1374e910df 100644 --- a/packages/challenges/src/index.ts +++ b/packages/challenges/src/index.ts @@ -81,7 +81,6 @@ export type ChallengeSettings = { const challenges: Record> = { "69": { display: "6969696969", - isHidden: true, discordRoleId: "749505965174292511", category: "other", description: @@ -102,7 +101,6 @@ const challenges: Record> = { }, oneHourWarrior: { display: "One Hour Warrior", - isHidden: true, discordRoleId: "728371749737201855", category: "endurance", description: "Complete a one-hour test.", @@ -115,7 +113,6 @@ const challenges: Record> = { }, doubleDown: { display: "Double Down", - isHidden: true, discordRoleId: "732008008514535544", category: "endurance", description: "Complete a two-hour test.", @@ -128,7 +125,6 @@ const challenges: Record> = { }, tripleTrouble: { display: "Triple Trouble", - isHidden: true, discordRoleId: "732008047618293762", category: "endurance", description: "Complete a three-hour test.", @@ -141,7 +137,6 @@ const challenges: Record> = { }, quad: { display: "Quaaaaad", - isHidden: true, discordRoleId: "736215666352455801", category: "endurance", description: "Complete a four-hour test.", @@ -154,7 +149,6 @@ const challenges: Record> = { }, "8Ball": { display: "8 Ball", - isHidden: true, discordRoleId: "736528159956271126", category: "endurance", description: "Complete an eight-hour test.", @@ -166,7 +160,6 @@ const challenges: Record> = { }, theBig12: { display: "The Big 12", - isHidden: true, discordRoleId: "740532256388546581", category: "endurance", description: "Complete a twelve-hour test.", @@ -178,7 +171,6 @@ const challenges: Record> = { }, "1Day": { display: "1 Day", - isHidden: true, discordRoleId: "751801958511149057", category: "endurance", description: "Complete a twenty-four-hour test.", @@ -190,7 +182,6 @@ const challenges: Record> = { }, trueSimp: { display: "True Simp", - isHidden: true, discordRoleId: "744328648211038359", category: "script", description: "Type miodec ten thousand times.", @@ -208,7 +199,6 @@ const challenges: Record> = { }, bigramSalad: { display: "Bigram Salad", - isHidden: true, discordRoleId: "818535054145093652", category: "speed", description: @@ -228,7 +218,6 @@ const challenges: Record> = { }, simp: { display: "Simp", - isHidden: true, discordRoleId: "743854992699687023", category: "script", description: "Type miodec one thousand times.", @@ -246,12 +235,10 @@ const challenges: Record> = { }, simpLord: { display: "Simp Lord", - isHidden: true, discordRoleId: "984911956949479445", category: "script", description: "Type miodec one hundred thousand times.", settings: { - autoRole: false, type: "customText", parameters: { text: "miodec", @@ -264,7 +251,6 @@ const challenges: Record> = { }, antidiseWhat: { display: "Antidise-what?", - isHidden: true, discordRoleId: "782006507360616449", category: "script", description: "Get at least 200 wpm typing antidisestablishmentarianism.", @@ -283,7 +269,6 @@ const challenges: Record> = { }, whatsThisWebsiteCalledAgain: { display: "What's this website called again?", - isHidden: true, discordRoleId: "739276161603076116", category: "script", description: "Type monkeytype one thousand times.", @@ -301,7 +286,6 @@ const challenges: Record> = { }, developd: { display: "Develop'd", - isHidden: true, discordRoleId: "735964917877964932", category: "script", description: "Type develop one thousand times.", @@ -319,7 +303,6 @@ const challenges: Record> = { }, slowAndSteady: { display: "Slow and Steady", - isHidden: true, discordRoleId: "782005061935956008", category: "speed", description: @@ -336,7 +319,6 @@ const challenges: Record> = { }, speedSpacer: { display: "Speed Spacer", - isHidden: true, discordRoleId: "755244049446731856", category: "speed", description: @@ -356,7 +338,6 @@ const challenges: Record> = { }, iveGotThePower: { display: "I've got the POWER", - isHidden: true, discordRoleId: "764879734873915402", category: "speed", description: "Get 400 WPM while typing power 10 times.", @@ -375,7 +356,6 @@ const challenges: Record> = { }, accuracyExpert: { display: "Accuracy Expert", - isHidden: true, discordRoleId: "751168451263070259", category: "accuracy", description: "Complete a 10-minute Master mode test.", @@ -393,7 +373,6 @@ const challenges: Record> = { }, accuracyMaster: { display: "Accuracy Master", - isHidden: true, discordRoleId: "751168567432708239", category: "accuracy", description: "Complete a 20-minute Master mode test.", @@ -411,7 +390,6 @@ const challenges: Record> = { }, accuracyGod: { display: "Accuracy God", - isHidden: true, discordRoleId: "751168657626890361", category: "accuracy", description: "Complete a 30-minute Master mode test.", @@ -429,7 +407,6 @@ const challenges: Record> = { }, inAGalaxyFarFarAway: { display: "In a galaxy far, far away", - isHidden: true, discordRoleId: "740004324301602907", category: "script", description: @@ -442,7 +419,6 @@ const challenges: Record> = { }, beepBoop: { display: "Beep Boop", - isHidden: true, discordRoleId: "813076265145729024", category: "script", description: @@ -459,8 +435,7 @@ const challenges: Record> = { }, }, whosYourDaddy: { - display: "Who's your daddy", - isHidden: true, + display: "Who's your daddy?", discordRoleId: "742171915405361204", category: "script", description: @@ -473,7 +448,6 @@ const challenges: Record> = { }, itsATrap: { display: "It's a trap!!", - isHidden: true, discordRoleId: "744325174668820550", category: "script", description: @@ -486,7 +460,6 @@ const challenges: Record> = { }, jolly: { display: "Jolly", - isHidden: true, discordRoleId: "768497412548329563", category: "script", description: "Type the Jolly script with a minimum of 70 wpm.", @@ -499,8 +472,7 @@ const challenges: Record> = { }, }, gottaCatchEmAll: { - display: "Gotta Catch 'Em All", - isHidden: true, + display: "Gotta catch 'em all", discordRoleId: "767069340599975998", category: "script", description: "Type out the names of all Pokemon.", @@ -512,7 +484,6 @@ const challenges: Record> = { }, rapGod: { display: "Rap God", - isHidden: true, discordRoleId: "743844891045396603", category: "script", description: @@ -527,7 +498,6 @@ const challenges: Record> = { }, navySeal: { display: "Navy Seal", - isHidden: true, discordRoleId: "762345535969165342", category: "script", description: @@ -542,7 +512,6 @@ const challenges: Record> = { }, littleChef: { display: "Little Chef", - isHidden: true, discordRoleId: "763544714028122153", category: "script", description: @@ -551,7 +520,6 @@ const challenges: Record> = { }, crosstalk: { display: "(CROSSTALK)", - isHidden: true, discordRoleId: "761276009664217129", category: "script", description: @@ -560,7 +528,6 @@ const challenges: Record> = { }, bees: { display: "Bees!!!", - isHidden: true, discordRoleId: "739636003182084307", category: "script", description: @@ -568,8 +535,7 @@ const challenges: Record> = { settings: { type: "script", parameters: { script: "bees.txt" } }, }, getOffMySwamp: { - display: "Get Off My Swamp", - isHidden: true, + display: "Get off my swamp", discordRoleId: "757346966987342026", category: "script", description: @@ -578,7 +544,6 @@ const challenges: Record> = { }, lookAtMeIAmTheDeveloperNow: { display: "Look at me. I am the developer now.", - isHidden: true, discordRoleId: "937358772635074600", category: "script", description: @@ -590,8 +555,7 @@ const challenges: Record> = { }, }, beLikeWater: { - display: "Be Like Water", - isHidden: true, + display: "Be like water", discordRoleId: "740568679485276201", category: "funbox", description: @@ -604,7 +568,6 @@ const challenges: Record> = { }, rollercoaster: { display: "Rollercoaster", - isHidden: true, discordRoleId: "736032495526740001", category: "funbox", description: @@ -621,7 +584,6 @@ const challenges: Record> = { }, oneHourMirror: { display: "ɿoɿɿim ɿυoʜ ɘno", - isHidden: true, discordRoleId: "737385182998429757", category: "funbox", description: "Complete at least a one-hour test using the mirror mode.", @@ -634,7 +596,6 @@ const challenges: Record> = { }, chooChoo: { display: "Choo choo", - isHidden: true, discordRoleId: "739306439574683710", category: "funbox", description: "Complete at least a one-hour test using choo choo mode.", @@ -647,7 +608,6 @@ const challenges: Record> = { }, mnemonist: { display: "Mnemonist", - isHidden: true, discordRoleId: "782005606852067328", category: "funbox", description: @@ -665,7 +625,6 @@ const challenges: Record> = { }, earfquake: { display: "Earfquake", - isHidden: true, discordRoleId: "740730587429601291", category: "funbox", description: @@ -679,7 +638,6 @@ const challenges: Record> = { }, simonSez: { display: "Simon Sez", - isHidden: true, discordRoleId: "742128871825997914", category: "funbox", description: @@ -693,7 +651,6 @@ const challenges: Record> = { }, accountant: { display: "Accountant", - isHidden: true, discordRoleId: "743962178821816391", category: "funbox", description: @@ -707,7 +664,6 @@ const challenges: Record> = { }, hidden: { display: "Hidden", - isHidden: true, discordRoleId: "782006137742557194", category: "funbox", description: @@ -726,7 +682,6 @@ const challenges: Record> = { }, iCanSeeTheFuture: { display: "I can see the future", - isHidden: true, discordRoleId: "814877508008411226", category: "funbox", description: @@ -744,8 +699,7 @@ const challenges: Record> = { }, }, whatAreWordsAtThisPoint: { - display: "What are words at this point", - isHidden: true, + display: "What are words at this point?", discordRoleId: "744209241396740176", category: "funbox", description: @@ -759,7 +713,6 @@ const challenges: Record> = { }, specials: { display: "Specials", - isHidden: true, discordRoleId: "744209452714033162", category: "funbox", description: @@ -773,7 +726,6 @@ const challenges: Record> = { }, aeiou: { display: "Aeiou.", - isHidden: true, discordRoleId: "744318102766092362", category: "funbox", description: "Complete at least a one-hour test using the tts funbox mode.", @@ -786,7 +738,6 @@ const challenges: Record> = { }, asciiWarrior: { display: "ASCII warrior", - isHidden: true, discordRoleId: "746142791326760980", category: "funbox", description: @@ -800,7 +751,6 @@ const challenges: Record> = { }, iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS: { display: "i KINda LikE HoW inEFFICIeNt QwErtY Is.", - isHidden: true, discordRoleId: "760999194525171724", category: "funbox", description: @@ -814,7 +764,6 @@ const challenges: Record> = { }, oneNauseousMonkey: { display: "One Nauseous Monkey", - isHidden: true, discordRoleId: "760930262740631633", category: "funbox", description: @@ -827,8 +776,7 @@ const challenges: Record> = { }, }, thumbWarrior: { - display: "Thumb Warrior", - isHidden: true, + display: "Thumb warrior", discordRoleId: "761794585109200906", category: "other", description: "Complete a one-hour test using only your thumbs.", @@ -836,7 +784,6 @@ const challenges: Record> = { }, mouseWarrior: { display: "Mouse warrior", - isHidden: true, discordRoleId: "744580294442614790", category: "other", description: @@ -845,7 +792,6 @@ const challenges: Record> = { }, mobileWarrior: { display: "Mobile warrior", - isHidden: true, discordRoleId: "744723801526370407", category: "other", description: "Complete a one-hour test on mobile.", @@ -853,7 +799,6 @@ const challenges: Record> = { }, upsideDown: { display: "uʍop ǝpᴉsdn", - isHidden: true, discordRoleId: "782725716114014237", category: "other", description: @@ -862,7 +807,6 @@ const challenges: Record> = { }, oneArmedBandit: { display: "One armed bandit", - isHidden: true, discordRoleId: "765919192557682708", category: "other", description: @@ -871,7 +815,6 @@ const challenges: Record> = { }, englishMaster: { display: "English master", - isHidden: true, discordRoleId: "751166528824672396", category: "other", description: @@ -887,8 +830,7 @@ const challenges: Record> = { }, }, feetWarrior: { - display: "Foot Warrior", - isHidden: true, + display: "Feet warrior", discordRoleId: "751953592860147822", category: "other", description: "Complete a one-hour test using your feet. Don't ask me why.", @@ -896,7 +838,6 @@ const challenges: Record> = { }, wingdings: { display: "Ten Words of Pain", - isHidden: true, discordRoleId: "863192575984140338", category: "other", description: