From 633926e862e04a806221298cd8974bb28c1c7629 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 01:03:41 +0100 Subject: [PATCH 01/20] move user to shared definitions this includes whatever user can have on it, tags, presets and so on --- backend/src/api/controllers/user.ts | 13 +- backend/src/dal/leaderboards.ts | 2 +- backend/src/dal/result.ts | 2 +- backend/src/dal/user.ts | 60 ++++---- backend/src/middlewares/api-utils.ts | 2 +- backend/src/types/types.d.ts | 140 +++++++----------- frontend/src/ts/account/result-filters.ts | 2 +- frontend/src/ts/ape/endpoints/users.ts | 4 +- frontend/src/ts/ape/types/users.d.ts | 8 + frontend/src/ts/commandline/index.ts | 2 +- .../commandline/lists/custom-themes-list.ts | 6 +- frontend/src/ts/config.ts | 2 +- frontend/src/ts/constants/default-snapshot.ts | 8 +- .../src/ts/controllers/account-controller.ts | 5 +- .../src/ts/controllers/quotes-controller.ts | 8 + .../src/ts/controllers/theme-controller.ts | 6 +- frontend/src/ts/db.ts | 137 ++++++++++------- frontend/src/ts/elements/leaderboards.ts | 2 +- frontend/src/ts/popups/edit-preset-popup.ts | 4 +- frontend/src/ts/popups/edit-profile-popup.ts | 6 +- frontend/src/ts/popups/quote-search-popup.ts | 12 +- frontend/src/ts/popups/simple-popups.ts | 4 +- frontend/src/ts/settings/theme-picker.ts | 2 +- frontend/src/ts/test/result.ts | 9 +- frontend/src/ts/types/types.d.ts | 95 ++++-------- frontend/webpack/config.base.js | 8 +- shared-types/types.d.ts | 77 ++++++++++ 27 files changed, 357 insertions(+), 269 deletions(-) create mode 100644 frontend/src/ts/ape/types/users.d.ts diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 65aa304a7857..fc611ed01ec3 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -316,8 +316,8 @@ export async function updateEmail( } function getRelevantUserInfo( - user: MonkeyTypes.User -): Partial { + user: MonkeyTypes.DBUser +): Partial { return _.omit(user, [ "bananas", "lbPersonalBests", @@ -335,7 +335,7 @@ export async function getUser( ): Promise { const { uid } = req.ctx.decodedToken; - let userInfo: MonkeyTypes.User; + let userInfo: MonkeyTypes.DBUser; try { userInfo = await UserDAL.getUser(uid, "get user"); } catch (e) { @@ -810,10 +810,13 @@ export async function updateProfile( } }); - const profileDetailsUpdates: Partial = { + const profileDetailsUpdates: Partial = { bio: sanitizeString(bio), keyboard: sanitizeString(keyboard), - socialProfiles: _.mapValues(socialProfiles, sanitizeString), + socialProfiles: _.mapValues( + socialProfiles, + sanitizeString + ) as SharedTypes.UserProfileDetails["socialProfiles"], }; await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory); diff --git a/backend/src/dal/leaderboards.ts b/backend/src/dal/leaderboards.ts index 81669f98c622..d4a62334e33c 100644 --- a/backend/src/dal/leaderboards.ts +++ b/backend/src/dal/leaderboards.ts @@ -70,7 +70,7 @@ export async function update( leaderboardUpdating[`${language}_${mode}_${mode2}`] = true; const start1 = performance.now(); const lb = db - .collection("users") + .collection("users") .aggregate( [ { diff --git a/backend/src/dal/result.ts b/backend/src/dal/result.ts index e96f306eb4bd..b1e28ad263d2 100644 --- a/backend/src/dal/result.ts +++ b/backend/src/dal/result.ts @@ -13,7 +13,7 @@ export async function addResult( uid: string, result: DBResult ): Promise<{ insertedId: ObjectId }> { - let user: MonkeyTypes.User | null = null; + let user: MonkeyTypes.DBUser | null = null; try { user = await getUser(uid, "add result"); } catch (e) { diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 829acab25027..327be2ce3a0e 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -16,15 +16,15 @@ type Result = Omit< >; // Export for use in tests -export const getUsersCollection = (): Collection> => - db.collection("users"); +export const getUsersCollection = (): Collection => + db.collection("users"); export async function addUser( name: string, email: string, uid: string ): Promise { - const newUserDocument: Partial = { + const newUserDocument: Partial = { name, email, uid, @@ -76,7 +76,11 @@ export async function resetUser(uid: string): Promise { profileDetails: { bio: "", keyboard: "", - socialProfiles: {}, + socialProfiles: { + github: "", + twitter: "", + website: "", + }, }, favoriteQuotes: {}, customThemes: [], @@ -170,7 +174,7 @@ export async function optOutOfLeaderboards(uid: string): Promise { export async function updateQuoteRatings( uid: string, - quoteRatings: MonkeyTypes.UserQuoteRatings + quoteRatings: SharedTypes.UserQuoteRatings ): Promise { await getUser(uid, "update quote ratings"); @@ -191,13 +195,15 @@ export async function updateEmail( export async function getUser( uid: string, stack: string -): Promise { +): Promise { const user = await getUsersCollection().findOne({ uid }); if (!user) throw new MonkeyError(404, "User not found", stack); return user; } -async function findByName(name: string): Promise { +async function findByName( + name: string +): Promise { return ( await getUsersCollection() .find({ name }) @@ -220,7 +226,7 @@ export async function isNameAvailable( export async function getUserByName( name: string, stack: string -): Promise { +): Promise { const user = await findByName(name); if (!user) throw new MonkeyError(404, "User not found", stack); return user; @@ -284,7 +290,7 @@ export async function removeResultFilterPreset( export async function addTag( uid: string, name: string -): Promise { +): Promise { const user = await getUser(uid, "add tag"); if ((user?.tags?.length ?? 0) >= 15) { @@ -315,7 +321,7 @@ export async function addTag( return toPush; } -export async function getTags(uid: string): Promise { +export async function getTags(uid: string): Promise { const user = await getUser(uid, "get tags"); return user.tags ?? []; @@ -396,9 +402,11 @@ export async function updateLbMemory( const user = await getUser(uid, "update lb memory"); if (user.lbMemory === undefined) user.lbMemory = {}; if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {}; - if (user.lbMemory[mode][mode2] === undefined) { + if (user.lbMemory[mode]?.[mode2] === undefined) { + //@ts-expect-error guarded above user.lbMemory[mode][mode2] = {}; } + //@ts-expect-error guarded above user.lbMemory[mode][mode2][language] = rank; await getUsersCollection().updateOne( { uid }, @@ -410,7 +418,7 @@ export async function updateLbMemory( export async function checkIfPb( uid: string, - user: MonkeyTypes.User, + user: MonkeyTypes.DBUser, result: Result ): Promise { const { mode } = result; @@ -452,7 +460,7 @@ export async function checkIfPb( export async function checkIfTagPb( uid: string, - user: MonkeyTypes.User, + user: MonkeyTypes.DBUser, result: Result ): Promise { if (user.tags === undefined || user.tags.length === 0) { @@ -466,7 +474,7 @@ export async function checkIfTagPb( return []; } - const tagsToCheck: MonkeyTypes.UserTag[] = []; + const tagsToCheck: MonkeyTypes.DBUserTag[] = []; user.tags.forEach((userTag) => { for (const resultTag of resultTags ?? []) { if (resultTag === userTag._id.toHexString()) { @@ -553,7 +561,7 @@ export async function linkDiscord( discordId: string, discordAvatar?: string ): Promise { - const updates: Partial = _.pickBy( + const updates: Partial = _.pickBy( { discordId, discordAvatar }, _.identity ); @@ -672,7 +680,7 @@ export async function editTheme(uid: string, _id, theme): Promise { export async function getThemes( uid: string -): Promise { +): Promise { const user = await getUser(uid, "get themes"); return user.customThemes ?? []; } @@ -705,7 +713,7 @@ export async function getStats( export async function getFavoriteQuotes( uid -): Promise { +): Promise { const user = await getUser(uid, "get favorite quotes"); return user.favoriteQuotes ?? {}; @@ -789,7 +797,7 @@ export async function recordAutoBanEvent( recentAutoBanTimestamps.push(now); //update user, ban if needed - const updateObj: Partial = { + const updateObj: Partial = { autoBanTimestamps: recentAutoBanTimestamps, }; let banningUser = false; @@ -810,8 +818,8 @@ export async function recordAutoBanEvent( export async function updateProfile( uid: string, - profileDetailUpdates: Partial, - inventory?: MonkeyTypes.UserInventory + profileDetailUpdates: Partial, + inventory?: SharedTypes.UserInventory ): Promise { const profileUpdates = _.omitBy( flattenObjectDeep(profileDetailUpdates, "profileDetails"), @@ -837,7 +845,7 @@ export async function updateProfile( export async function getInbox( uid: string -): Promise { +): Promise { const user = await getUser(uid, "get inventory"); return user.inbox ?? []; } @@ -904,9 +912,9 @@ export async function addToInbox( function buildRewardUpdates( rewards: MonkeyTypes.AllRewards[], inventoryIsNull = false -): UpdateFilter> { +): UpdateFilter { let totalXp = 0; - const newBadges: MonkeyTypes.Badge[] = []; + const newBadges: SharedTypes.Badge[] = []; rewards.forEach((reward) => { if (reward.type === "xp") { @@ -988,7 +996,7 @@ export async function updateStreak( timestamp: number ): Promise { const user = await getUser(uid, "calculate streak"); - const streak: MonkeyTypes.UserStreak = { + const streak: SharedTypes.UserStreak = { lastResultTimestamp: user.streak?.lastResultTimestamp ?? 0, length: user.streak?.length ?? 0, maxLength: user.streak?.maxLength ?? 0, @@ -1043,7 +1051,7 @@ export async function setBanned(uid: string, banned: boolean): Promise { export async function checkIfUserIsPremium( uid: string, - userInfoOverride?: MonkeyTypes.User + userInfoOverride?: MonkeyTypes.DBUser ): Promise { const user = userInfoOverride ?? (await getUser(uid, "checkIfUserIsPremium")); const expirationDate = user.premium?.expirationTimestamp; @@ -1056,7 +1064,7 @@ export async function checkIfUserIsPremium( export async function logIpAddress( uid: string, ip: string, - userInfoOverride?: MonkeyTypes.User + userInfoOverride?: MonkeyTypes.DBUser ): Promise { const user = userInfoOverride ?? (await getUser(uid, "logIpAddress")); const currentIps = user.ips ?? []; diff --git a/backend/src/middlewares/api-utils.ts b/backend/src/middlewares/api-utils.ts index dfffce16fb26..347425a5fe0e 100644 --- a/backend/src/middlewares/api-utils.ts +++ b/backend/src/middlewares/api-utils.ts @@ -72,7 +72,7 @@ function checkIfUserIsAdmin(): RequestHandler { * Note that this middleware must be used after authentication in the middleware stack. */ function checkUserPermissions( - options: ValidationOptions + options: ValidationOptions ): RequestHandler { const { criteria, invalidMessage = "You don't have permission to do this." } = options; diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index d8e8b720be15..4029e693d832 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -20,16 +20,6 @@ declare namespace MonkeyTypes { // Data Model - type UserProfileDetails = { - bio?: string; - keyboard?: string; - socialProfiles: { - twitter?: string; - github?: string; - website?: string; - }; - }; - type Reward = { type: string; item: T; @@ -42,8 +32,8 @@ declare namespace MonkeyTypes { type BadgeReward = { type: "badge"; - item: Badge; - } & Reward; + item: SharedTypes.Badge; + } & Reward; type AllRewards = XpReward | BadgeReward; @@ -56,65 +46,66 @@ declare namespace MonkeyTypes { rewards: AllRewards[]; }; - type UserIpHistory = string[]; - - type User = { - autoBanTimestamps?: number[]; - addedAt: number; - verified?: boolean; - bananas?: number; - completedTests?: number; - discordId?: string; - email: string; - lastNameChange?: number; - lbMemory?: object; + type DBUser = Omit< + SharedTypes.User, + "resultFilterPresets" | "tags" | "customThemes" + > & { + _id: ObjectId; + resultFilterPresets?: WithObjectIdArray; + tags?: DBUserTag[]; lbPersonalBests?: LbPersonalBests; - name: string; - customThemes?: CustomTheme[]; - personalBests: SharedTypes.PersonalBests; - quoteRatings?: UserQuoteRatings; - startedTests?: number; - tags?: UserTag[]; - timeTyping?: number; - uid: string; - quoteMod?: boolean; - configurationMod?: boolean; - admin?: boolean; + customThemes?: DBCustomTheme[]; + autoBanTimestamps?: number[]; + inbox?: MonkeyMail[]; + ips?: string[]; canReport?: boolean; - banned?: boolean; + lastNameChange?: number; canManageApeKeys?: boolean; - favoriteQuotes?: Record; - needsToChangeName?: boolean; - discordAvatar?: string; - resultFilterPresets?: WithObjectIdArray; - profileDetails?: UserProfileDetails; - inventory?: UserInventory; - xp?: number; - inbox?: MonkeyMail[]; - streak?: UserStreak; - lastReultHashes?: string[]; - lbOptOut?: boolean; - premium?: PremiumInfo; - ips?: UserIpHistory; }; - type UserStreak = { - lastResultTimestamp: number; - length: number; - maxLength: number; - hourOffset?: number; - }; - - type UserInventory = { - badges: Badge[]; - }; - - type Badge = { - id: number; - selected?: boolean; - }; - - type UserQuoteRatings = Record>; + type DBCustomTheme = WithObjectId; + + type DBUserTag = WithObjectId; + + // type User = { + // autoBanTimestamps?: number[]; + // addedAt: number; + // verified?: boolean; + // bananas?: number; + // completedTests?: number; + // discordId?: string; + // email: string; + // lastNameChange?: number; + // lbMemory?: object; + // lbPersonalBests?: LbPersonalBests; + // name: string; + // customThemes?: MonkeyTypes.WithObjectIdArray; + // personalBests: SharedTypes.PersonalBests; + // quoteRatings?: SharedTypes.UserQuoteRatings; + // startedTests?: number; + // tags?: MonkeyTypes.WithObjectIdArray; + // timeTyping?: number; + // uid: string; + // quoteMod?: boolean; + // configurationMod?: boolean; + // admin?: boolean; + // canReport?: boolean; + // banned?: boolean; + // canManageApeKeys?: boolean; + // favoriteQuotes?: Record; + // needsToChangeName?: boolean; + // discordAvatar?: string; + // resultFilterPresets?: WithObjectIdArray; + // profileDetails?: SharedTypes.UserProfileDetails; + // inventory?: SharedTypes.UserInventory; + // xp?: number; + // inbox?: MonkeyMail[]; + // streak?: SharedTypes.UserStreak; + // lastReultHashes?: string[]; + // lbOptOut?: boolean; + // premium?: SharedTypes.PremiumInfo; + // ips?: UserIpHistory; + // }; type LbPersonalBests = { time: Record>; @@ -129,18 +120,6 @@ declare namespace MonkeyTypes { _id: ObjectId; }[]; - type UserTag = { - _id: ObjectId; - name: string; - personalBests: SharedTypes.PersonalBests; - }; - - type CustomTheme = { - _id: ObjectId; - name: string; - colors: string[]; - }; - type ApeKeyDB = SharedTypes.ApeKey & { _id: ObjectId; uid: string; @@ -188,9 +167,4 @@ declare namespace MonkeyTypes { frontendForcedConfig?: Record; frontendFunctions?: string[]; }; - - type PremiumInfo = { - startTimestamp: number; - expirationTimestamp: number; - }; } diff --git a/frontend/src/ts/account/result-filters.ts b/frontend/src/ts/account/result-filters.ts index e35f39228350..b2e3dbce5d08 100644 --- a/frontend/src/ts/account/result-filters.ts +++ b/frontend/src/ts/account/result-filters.ts @@ -299,7 +299,7 @@ function setAllFilters( }); } -export function loadTags(tags: MonkeyTypes.Tag[]): void { +export function loadTags(tags: MonkeyTypes.UserTag[]): void { tags.forEach((tag) => { defaultResultFilters.tags[tag._id] = true; }); diff --git a/frontend/src/ts/ape/endpoints/users.ts b/frontend/src/ts/ape/endpoints/users.ts index 9837627db73c..f617383a8155 100644 --- a/frontend/src/ts/ape/endpoints/users.ts +++ b/frontend/src/ts/ape/endpoints/users.ts @@ -5,7 +5,7 @@ export default class Users { this.httpClient = httpClient; } - async getData(): Ape.EndpointResponse { + async getData(): Ape.EndpointResponse { return await this.httpClient.get(BASE_PATH); } @@ -203,7 +203,7 @@ export default class Users { } async updateProfile( - profileUpdates: Partial, + profileUpdates: Partial, selectedBadgeId?: number ): Promise { return await this.httpClient.patch(`${BASE_PATH}/profile`, { diff --git a/frontend/src/ts/ape/types/users.d.ts b/frontend/src/ts/ape/types/users.d.ts new file mode 100644 index 000000000000..125b7bfa5a19 --- /dev/null +++ b/frontend/src/ts/ape/types/users.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// for some reason when using the dot notaion, the types are not being recognized as used +declare namespace Ape.Users { + type GetUser = SharedTypes.User & { + inboxUnreadSize: number; + isPremium: boolean; + }; +} diff --git a/frontend/src/ts/commandline/index.ts b/frontend/src/ts/commandline/index.ts index b9b09eb907f0..61514156c416 100644 --- a/frontend/src/ts/commandline/index.ts +++ b/frontend/src/ts/commandline/index.ts @@ -805,7 +805,7 @@ $("footer").on("click", ".leftright .right .current-theme", (e) => { if (e.shiftKey) { if (!Config.customTheme) { if (isAuthenticated()) { - if ((DB.getSnapshot()?.customThemes.length ?? 0) < 1) { + if ((DB.getSnapshot()?.customThemes?.length ?? 0) < 1) { Notifications.add("No custom themes!", 0); UpdateConfig.setCustomTheme(false); // UpdateConfig.setCustomThemeId(""); diff --git a/frontend/src/ts/commandline/lists/custom-themes-list.ts b/frontend/src/ts/commandline/lists/custom-themes-list.ts index 336855544015..d5463f2a1ac9 100644 --- a/frontend/src/ts/commandline/lists/custom-themes-list.ts +++ b/frontend/src/ts/commandline/lists/custom-themes-list.ts @@ -33,7 +33,11 @@ export function update(): void { if (!snapshot) return; - if (snapshot.customThemes.length === 0) { + if (snapshot.customThemes === undefined) { + return; + } + + if (snapshot.customThemes?.length === 0) { return; } snapshot.customThemes.forEach((theme) => { diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 9f93316c9470..8f60cc6edc8f 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -1440,7 +1440,7 @@ export function setRandomTheme( return false; } if (!DB.getSnapshot()) return true; - if (DB.getSnapshot()?.customThemes.length === 0) { + if (DB.getSnapshot()?.customThemes?.length === 0) { Notifications.add("You need to create a custom theme first", 0); config.randomTheme = "off"; return false; diff --git a/frontend/src/ts/constants/default-snapshot.ts b/frontend/src/ts/constants/default-snapshot.ts index fed4dbd472b8..913adddc115f 100644 --- a/frontend/src/ts/constants/default-snapshot.ts +++ b/frontend/src/ts/constants/default-snapshot.ts @@ -1,3 +1,5 @@ +import defaultConfig from "./default-config"; + export const defaultSnap: MonkeyTypes.Snapshot = { results: undefined, personalBests: { @@ -8,13 +10,15 @@ export const defaultSnap: MonkeyTypes.Snapshot = { custom: {}, }, name: "", + email: "", + uid: "", + isPremium: false, + config: defaultConfig, customThemes: [], presets: [], tags: [], - favouriteThemes: [], banned: undefined, verified: undefined, - emailVerified: undefined, lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } }, typingStats: { timeTyping: 0, diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts index 6af719cbcf46..0362bc8cfb14 100644 --- a/frontend/src/ts/controllers/account-controller.ts +++ b/frontend/src/ts/controllers/account-controller.ts @@ -153,10 +153,7 @@ async function getDataAndInit(): Promise { const areConfigsEqual = JSON.stringify(Config) === JSON.stringify(snapshot.config); - if ( - snapshot.config && - (UpdateConfig.localStorageConfig === undefined || !areConfigsEqual) - ) { + if (UpdateConfig.localStorageConfig === undefined || !areConfigsEqual) { console.log( "no local config or local and db configs are different - applying db" ); diff --git a/frontend/src/ts/controllers/quotes-controller.ts b/frontend/src/ts/controllers/quotes-controller.ts index 89c9588249d3..b9bbb0e1eaeb 100644 --- a/frontend/src/ts/controllers/quotes-controller.ts +++ b/frontend/src/ts/controllers/quotes-controller.ts @@ -164,6 +164,10 @@ class QuotesController { const quoteIds: string[] = []; const { favoriteQuotes } = snapshot; + if (favoriteQuotes === undefined) { + return null; + } + Object.keys(favoriteQuotes).forEach((language) => { if (removeLanguageSize(language) !== normalizedLanguage) { return; @@ -190,6 +194,10 @@ class QuotesController { const { favoriteQuotes } = snapshot; + if (favoriteQuotes === undefined) { + return false; + } + const normalizedQuoteLanguage = removeLanguageSize(quoteLanguage); const matchedLanguage = Object.keys(favoriteQuotes).find((language) => { diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 3d4f5880f295..81a5717451fa 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -262,7 +262,7 @@ async function changeThemeList(): Promise { return t.name; }); } else if (Config.randomTheme === "custom" && DB.getSnapshot()) { - themesList = DB.getSnapshot()?.customThemes.map((ct) => ct._id) ?? []; + themesList = DB.getSnapshot()?.customThemes?.map((ct) => ct._id) ?? []; } Misc.shuffle(themesList); randomThemeIndex = 0; @@ -284,7 +284,7 @@ export async function randomizeTheme(): Promise { let colorsOverride: string[] | undefined; if (Config.randomTheme === "custom") { - const theme = DB.getSnapshot()?.customThemes.find( + const theme = DB.getSnapshot()?.customThemes?.find( (ct) => ct._id === randomTheme ); colorsOverride = theme?.colors; @@ -297,7 +297,7 @@ export async function randomizeTheme(): Promise { let name = randomTheme.replace(/_/g, " "); if (Config.randomTheme === "custom") { name = ( - DB.getSnapshot()?.customThemes.find((ct) => ct._id === randomTheme) + DB.getSnapshot()?.customThemes?.find((ct) => ct._id === randomTheme) ?.name ?? "custom" ).replace(/_/g, " "); } diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 0d8e57398d81..b5da91eb53db 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -84,10 +84,17 @@ export async function initSnapshot(): Promise< }; } + const userData = userResponse.data; const configData = configResponse.data; const presetsData = presetsResponse.data; - const [userData] = [userResponse].map((response) => response.data); + if (userData === null) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw { + message: "Request was successful but user data is null", + responseCode: 200, + }; + } snap.name = userData.name; snap.personalBests = userData.personalBests; @@ -110,15 +117,13 @@ export async function initSnapshot(): Promise< snap.discordAvatar = userData.discordAvatar; snap.needsToChangeName = userData.needsToChangeName; snap.typingStats = { - timeTyping: userData.timeTyping, - startedTests: userData.startedTests, - completedTests: userData.completedTests, + timeTyping: userData.timeTyping ?? 0, + startedTests: userData.startedTests ?? 0, + completedTests: userData.completedTests ?? 0, }; snap.quoteMod = userData.quoteMod; snap.favoriteQuotes = userData.favoriteQuotes ?? {}; snap.quoteRatings = userData.quoteRatings; - snap.favouriteThemes = - userData.favouriteThemes === undefined ? [] : userData.favouriteThemes; snap.details = userData.profileDetails; snap.addedAt = userData.addedAt; snap.inventory = userData.inventory; @@ -133,13 +138,7 @@ export async function initSnapshot(): Promise< snap.streakHourOffset = hourOffset === undefined || hourOffset === null ? undefined : hourOffset; - if ( - userData.lbMemory?.time15 !== undefined || - userData.lbMemory?.time60 !== undefined - ) { - //old memory format - snap.lbMemory = {} as MonkeyTypes.LeaderboardMemory; - } else if (userData.lbMemory !== undefined) { + if (userData.lbMemory !== undefined) { snap.lbMemory = userData.lbMemory; } // if (ActivePage.get() === "loading") { @@ -163,24 +162,43 @@ export async function initSnapshot(): Promise< // LoadingPage.updateText("Downloading tags..."); snap.customThemes = userData.customThemes ?? []; - const userDataTags: MonkeyTypes.Tag[] = userData.tags ?? []; - - userDataTags.forEach((tag) => { - tag.display = tag.name.replaceAll("_", " "); - tag.personalBests ??= { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; - - for (const mode of ["time", "words", "quote", "zen", "custom"]) { - tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {}; - } - }); - - snap.tags = userDataTags; + // const userDataTags: MonkeyTypes.UserTagWithDisplay[] = userData.tags ?? []; + + // userDataTags.forEach((tag) => { + // tag.display = tag.name.replaceAll("_", " "); + // tag.personalBests ??= { + // time: {}, + // words: {}, + // quote: {}, + // zen: {}, + // custom: {}, + // }; + + // for (const mode of ["time", "words", "quote", "zen", "custom"]) { + // tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {}; + // } + // }); + + // snap.tags = userDataTags; + + snap.tags = + userData.tags?.map((tag) => { + const newTag = { + ...tag, + display: tag.name.replaceAll("_", " "), + personalBests: { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }, + }; + for (const mode of ["time", "words", "quote", "zen", "custom"]) { + newTag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {}; + } + return newTag; + }) ?? []; snap.tags = snap.tags?.sort((a, b) => { if (a.name > b.name) { @@ -304,7 +322,7 @@ export async function getUserResults(offset?: number): Promise { function _getCustomThemeById( themeID: string ): MonkeyTypes.CustomTheme | undefined { - return dbSnapshot?.customThemes.find((t) => t._id === themeID); + return dbSnapshot?.customThemes?.find((t) => t._id === themeID); } export async function addCustomTheme( @@ -312,6 +330,10 @@ export async function addCustomTheme( ): Promise { if (!dbSnapshot) return false; + if (dbSnapshot.customThemes === undefined) { + dbSnapshot.customThemes = []; + } + if (dbSnapshot.customThemes.length >= 10) { Notifications.add("Too many custom themes!", 0); return false; @@ -339,7 +361,11 @@ export async function editCustomTheme( if (!isAuthenticated()) return false; if (!dbSnapshot) return false; - const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId); + if (dbSnapshot.customThemes === undefined) { + dbSnapshot.customThemes = []; + } + + const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId); if (!customTheme) { Notifications.add( "Editing failed: Custom theme with id: " + themeId + " does not exist", @@ -369,7 +395,7 @@ export async function deleteCustomTheme(themeId: string): Promise { if (!isAuthenticated()) return false; if (!dbSnapshot) return false; - const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId); + const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId); if (!customTheme) return false; const response = await Ape.users.deleteCustomTheme(themeId); @@ -378,7 +404,7 @@ export async function deleteCustomTheme(themeId: string): Promise { return false; } - dbSnapshot.customThemes = dbSnapshot.customThemes.filter( + dbSnapshot.customThemes = dbSnapshot.customThemes?.filter( (t) => t._id !== themeId ); @@ -762,7 +788,7 @@ export async function saveLocalTagPB( function cont(): void { const filteredtag = dbSnapshot?.tags?.filter( (t) => t._id === tagId - )[0] as MonkeyTypes.Tag; + )[0] as MonkeyTypes.UserTag; filteredtag.personalBests ??= { time: {}, @@ -879,8 +905,14 @@ export async function updateLbMemory( if (snapshot.lbMemory[timeMode][timeMode2] === undefined) { snapshot.lbMemory[timeMode][timeMode2] = {}; } - const current = snapshot.lbMemory[timeMode][timeMode2][language]; - snapshot.lbMemory[timeMode][timeMode2][language] = rank; + const current = snapshot.lbMemory?.[timeMode]?.[timeMode2]?.[language]; + + //this is protected above so not sure why it would be undefined + const mem = snapshot.lbMemory[timeMode][timeMode2] as Record< + string, + number + >; + mem[language] = rank; if (api && current !== rank) { await Ape.users.updateLeaderboardMemory(mode, mode2, language, rank); } @@ -914,26 +946,17 @@ export function updateLocalStats(started: number, time: number): void { const snapshot = getSnapshot(); if (!snapshot) return; if (snapshot.typingStats === undefined) { - snapshot.typingStats = {} as MonkeyTypes.TypingStats; - } - if (snapshot?.typingStats !== undefined) { - if (snapshot.typingStats.timeTyping === undefined) { - snapshot.typingStats.timeTyping = time; - } else { - snapshot.typingStats.timeTyping += time; - } - if (snapshot.typingStats.startedTests === undefined) { - snapshot.typingStats.startedTests = started; - } else { - snapshot.typingStats.startedTests += started; - } - if (snapshot.typingStats.completedTests === undefined) { - snapshot.typingStats.completedTests = 1; - } else { - snapshot.typingStats.completedTests += 1; - } + snapshot.typingStats = { + timeTyping: 0, + startedTests: 0, + completedTests: 0, + }; } + snapshot.typingStats.timeTyping += time; + snapshot.typingStats.startedTests += started; + snapshot.typingStats.completedTests += 1; + setSnapshot(snapshot); } @@ -948,7 +971,7 @@ export function addXp(xp: number): void { setSnapshot(snapshot); } -export function addBadge(badge: MonkeyTypes.Badge): void { +export function addBadge(badge: SharedTypes.Badge): void { const snapshot = getSnapshot(); if (!snapshot) return; diff --git a/frontend/src/ts/elements/leaderboards.ts b/frontend/src/ts/elements/leaderboards.ts index d261b81abeee..712c6707c43a 100644 --- a/frontend/src/ts/elements/leaderboards.ts +++ b/frontend/src/ts/elements/leaderboards.ts @@ -246,7 +246,7 @@ function checkLbMemory(lb: LbKey): void { side = "right"; } - const memory = DB.getSnapshot()?.lbMemory?.time?.[lb]?.["english"] ?? 0; + const memory = DB.getSnapshot()?.lbMemory?.["time"]?.[lb]?.["english"] ?? 0; const rank = currentRank[lb]?.rank; if (rank) { diff --git a/frontend/src/ts/popups/edit-preset-popup.ts b/frontend/src/ts/popups/edit-preset-popup.ts index 25535a038aac..6c9830af631a 100644 --- a/frontend/src/ts/popups/edit-preset-popup.ts +++ b/frontend/src/ts/popups/edit-preset-popup.ts @@ -98,8 +98,8 @@ async function apply(): Promise { const tags = DB.getSnapshot()?.tags ?? []; const activeTagIds: string[] = tags - .filter((tag: MonkeyTypes.Tag) => tag.active) - .map((tag: MonkeyTypes.Tag) => tag._id); + .filter((tag: MonkeyTypes.UserTag) => tag.active) + .map((tag: MonkeyTypes.UserTag) => tag._id); configChanges.tags = activeTagIds; } diff --git a/frontend/src/ts/popups/edit-profile-popup.ts b/frontend/src/ts/popups/edit-profile-popup.ts index 9212807c6921..f0fff58dc3de 100644 --- a/frontend/src/ts/popups/edit-profile-popup.ts +++ b/frontend/src/ts/popups/edit-profile-popup.ts @@ -76,7 +76,7 @@ function hydrateInputs(): void { websiteInput.val(socialProfiles?.website ?? ""); badgeIdsSelect.html(""); - badges?.forEach((badge: MonkeyTypes.Badge) => { + badges?.forEach((badge: SharedTypes.Badge) => { if (badge.selected) { currentSelectedBadgeId = badge.id; } @@ -108,14 +108,14 @@ function hydrateInputs(): void { }); } -function buildUpdatesFromInputs(): MonkeyTypes.UserDetails { +function buildUpdatesFromInputs(): SharedTypes.UserProfileDetails { const bio = (bioInput.val() ?? "") as string; const keyboard = (keyboardInput.val() ?? "") as string; const twitter = (twitterInput.val() ?? "") as string; const github = (githubInput.val() ?? "") as string; const website = (websiteInput.val() ?? "") as string; - const profileUpdates: MonkeyTypes.UserDetails = { + const profileUpdates: SharedTypes.UserProfileDetails = { bio, keyboard, socialProfiles: { diff --git a/frontend/src/ts/popups/quote-search-popup.ts b/frontend/src/ts/popups/quote-search-popup.ts index 4f0a93ea683e..2ed79126fcf1 100644 --- a/frontend/src/ts/popups/quote-search-popup.ts +++ b/frontend/src/ts/popups/quote-search-popup.ts @@ -230,7 +230,10 @@ export async function show(clearText = true): Promise { $("#quoteSearchPopup #toggleShowFavorites").removeClass("hidden"); } - if (DB.getSnapshot()?.quoteMod) { + const isQuoteMod = + DB.getSnapshot()?.quoteMod === true || DB.getSnapshot()?.quoteMod !== ""; + + if (isQuoteMod) { $("#quoteSearchPopup #goToApproveQuotes").removeClass("hidden"); } else { $("#quoteSearchPopup #goToApproveQuotes").addClass("hidden"); @@ -431,10 +434,10 @@ $("#popups").on( if (response.status === 200) { $button.removeClass("fas").addClass("far"); - const quoteIndex = dbSnapshot.favoriteQuotes[quoteLang]?.indexOf( + const quoteIndex = dbSnapshot.favoriteQuotes?.[quoteLang]?.indexOf( quoteId ) as number; - dbSnapshot.favoriteQuotes[quoteLang]?.splice(quoteIndex, 1); + dbSnapshot.favoriteQuotes?.[quoteLang]?.splice(quoteIndex, 1); } } else { // Add to favorites @@ -446,6 +449,9 @@ $("#popups").on( if (response.status === 200) { $button.removeClass("far").addClass("fas"); + if (dbSnapshot.favoriteQuotes === undefined) { + dbSnapshot.favoriteQuotes = {}; + } if (!dbSnapshot.favoriteQuotes[quoteLang]) { dbSnapshot.favoriteQuotes[quoteLang] = []; } diff --git a/frontend/src/ts/popups/simple-popups.ts b/frontend/src/ts/popups/simple-popups.ts index 41ea889dee5d..b901cc22d4d3 100644 --- a/frontend/src/ts/popups/simple-popups.ts +++ b/frontend/src/ts/popups/simple-popups.ts @@ -1539,7 +1539,7 @@ list.updateCustomTheme = new SimplePopup( }; } - const customTheme = snapshot.customThemes.find( + const customTheme = snapshot.customThemes?.find( (t) => t._id === _thisPopup.parameters[0] ); if (customTheme === undefined) { @@ -1585,7 +1585,7 @@ list.updateCustomTheme = new SimplePopup( const snapshot = DB.getSnapshot(); if (!snapshot) return; - const customTheme = snapshot.customThemes.find( + const customTheme = snapshot.customThemes?.find( (t) => t._id === _thisPopup.parameters[0] ); if (!customTheme) return; diff --git a/frontend/src/ts/settings/theme-picker.ts b/frontend/src/ts/settings/theme-picker.ts index 427c9e2abc43..03d71dfbf316 100644 --- a/frontend/src/ts/settings/theme-picker.ts +++ b/frontend/src/ts/settings/theme-picker.ts @@ -341,7 +341,7 @@ $(".pageSettings").on("click", " .section.themes .customTheme.button", (e) => { if ($(e.target).hasClass("delButton")) return; if ($(e.target).hasClass("editButton")) return; const customThemeId = $(e.currentTarget).attr("customThemeId") ?? ""; - const theme = DB.getSnapshot()?.customThemes.find( + const theme = DB.getSnapshot()?.customThemes?.find( (e) => e._id === customThemeId ); diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index cc6906164b42..6289e2088338 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -425,7 +425,7 @@ export async function updateCrown(): Promise { } async function updateTags(dontSave: boolean): Promise { - const activeTags: MonkeyTypes.Tag[] = []; + const activeTags: MonkeyTypes.UserTag[] = []; const userTagsCount = DB.getSnapshot()?.tags?.length ?? 0; try { DB.getSnapshot()?.tags?.forEach((tag) => { @@ -893,10 +893,10 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => { if (response.status === 200) { $button.removeClass("fas").addClass("far"); - const quoteIndex = dbSnapshot.favoriteQuotes[quoteLang]?.indexOf( + const quoteIndex = dbSnapshot.favoriteQuotes?.[quoteLang]?.indexOf( quoteId ) as number; - dbSnapshot.favoriteQuotes[quoteLang]?.splice(quoteIndex, 1); + dbSnapshot.favoriteQuotes?.[quoteLang]?.splice(quoteIndex, 1); } } else { // Add to favorites @@ -908,6 +908,9 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => { if (response.status === 200) { $button.removeClass("far").addClass("fas"); + if (dbSnapshot.favoriteQuotes === undefined) { + dbSnapshot.favoriteQuotes = {}; + } if (!dbSnapshot.favoriteQuotes[quoteLang]) { dbSnapshot.favoriteQuotes[quoteLang] = []; } diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index fe64adfaefb5..073a9e676100 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -170,14 +170,6 @@ declare namespace MonkeyTypes { display: string; }; - type Tag = { - _id: string; - name: string; - display: string; - personalBests: SharedTypes.PersonalBests; - active?: boolean; - }; - type RawCustomTheme = { name: string; colors: string[]; @@ -187,12 +179,6 @@ declare namespace MonkeyTypes { _id: string; } & RawCustomTheme; - type TypingStats = { - timeTyping: number; - startedTests: number; - completedTests: number; - }; - type ConfigChanges = { tags?: string[]; } & Partial; @@ -211,60 +197,41 @@ declare namespace MonkeyTypes { type QuoteRatings = Record>; - type Snapshot = { - banned?: boolean; - emailVerified?: boolean; - quoteRatings?: QuoteRatings; - results?: SharedTypes.Result[]; - verified?: boolean; - personalBests: SharedTypes.PersonalBests; - name: string; - customThemes: CustomTheme[]; - presets?: SnapshotPreset[]; - tags: Tag[]; - favouriteThemes?: string[]; - lbMemory?: LeaderboardMemory; - typingStats?: TypingStats; - quoteMod?: boolean; - discordId?: string; - config?: SharedTypes.Config; - favoriteQuotes: FavoriteQuotes; - needsToChangeName?: boolean; - discordAvatar?: string; - details?: UserDetails; - inventory?: UserInventory; - addedAt: number; - filterPresets: SharedTypes.ResultFilters[]; - xp: number; + type UserTag = SharedTypes.UserTag & { + active?: boolean; + display: string; + }; + + type Snapshot = Omit< + SharedTypes.User, + | "timeTyping" + | "startedTests" + | "completedTests" + | "profileDetails" + | "streak" + | "resultFilterPresets" + | "tags" + | "xp" + > & { + typingStats: { + timeTyping: number; + startedTests: number; + completedTests: number; + }; + details?: SharedTypes.UserProfileDetails; inboxUnreadSize: number; streak: number; maxStreak: number; + filterPresets: SharedTypes.ResultFilters[]; + isPremium: boolean; streakHourOffset?: number; - lbOptOut?: boolean; - isPremium?: boolean; - }; - - type UserDetails = { - bio?: string; - keyboard?: string; - socialProfiles: { - twitter?: string; - github?: string; - website?: string; - }; - }; - - type UserInventory = { - badges: Badge[]; - }; - - type Badge = { - id: number; - selected?: boolean; + config: SharedTypes.Config; + tags: UserTag[]; + presets: SnapshotPreset[]; + results?: SharedTypes.Result[]; + xp: number; }; - type FavoriteQuotes = Record; - type Group< G extends keyof SharedTypes.ResultFilters = keyof SharedTypes.ResultFilters > = G extends G ? SharedTypes.ResultFilters[G] : never; @@ -466,8 +433,8 @@ declare namespace MonkeyTypes { type BadgeReward = { type: "badge"; - item: Badge; - } & Reward; + item: SharedTypes.Badge; + } & Reward; type AllRewards = XpReward | BadgeReward; diff --git a/frontend/webpack/config.base.js b/frontend/webpack/config.base.js index 7fea9c2ce54d..f554ee440cb7 100644 --- a/frontend/webpack/config.base.js +++ b/frontend/webpack/config.base.js @@ -35,7 +35,13 @@ const BASE_CONFIG = { }, module: { rules: [ - { test: /\.tsx?$/, loader: "ts-loader" }, + { + test: /\.tsx?$/, + loader: "ts-loader", + options: { + transpileOnly: true, + }, + }, { test: /\.s[ac]ss$/i, use: [ diff --git a/shared-types/types.d.ts b/shared-types/types.d.ts index 68d258354697..c82329cdd122 100644 --- a/shared-types/types.d.ts +++ b/shared-types/types.d.ts @@ -459,4 +459,81 @@ declare namespace SharedTypes { xpBreakdown: Record; streak: number; }; + + type UserStreak = { + lastResultTimestamp: number; + length: number; + maxLength: number; + hourOffset?: number; + }; + + type UserTag = { + _id: string; + name: string; + personalBests: PersonalBests; + }; + + type UserProfileDetails = { + bio: string; + keyboard: string; + socialProfiles: { + twitter: string; + github: string; + website: string; + }; + }; + + type CustomTheme = { + _id: string; + name: string; + colors: string[]; + }; + + type PremiumInfo = { + startTimestamp: number; + expirationTimestamp: number; + }; + + type UserQuoteRatings = Record>; + + type UserLbMemory = Record>>; + + type UserInventory = { + badges: Badge[]; + }; + + type Badge = { + id: number; + selected?: boolean; + }; + + type User = { + name: string; + email: string; + uid: string; + addedAt: number; + personalBests: PersonalBests; + lastReultHashes?: string[]; //todo: fix typo (its in the db too) + completedTests?: number; + startedTests?: number; + timeTyping?: number; + streak?: UserStreak; + xp?: number; + discordId?: string; + discordAvatar?: string; + tags?: UserTag[]; + profileDetails?: UserProfileDetails; + customThemes?: CustomTheme[]; + premium?: PremiumInfo; + quoteRatings?: UserQuoteRatings; + favoriteQuotes?: Record; + lbMemory?: UserLbMemory; + inventory?: UserInventory; + banned?: boolean; + lbOptOut?: boolean; + verified?: boolean; + needsToChangeName?: boolean; + quoteMod?: boolean | string; + resultFilterPresets?: ResultFilters[]; + }; } From 90f0c0706dfc076865bf31437499376ca5283a57 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 01:56:20 +0100 Subject: [PATCH 02/20] profiles, mail and others --- backend/src/api/controllers/user.ts | 2 +- backend/src/dal/user.ts | 8 +-- backend/src/types/types.d.ts | 30 +---------- backend/src/utils/monkey-mail.ts | 4 +- backend/src/workers/later-worker.ts | 4 +- frontend/src/ts/ape/endpoints/users.ts | 72 ++++++++++++++------------ frontend/src/ts/ape/types/ape.d.ts | 2 +- frontend/src/ts/ape/types/users.d.ts | 10 ++++ frontend/src/ts/elements/profile.ts | 42 ++++++--------- frontend/src/ts/pages/profile.ts | 21 +++++--- shared-types/types.d.ts | 61 ++++++++++++++++++++++ 11 files changed, 151 insertions(+), 105 deletions(-) diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index fc611ed01ec3..ef74b65150fc 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -785,7 +785,7 @@ export async function getProfile( details: profileDetails, allTimeLbs: alltimelbs, uid: user.uid, - }; + } as SharedTypes.UserProfile; return new MonkeyResponse("Profile retrieved", profileData); } diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 327be2ce3a0e..f41a6fb84d89 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -852,7 +852,7 @@ export async function getInbox( type AddToInboxBulkEntry = { uid: string; - mail: MonkeyTypes.MonkeyMail[]; + mail: SharedTypes.MonkeyMail[]; }; export async function addToInboxBulk( @@ -884,7 +884,7 @@ export async function addToInboxBulk( export async function addToInbox( uid: string, - mail: MonkeyTypes.MonkeyMail[], + mail: SharedTypes.MonkeyMail[], inboxConfig: SharedTypes.Configuration["users"]["inbox"] ): Promise { const { enabled, maxMail } = inboxConfig; @@ -910,7 +910,7 @@ export async function addToInbox( } function buildRewardUpdates( - rewards: MonkeyTypes.AllRewards[], + rewards: SharedTypes.AllRewards[], inventoryIsNull = false ): UpdateFilter { let totalXp = 0; @@ -962,7 +962,7 @@ export async function updateInbox( const mailToReadSet = new Set(mailToRead); const mailToDeleteSet = new Set(mailToDelete); - const allRewards: MonkeyTypes.AllRewards[] = []; + const allRewards: SharedTypes.AllRewards[] = []; const newInbox = inbox .filter((mail) => { diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index 4029e693d832..66a19b1bc071 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -18,34 +18,6 @@ declare namespace MonkeyTypes { ctx: Readonly; } & ExpressRequest; - // Data Model - - type Reward = { - type: string; - item: T; - }; - - type XpReward = { - type: "xp"; - item: number; - } & Reward; - - type BadgeReward = { - type: "badge"; - item: SharedTypes.Badge; - } & Reward; - - type AllRewards = XpReward | BadgeReward; - - type MonkeyMail = { - id: string; - subject: string; - body: string; - timestamp: number; - read: boolean; - rewards: AllRewards[]; - }; - type DBUser = Omit< SharedTypes.User, "resultFilterPresets" | "tags" | "customThemes" @@ -56,7 +28,7 @@ declare namespace MonkeyTypes { lbPersonalBests?: LbPersonalBests; customThemes?: DBCustomTheme[]; autoBanTimestamps?: number[]; - inbox?: MonkeyMail[]; + inbox?: SharedTypes.MonkeyMail[]; ips?: string[]; canReport?: boolean; lastNameChange?: number; diff --git a/backend/src/utils/monkey-mail.ts b/backend/src/utils/monkey-mail.ts index 095154d1f7d4..ceeb2d8bd42b 100644 --- a/backend/src/utils/monkey-mail.ts +++ b/backend/src/utils/monkey-mail.ts @@ -1,10 +1,10 @@ import { v4 } from "uuid"; -type MonkeyMailOptions = Partial>; +type MonkeyMailOptions = Partial>; export function buildMonkeyMail( options: MonkeyMailOptions -): MonkeyTypes.MonkeyMail { +): SharedTypes.MonkeyMail { return { id: v4(), subject: options.subject ?? "", diff --git a/backend/src/workers/later-worker.ts b/backend/src/workers/later-worker.ts index 3cbc33f76dbe..0ac87cc594a6 100644 --- a/backend/src/workers/later-worker.ts +++ b/backend/src/workers/later-worker.ts @@ -43,7 +43,7 @@ async function handleDailyLeaderboardResults( if (inboxConfig.enabled && xpRewardBrackets.length > 0) { const mailEntries: { uid: string; - mail: MonkeyTypes.MonkeyMail[]; + mail: SharedTypes.MonkeyMail[]; }[] = []; allResults.forEach((entry) => { @@ -132,7 +132,7 @@ async function handleWeeklyXpLeaderboardResults( const mailEntries: { uid: string; - mail: MonkeyTypes.MonkeyMail[]; + mail: SharedTypes.MonkeyMail[]; }[] = []; allResults.forEach((entry) => { diff --git a/frontend/src/ts/ape/endpoints/users.ts b/frontend/src/ts/ape/endpoints/users.ts index f617383a8155..e4efff2799a8 100644 --- a/frontend/src/ts/ape/endpoints/users.ts +++ b/frontend/src/ts/ape/endpoints/users.ts @@ -14,7 +14,7 @@ export default class Users { captcha: string, email?: string, uid?: string - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { email, name, @@ -25,23 +25,23 @@ export default class Users { return await this.httpClient.post(`${BASE_PATH}/signup`, { payload }); } - async getNameAvailability(name: string): Ape.EndpointResponse { + async getNameAvailability(name: string): Ape.EndpointResponse { return await this.httpClient.get(`${BASE_PATH}/checkName/${name}`); } - async delete(): Ape.EndpointResponse { + async delete(): Ape.EndpointResponse { return await this.httpClient.delete(BASE_PATH); } - async reset(): Ape.EndpointResponse { + async reset(): Ape.EndpointResponse { return await this.httpClient.patch(`${BASE_PATH}/reset`); } - async optOutOfLeaderboards(): Ape.EndpointResponse { + async optOutOfLeaderboards(): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/optOutOfLeaderboards`); } - async updateName(name: string): Ape.EndpointResponse { + async updateName(name: string): Ape.EndpointResponse { return await this.httpClient.patch(`${BASE_PATH}/name`, { payload: { name }, }); @@ -52,7 +52,7 @@ export default class Users { mode2: SharedTypes.Config.Mode2, language: string, rank: number - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { mode, mode2, @@ -68,7 +68,7 @@ export default class Users { async updateEmail( newEmail: string, previousEmail: string - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { newEmail, previousEmail, @@ -77,31 +77,31 @@ export default class Users { return await this.httpClient.patch(`${BASE_PATH}/email`, { payload }); } - async deletePersonalBests(): Ape.EndpointResponse { + async deletePersonalBests(): Ape.EndpointResponse { return await this.httpClient.delete(`${BASE_PATH}/personalBests`); } async addResultFilterPreset( filter: SharedTypes.ResultFilters - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/resultFilterPresets`, { payload: filter, }); } - async removeResultFilterPreset(id: string): Ape.EndpointResponse { + async removeResultFilterPreset(id: string): Ape.EndpointResponse { return await this.httpClient.delete( `${BASE_PATH}/resultFilterPresets/${id}` ); } - async createTag(tagName: string): Ape.EndpointResponse { + async createTag(tagName: string): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/tags`, { payload: { tagName }, }); } - async editTag(tagId: string, newName: string): Ape.EndpointResponse { + async editTag(tagId: string, newName: string): Ape.EndpointResponse { const payload = { tagId, newName, @@ -110,24 +110,24 @@ export default class Users { return await this.httpClient.patch(`${BASE_PATH}/tags`, { payload }); } - async deleteTag(tagId: string): Ape.EndpointResponse { + async deleteTag(tagId: string): Ape.EndpointResponse { return await this.httpClient.delete(`${BASE_PATH}/tags/${tagId}`); } - async deleteTagPersonalBest(tagId: string): Ape.EndpointResponse { + async deleteTagPersonalBest(tagId: string): Ape.EndpointResponse { return await this.httpClient.delete( `${BASE_PATH}/tags/${tagId}/personalBest` ); } - async getCustomThemes(): Ape.EndpointResponse { + async getCustomThemes(): Ape.EndpointResponse { return await this.httpClient.get(`${BASE_PATH}/customThemes`); } async editCustomTheme( themeId: string, newTheme: Partial - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { themeId: themeId, theme: { @@ -140,7 +140,7 @@ export default class Users { }); } - async deleteCustomTheme(themeId: string): Ape.EndpointResponse { + async deleteCustomTheme(themeId: string): Ape.EndpointResponse { const payload = { themeId: themeId, }; @@ -151,12 +151,12 @@ export default class Users { async addCustomTheme( newTheme: Partial - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { name: newTheme.name, colors: newTheme.colors }; return await this.httpClient.post(`${BASE_PATH}/customThemes`, { payload }); } - async getOauthLink(): Ape.EndpointResponse { + async getOauthLink(): Ape.EndpointResponse { return await this.httpClient.get(`${BASE_PATH}/discord/oauth`); } @@ -164,20 +164,20 @@ export default class Users { tokenType: string, accessToken: string, state: string - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/discord/link`, { payload: { tokenType, accessToken, state }, }); } - async unlinkDiscord(): Ape.EndpointResponse { + async unlinkDiscord(): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/discord/unlink`); } async addQuoteToFavorites( language: string, quoteId: string - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { language, quoteId }; return await this.httpClient.post(`${BASE_PATH}/favoriteQuotes`, { payload, @@ -187,25 +187,29 @@ export default class Users { async removeQuoteFromFavorites( language: string, quoteId: string - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { language, quoteId }; return await this.httpClient.delete(`${BASE_PATH}/favoriteQuotes`, { payload, }); } - async getProfileByUid(uid: string): Promise { + async getProfileByUid( + uid: string + ): Ape.EndpointResponse { return await this.httpClient.get(`${BASE_PATH}/${uid}/profile?isUid`); } - async getProfileByName(name: string): Promise { + async getProfileByName( + name: string + ): Ape.EndpointResponse { return await this.httpClient.get(`${BASE_PATH}/${name}/profile`); } async updateProfile( profileUpdates: Partial, selectedBadgeId?: number - ): Promise { + ): Ape.EndpointResponse { return await this.httpClient.patch(`${BASE_PATH}/profile`, { payload: { ...profileUpdates, @@ -214,14 +218,14 @@ export default class Users { }); } - async getInbox(): Promise { + async getInbox(): Ape.EndpointResponse { return await this.httpClient.get(`${BASE_PATH}/inbox`); } async updateInbox(options: { mailIdsToDelete?: string[]; mailIdsToMarkRead?: string[]; - }): Promise { + }): Ape.EndpointResponse { const payload = { mailIdsToDelete: options.mailIdsToDelete, mailIdsToMarkRead: options.mailIdsToMarkRead, @@ -234,7 +238,7 @@ export default class Users { reason: string, comment: string, captcha: string - ): Ape.EndpointResponse { + ): Ape.EndpointResponse { const payload = { uid, reason, @@ -245,23 +249,23 @@ export default class Users { return await this.httpClient.post(`${BASE_PATH}/report`, { payload }); } - async verificationEmail(): Ape.EndpointResponse { + async verificationEmail(): Ape.EndpointResponse { return await this.httpClient.get(`${BASE_PATH}/verificationEmail`); } - async forgotPasswordEmail(email: string): Ape.EndpointResponse { + async forgotPasswordEmail(email: string): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/forgotPasswordEmail`, { payload: { email }, }); } - async setStreakHourOffset(hourOffset: number): Ape.EndpointResponse { + async setStreakHourOffset(hourOffset: number): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/setStreakHourOffset`, { payload: { hourOffset }, }); } - async revokeAllTokens(): Ape.EndpointResponse { + async revokeAllTokens(): Ape.EndpointResponse { return await this.httpClient.post(`${BASE_PATH}/revokeAllTokens`); } } diff --git a/frontend/src/ts/ape/types/ape.d.ts b/frontend/src/ts/ape/types/ape.d.ts index f25054bd8283..65e023ff6274 100644 --- a/frontend/src/ts/ape/types/ape.d.ts +++ b/frontend/src/ts/ape/types/ape.d.ts @@ -27,7 +27,7 @@ declare namespace Ape { }; // todo: remove any after all ape endpoints are typed - type EndpointResponse = Promise>; + type EndpointResponse = Promise>; type HttpClient = { get: HttpClientMethod; diff --git a/frontend/src/ts/ape/types/users.d.ts b/frontend/src/ts/ape/types/users.d.ts index 125b7bfa5a19..44e0f8b6be90 100644 --- a/frontend/src/ts/ape/types/users.d.ts +++ b/frontend/src/ts/ape/types/users.d.ts @@ -5,4 +5,14 @@ declare namespace Ape.Users { inboxUnreadSize: number; isPremium: boolean; }; + type GetOauthLink = { + url: string; + }; + type LinkDiscord = { + discordId: string; + discordAvatar: string; + }; + type GetInbox = { + inbox: SharedTypes.MonkeyMail[] | undefined; + }; } diff --git a/frontend/src/ts/elements/profile.ts b/frontend/src/ts/elements/profile.ts index e973e4406336..a6f4793b0baf 100644 --- a/frontend/src/ts/elements/profile.ts +++ b/frontend/src/ts/elements/profile.ts @@ -9,15 +9,13 @@ import * as ActivePage from "../states/active-page"; import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict"; type ProfileViewPaths = "profile" | "account"; +type UserProfileOrSnapshot = SharedTypes.UserProfile | MonkeyTypes.Snapshot; -export type ProfileData = { - allTimeLbs: MonkeyTypes.LeaderboardMemory; - uid: string; -} & MonkeyTypes.Snapshot; +//this is probably the dirtiest code ive ever written export async function update( where: ProfileViewPaths, - profile: Partial + profile: UserProfileOrSnapshot ): Promise { const elementClass = where.charAt(0).toUpperCase() + where.slice(1); const profileElement = $(`.page${elementClass} .profile`); @@ -140,10 +138,11 @@ export async function update( const results = DB.getSnapshot()?.results; const lastResult = results?.[0]; + const streakOffset = (profile as MonkeyTypes.Snapshot).streakHourOffset; + const dayInMilis = 1000 * 60 * 60 * 24; - let target = - Misc.getCurrentDayTimestamp(profile.streakHourOffset) + dayInMilis; + let target = Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis; if (target < Date.now()) { target += dayInMilis; } @@ -154,9 +153,7 @@ export async function update( console.debug("dayInMilis", dayInMilis); console.debug( "difTarget", - new Date( - Misc.getCurrentDayTimestamp(profile.streakHourOffset) + dayInMilis - ) + new Date(Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis) ); console.debug("timeDif", timeDif); console.debug( @@ -164,18 +161,12 @@ export async function update( Misc.getCurrentDayTimestamp(), new Date(Misc.getCurrentDayTimestamp()) ); - console.debug("profile.streakHourOffset", profile.streakHourOffset); + console.debug("profile.streakHourOffset", streakOffset); if (lastResult) { //check if the last result is from today - const isToday = Misc.isToday( - lastResult.timestamp, - profile.streakHourOffset - ); - const isYesterday = Misc.isYesterday( - lastResult.timestamp, - profile.streakHourOffset - ); + const isToday = Misc.isToday(lastResult.timestamp, streakOffset); + const isYesterday = Misc.isYesterday(lastResult.timestamp, streakOffset); console.debug( "lastResult.timestamp", @@ -185,10 +176,8 @@ export async function update( console.debug("isToday", isToday); console.debug("isYesterday", isYesterday); - const offsetString = profile.streakHourOffset - ? `(${profile.streakHourOffset > 0 ? "+" : ""}${ - profile.streakHourOffset - } offset)` + const offsetString = streakOffset + ? `(${streakOffset > 0 ? "+" : ""}${streakOffset} offset)` : ""; if (isToday) { @@ -201,7 +190,7 @@ export async function update( console.debug(hoverText); - if (profile.streakHourOffset === undefined) { + if (streakOffset === undefined) { hoverText += `\n\nIf the streak reset time doesn't line up with your timezone, you can change it in Settings > Danger zone > Update streak hour offset.`; } } @@ -322,7 +311,10 @@ export async function update( } else { profileElement.find(".leaderboardsPositions").removeClass("hidden"); - const lbPos = where === "profile" ? profile.allTimeLbs : profile.lbMemory; + const lbPos = + where === "profile" + ? (profile as SharedTypes.UserProfile).allTimeLbs + : (profile as MonkeyTypes.Snapshot).lbMemory; const t15 = lbPos?.time?.["15"]?.["english"]; const t60 = lbPos?.time?.["60"]?.["english"]; diff --git a/frontend/src/ts/pages/profile.ts b/frontend/src/ts/pages/profile.ts index a01cd8730b32..034c2f6de092 100644 --- a/frontend/src/ts/pages/profile.ts +++ b/frontend/src/ts/pages/profile.ts @@ -151,7 +151,7 @@ function reset(): void { type UpdateOptions = { uidOrName?: string; - data?: undefined | Profile.ProfileData; + data?: undefined | SharedTypes.UserProfile; }; async function update(options: UpdateOptions): Promise { @@ -159,14 +159,18 @@ async function update(options: UpdateOptions): Promise { if (options.data) { $(".page.pageProfile .preloader").addClass("hidden"); await Profile.update("profile", options.data); - PbTables.update(options.data.personalBests, true); + PbTables.update( + // this cast is fine because pb tables can handle the partial data inside user profiles + options.data.personalBests as unknown as SharedTypes.PersonalBests, + true + ); } else if (options.uidOrName !== undefined && options.uidOrName !== "") { const response = getParamExists ? await Ape.users.getProfileByUid(options.uidOrName) : await Ape.users.getProfileByName(options.uidOrName); $(".page.pageProfile .preloader").addClass("hidden"); - if (response.status === 404) { + if (response.status === 404 || response.data === null) { const message = getParamExists ? "User not found" : `User ${options.uidOrName} not found`; @@ -181,10 +185,13 @@ async function update(options: UpdateOptions): Promise { ); } else { window.history.replaceState(null, "", `/profile/${response.data.name}`); + await Profile.update("profile", response.data); + // this cast is fine because pb tables can handle the partial data inside user profiles + PbTables.update( + response.data.personalBests as unknown as SharedTypes.PersonalBests, + true + ); } - - await Profile.update("profile", response.data); - PbTables.update(response.data.personalBests, true); } else { Notifications.add("Missing update parameter!", -1); } @@ -199,7 +206,7 @@ $(".page.pageProfile").on("click", ".profile .userReportButton", () => { void UserReportPopup.show({ uid, name, lbOptOut }); }); -export const page = new Page( +export const page = new Page( "profile", $(".page.pageProfile"), "/profile", diff --git a/shared-types/types.d.ts b/shared-types/types.d.ts index c82329cdd122..227d6f73f899 100644 --- a/shared-types/types.d.ts +++ b/shared-types/types.d.ts @@ -536,4 +536,65 @@ declare namespace SharedTypes { quoteMod?: boolean | string; resultFilterPresets?: ResultFilters[]; }; + + type Reward = { + type: string; + item: T; + }; + + type XpReward = { + type: "xp"; + item: number; + } & Reward; + + type BadgeReward = { + type: "badge"; + item: SharedTypes.Badge; + } & Reward; + + type AllRewards = XpReward | BadgeReward; + + type MonkeyMail = { + id: string; + subject: string; + body: string; + timestamp: number; + read: boolean; + rewards: AllRewards[]; + }; + + type UserProfile = Pick< + User, + | "name" + | "banned" + | "addedAt" + | "discordId" + | "discordAvatar" + | "xp" + | "lbOptOut" + | "inventory" + | "uid" + > & { + typingStats: { + completedTests: User["completedTests"]; + startedTests: User["startedTests"]; + timeTyping: User["timeTyping"]; + }; + streak: UserStreak["length"]; + maxStreak: UserStreak["maxLength"]; + details: UserProfileDetails; + allTimeLbs: { + time: Record>; + }; + personalBests: { + time: Pick< + Record<`${number}`, SharedTypes.PersonalBest[]>, + "15" | "30" | "60" | "120" + >; + words: Pick< + Record<`${number}`, SharedTypes.PersonalBest[]>, + "10" | "25" | "50" | "100" + >; + }; + }; } From 5e896be642e64b0e28a6042318f3dfbf3f323b33 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 14:56:58 +0100 Subject: [PATCH 03/20] fix logic --- backend/src/api/routes/quotes.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/api/routes/quotes.ts b/backend/src/api/routes/quotes.ts index 65a6836b5423..a4e122ac7f51 100644 --- a/backend/src/api/routes/quotes.ts +++ b/backend/src/api/routes/quotes.ts @@ -14,7 +14,10 @@ const router = Router(); const checkIfUserIsQuoteMod = checkUserPermissions({ criteria: (user) => { - return !!user.quoteMod; + return ( + user.quoteMod === true || + (typeof user.quoteMod === "string" && user.quoteMod !== "") + ); }, }); From f3c724d48d611966e22b9f1e7244ed58f2ab9a49 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 16:03:13 +0100 Subject: [PATCH 04/20] yeet --- backend/src/types/types.d.ts | 40 ------------------------------------ 1 file changed, 40 deletions(-) diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index 66a19b1bc071..ea5fac8ba40d 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -39,46 +39,6 @@ declare namespace MonkeyTypes { type DBUserTag = WithObjectId; - // type User = { - // autoBanTimestamps?: number[]; - // addedAt: number; - // verified?: boolean; - // bananas?: number; - // completedTests?: number; - // discordId?: string; - // email: string; - // lastNameChange?: number; - // lbMemory?: object; - // lbPersonalBests?: LbPersonalBests; - // name: string; - // customThemes?: MonkeyTypes.WithObjectIdArray; - // personalBests: SharedTypes.PersonalBests; - // quoteRatings?: SharedTypes.UserQuoteRatings; - // startedTests?: number; - // tags?: MonkeyTypes.WithObjectIdArray; - // timeTyping?: number; - // uid: string; - // quoteMod?: boolean; - // configurationMod?: boolean; - // admin?: boolean; - // canReport?: boolean; - // banned?: boolean; - // canManageApeKeys?: boolean; - // favoriteQuotes?: Record; - // needsToChangeName?: boolean; - // discordAvatar?: string; - // resultFilterPresets?: WithObjectIdArray; - // profileDetails?: SharedTypes.UserProfileDetails; - // inventory?: SharedTypes.UserInventory; - // xp?: number; - // inbox?: MonkeyMail[]; - // streak?: SharedTypes.UserStreak; - // lastReultHashes?: string[]; - // lbOptOut?: boolean; - // premium?: SharedTypes.PremiumInfo; - // ips?: UserIpHistory; - // }; - type LbPersonalBests = { time: Record>; }; From 34c588d2607c870d7a30bed63dafd62f885c71e3 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 16:19:49 +0100 Subject: [PATCH 05/20] same as master for now --- shared-types/types.d.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shared-types/types.d.ts b/shared-types/types.d.ts index 227d6f73f899..6d7370f678bb 100644 --- a/shared-types/types.d.ts +++ b/shared-types/types.d.ts @@ -474,12 +474,12 @@ declare namespace SharedTypes { }; type UserProfileDetails = { - bio: string; - keyboard: string; + bio?: string; + keyboard?: string; socialProfiles: { - twitter: string; - github: string; - website: string; + twitter?: string; + github?: string; + website?: string; }; }; From c1febfc2e1fd311473a640a4da0ecd2727e059f4 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 16:28:53 +0100 Subject: [PATCH 06/20] tsc fixes --- frontend/src/ts/account/result-filters.ts | 2 +- frontend/src/ts/ape/utils.ts | 2 +- frontend/src/ts/db.ts | 7 +++- frontend/src/ts/pages/settings.ts | 2 +- frontend/src/ts/popups/edit-tags-popup.ts | 6 ++++ frontend/src/ts/popups/simple-popups.ts | 43 ++++++++++------------- frontend/src/ts/utils/url-handler.ts | 7 ++++ 7 files changed, 40 insertions(+), 29 deletions(-) diff --git a/frontend/src/ts/account/result-filters.ts b/frontend/src/ts/account/result-filters.ts index b2e3dbce5d08..46f3f52474ae 100644 --- a/frontend/src/ts/account/result-filters.ts +++ b/frontend/src/ts/account/result-filters.ts @@ -217,7 +217,7 @@ async function createFilterPresetCallback(name: string): Promise { const result = await Ape.users.addResultFilterPreset({ ...filters, name }); Loader.hide(); if (result.status === 200) { - addFilterPresetToSnapshot({ ...filters, name, _id: result.data }); + addFilterPresetToSnapshot({ ...filters, name, _id: result.data as string }); void updateFilterPresets(); Notifications.add("Filter preset created", 1); } else { diff --git a/frontend/src/ts/ape/utils.ts b/frontend/src/ts/ape/utils.ts index ecbe84cb362a..81bce25e5f49 100644 --- a/frontend/src/ts/ape/utils.ts +++ b/frontend/src/ts/ape/utils.ts @@ -22,7 +22,7 @@ const DEFAULT_RETRY_OPTIONS: Required = { export async function withRetry( fn: () => Ape.EndpointResponse, opts?: RetryOptions -): Ape.EndpointResponse { +): Ape.EndpointResponse { const retry = async ( previousData: Ape.HttpClientResponse, completeOpts: Required> diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index b5da91eb53db..5847d5383479 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -345,9 +345,14 @@ export async function addCustomTheme( return false; } + if (response.data === null) { + Notifications.add("Error adding custom theme: No data returned", -1); + return false; + } + const newCustomTheme: MonkeyTypes.CustomTheme = { ...theme, - _id: response.data.theme._id as string, + _id: response.data._id as string, }; dbSnapshot.customThemes.push(newCustomTheme); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 535b217294eb..947152593288 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -1264,7 +1264,7 @@ $(".pageSettings .section.discordIntegration .getLinkAndGoToOauth").on( "click", () => { void Ape.users.getOauthLink().then((res) => { - window.open(res.data.url, "_self"); + window.open(res.data?.url as string, "_self"); }); } ); diff --git a/frontend/src/ts/popups/edit-tags-popup.ts b/frontend/src/ts/popups/edit-tags-popup.ts index bdab0d803e7c..fb91c471d38f 100644 --- a/frontend/src/ts/popups/edit-tags-popup.ts +++ b/frontend/src/ts/popups/edit-tags-popup.ts @@ -100,6 +100,12 @@ async function apply(): Promise { -1 ); } else { + if (response.data === null) { + Notifications.add("Tag was added but data returned was null", -1); + Loader.hide(); + return; + } + Notifications.add("Tag added", 1); DB.getSnapshot()?.tags?.push({ display: propTagName, diff --git a/frontend/src/ts/popups/simple-popups.ts b/frontend/src/ts/popups/simple-popups.ts index b901cc22d4d3..27bba85b5fb5 100644 --- a/frontend/src/ts/popups/simple-popups.ts +++ b/frontend/src/ts/popups/simple-popups.ts @@ -1049,35 +1049,28 @@ list.clearTagPb = new SimplePopup( }; } - if (response.data.resultCode === 1) { - const tag = DB.getSnapshot()?.tags?.filter((t) => t._id === tagId)[0]; - - if (tag === undefined) { - return { - status: -1, - message: "Tag not found", - }; - } - tag.personalBests = { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; - $( - `.pageSettings .section.tags .tagsList .tag[id="${tagId}"] .clearPbButton` - ).attr("aria-label", "No PB found"); - return { - status: 1, - message: "Tag PB cleared", - }; - } else { + const tag = DB.getSnapshot()?.tags?.filter((t) => t._id === tagId)[0]; + + if (tag === undefined) { return { status: -1, - message: "Failed to clear tag PB: " + response.data.message, + message: "Tag not found", }; } + tag.personalBests = { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; + $( + `.pageSettings .section.tags .tagsList .tag[id="${tagId}"] .clearPbButton` + ).attr("aria-label", "No PB found"); + return { + status: 1, + message: "Tag PB cleared", + }; }, (thisPopup) => { thisPopup.text = `Are you sure you want to clear PB for tag ${thisPopup.parameters[1]}?`; diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index b959f814ea5d..6366f4874074 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -32,6 +32,13 @@ export async function linkDiscord(hashOverride: string): Promise { ); } + if (response.data === null) { + return Notifications.add( + "Failed to link Discord: data returned was null", + -1 + ); + } + Notifications.add(response.message, 1); const snapshot = DB.getSnapshot(); From 45c00d9686be299defcfba19d2ec53d52c3968da Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 16:30:30 +0100 Subject: [PATCH 07/20] remove comment --- frontend/src/ts/ape/types/ape.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/ts/ape/types/ape.d.ts b/frontend/src/ts/ape/types/ape.d.ts index 65e023ff6274..9a7e56d28bb6 100644 --- a/frontend/src/ts/ape/types/ape.d.ts +++ b/frontend/src/ts/ape/types/ape.d.ts @@ -26,7 +26,6 @@ declare namespace Ape { data: TData | null; }; - // todo: remove any after all ape endpoints are typed type EndpointResponse = Promise>; type HttpClient = { From ed4d3a9e6a00ec9d6d03d19c3c522ec330846ed8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 16:42:06 +0100 Subject: [PATCH 08/20] fix tests --- backend/__tests__/dal/leaderboards.spec.ts | 8 ++++---- backend/__tests__/dal/result.spec.ts | 3 ++- backend/src/dal/user.ts | 6 +----- backend/src/types/types.d.ts | 1 + 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/__tests__/dal/leaderboards.spec.ts b/backend/__tests__/dal/leaderboards.spec.ts index a49ea06f786e..56b7d5f4dcc2 100644 --- a/backend/__tests__/dal/leaderboards.spec.ts +++ b/backend/__tests__/dal/leaderboards.spec.ts @@ -157,7 +157,7 @@ describe("LeaderboardsDal", () => { }); }); -function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) { +function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) { const lbBest: SharedTypes.PersonalBest = user.lbPersonalBests?.time[time].english; @@ -178,13 +178,13 @@ function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) { async function createUser( lbPersonalBests?: MonkeyTypes.LbPersonalBests, - userProperties?: Partial -): Promise { + userProperties?: Partial +): Promise { const uid = new ObjectId().toHexString(); await UserDal.addUser("User " + uid, uid + "@example.com", uid); await DB.getDb() - ?.collection("users") + ?.collection("users") .updateOne( { uid }, { diff --git a/backend/__tests__/dal/result.spec.ts b/backend/__tests__/dal/result.spec.ts index 3e35b090b233..02ab7fe958ad 100644 --- a/backend/__tests__/dal/result.spec.ts +++ b/backend/__tests__/dal/result.spec.ts @@ -15,7 +15,8 @@ async function createDummyData( timestamp: number, tag?: string ): Promise { - const dummyUser: MonkeyTypes.User = { + const dummyUser: MonkeyTypes.DBUser = { + _id: new ObjectId(), uid, addedAt: 0, email: "test@example.com", diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index f41a6fb84d89..8ac560851cad 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -76,11 +76,7 @@ export async function resetUser(uid: string): Promise { profileDetails: { bio: "", keyboard: "", - socialProfiles: { - github: "", - twitter: "", - website: "", - }, + socialProfiles: {}, }, favoriteQuotes: {}, customThemes: [], diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index ea5fac8ba40d..fe9874270546 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -33,6 +33,7 @@ declare namespace MonkeyTypes { canReport?: boolean; lastNameChange?: number; canManageApeKeys?: boolean; + bananas?: number; }; type DBCustomTheme = WithObjectId; From 586fa80b327f3c2672c00cbefd863ed5b636956b Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 00:43:35 +0100 Subject: [PATCH 09/20] chore: omit ips --- backend/src/api/controllers/user.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index ef74b65150fc..797a86150e99 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -327,6 +327,7 @@ function getRelevantUserInfo( "_id", "lastResultHashes", "note", + "ips", ]); } From aee6f19167ce6b1382c2b8b997724b7f9d16c05d Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 12:50:13 +0100 Subject: [PATCH 10/20] fix(language): remove some unnecessarily capitalised words in german 1k --- frontend/static/languages/german_1k.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frontend/static/languages/german_1k.json b/frontend/static/languages/german_1k.json index 71e3de881072..73914793d578 100644 --- a/frontend/static/languages/german_1k.json +++ b/frontend/static/languages/german_1k.json @@ -127,7 +127,6 @@ "damit", "bereits", "da", - "Auch", "ihr", "seinen", "müssen", @@ -298,7 +297,6 @@ "wenig", "lange", "gemacht", - "Wer", "Dies", "Fall", "mir", @@ -458,7 +456,6 @@ "eigene", "Dann", "gegeben", - "Außerdem", "Stunden", "eigentlich", "Meter", @@ -468,7 +465,6 @@ "ebenso", "Bereich", "zum Beispiel", - "Bis", "Höhe", "Familie", "Während", @@ -720,7 +716,6 @@ "kamen", "Ausstellung", "Zeiten", - "Dem", "einzige", "meine", "Nun", @@ -809,7 +804,6 @@ "jedenfalls", "gesehen", "französischen", - "Trotz", "darunter", "Spieler", "forderte", @@ -826,7 +820,6 @@ "Einen", "Präsidenten", "hinaus", - "Zwar", "verletzt", "weltweit", "Sohn", From 0344c106e0ee0d88036539ee33e597b24c8a3299 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 13:25:02 +0100 Subject: [PATCH 11/20] fix(typing): first space sometimes soft locking the website --- frontend/src/ts/controllers/input-controller.ts | 2 -- frontend/src/ts/settings/settings-group.ts | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index 125bbca16725..28029cbce242 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -28,7 +28,6 @@ import * as TestWords from "../test/test-words"; import * as Hangul from "hangul-js"; import * as CustomTextState from "../states/custom-text-name"; import * as FunboxList from "../test/funbox/funbox-list"; -import * as Settings from "../pages/settings"; import * as KeymapEvent from "../observables/keymap-event"; import { IgnoredKeys } from "../constants/ignored-keys"; import { ModifierKeys } from "../constants/modifier-keys"; @@ -193,7 +192,6 @@ function handleSpace(): void { f.functions.handleSpace(); } } - Settings.groups["layout"]?.updateUI(); dontInsertSpace = true; diff --git a/frontend/src/ts/settings/settings-group.ts b/frontend/src/ts/settings/settings-group.ts index 91e3152db97e..116066667d22 100644 --- a/frontend/src/ts/settings/settings-group.ts +++ b/frontend/src/ts/settings/settings-group.ts @@ -82,7 +82,12 @@ export default class SettingsGroup { if (this.mode === "select") { const select = document.querySelector( `.pageSettings .section[data-config-name='${this.configName}'] select` - ) as HTMLSelectElement; + ) as HTMLSelectElement | null; + + if (select === null) { + return; + } + select.value = this.configValue as string; //@ts-expect-error From 1df91d1825c8a75689c79dcac00d02e83f97b4b7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 14:22:49 +0100 Subject: [PATCH 12/20] perf: speed up settings page loading --- frontend/src/ts/pages/settings.ts | 122 ++++++++++++++---------------- 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 947152593288..28ec43f2aad7 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -452,32 +452,30 @@ async function fillSettingsPage(): Promise { ); } - const languageSelectData = []; + const element = document.querySelector( + ".pageSettings .section[data-config-name='language'] select" + ) as Element; + + let html = ""; if (languageGroups) { for (const group of languageGroups) { - const groupData = { - label: group.name, - options: group.languages.map((language: string) => { - return { - value: language, - text: Misc.getLanguageDisplayString(language), - selected: language === Config.language, - }; - }), - }; - languageSelectData.push(groupData); + html += ``; + for (const language of group.languages) { + const selected = language === Config.language ? "selected" : ""; + const text = Misc.getLanguageDisplayString(language); + html += ``; + } + html += ``; } } + element.innerHTML = html; new SlimSelect({ - select: ".pageSettings .section[data-config-name='language'] select", - data: languageSelectData, + select: element, settings: { searchPlaceholder: "search", }, }); - await Misc.sleep(0); - let layoutsList; try { layoutsList = await Misc.getLayoutsList(); @@ -485,48 +483,42 @@ async function fillSettingsPage(): Promise { console.error(Misc.createErrorMessage(e, "Failed to refresh keymap")); } - const layoutSelectData = []; - const keymapLayoutSelectData = []; + const layoutSelectElement = document.querySelector( + ".pageSettings .section[data-config-name='layout'] select" + ) as Element; + const keymapLayoutSelectElement = document.querySelector( + ".pageSettings .section[data-config-name='keymapLayout'] select" + ) as Element; - layoutSelectData.push({ - value: "default", - text: "off", - }); - - keymapLayoutSelectData.push({ - value: "overrideSync", - text: "emulator sync", - }); + let layoutHtml = ''; + let keymapLayoutHtml = ''; if (layoutsList) { for (const layout of Object.keys(layoutsList)) { + const optionHtml = ``; if (layout.toString() !== "korean") { - layoutSelectData.push({ - value: layout, - text: layout.replace(/_/g, " "), - }); + layoutHtml += optionHtml; } if (layout.toString() !== "default") { - keymapLayoutSelectData.push({ - value: layout, - text: layout.replace(/_/g, " "), - }); + keymapLayoutHtml += optionHtml; } } } + layoutSelectElement.innerHTML = layoutHtml; + keymapLayoutSelectElement.innerHTML = keymapLayoutHtml; + new SlimSelect({ - select: ".pageSettings .section[data-config-name='layout'] select", - data: layoutSelectData, + select: layoutSelectElement, }); new SlimSelect({ - select: ".pageSettings .section[data-config-name='keymapLayout'] select", - data: keymapLayoutSelectData, + select: keymapLayoutSelectElement, }); - await Misc.sleep(0); - let themes; try { themes = await Misc.getThemesList(); @@ -536,27 +528,35 @@ async function fillSettingsPage(): Promise { ); } - const themeSelectLightData = []; - const themeSelectDarkData = []; + const themeSelectLightElement = document.querySelector( + ".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light" + ) as Element; + const themeSelectDarkElement = document.querySelector( + ".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark" + ) as Element; + + let themeSelectLightHtml = ""; + let themeSelectDarkHtml = ""; + if (themes) { for (const theme of themes) { - themeSelectLightData.push({ - value: theme.name, - text: theme.name.replace(/_/g, " "), - selected: theme.name === Config.themeLight, - }); - themeSelectDarkData.push({ - value: theme.name, - text: theme.name.replace(/_/g, " "), - selected: theme.name === Config.themeDark, - }); + const optionHtml = ``; + themeSelectLightHtml += optionHtml; + + const optionDarkHtml = ``; + themeSelectDarkHtml += optionDarkHtml; } } + themeSelectLightElement.innerHTML = themeSelectLightHtml; + themeSelectDarkElement.innerHTML = themeSelectDarkHtml; + new SlimSelect({ - select: - ".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light", - data: themeSelectLightData, + select: themeSelectLightElement, events: { afterChange: (newVal): void => { UpdateConfig.setThemeLight(newVal[0]?.value as string); @@ -565,9 +565,7 @@ async function fillSettingsPage(): Promise { }); new SlimSelect({ - select: - ".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark", - data: themeSelectDarkData, + select: themeSelectDarkElement, events: { afterChange: (newVal): void => { UpdateConfig.setThemeDark(newVal[0]?.value as string); @@ -575,8 +573,6 @@ async function fillSettingsPage(): Promise { }, }); - await Misc.sleep(0); - const funboxEl = document.querySelector( ".pageSettings .section[data-config-name='funbox'] .buttons" ) as HTMLDivElement; @@ -624,8 +620,6 @@ async function fillSettingsPage(): Promise { funboxEl.innerHTML = funboxElHTML; } - await Misc.sleep(0); - let isCustomFont = true; const fontsEl = document.querySelector( ".pageSettings .section[data-config-name='fontFamily'] .buttons" @@ -677,7 +671,6 @@ async function fillSettingsPage(): Promise { Config.customLayoutfluid.replace(/#/g, " ") ); - await Misc.sleep(0); setEventDisabled(true); if (!groupsInitialized) { await initGroups(); @@ -688,7 +681,6 @@ async function fillSettingsPage(): Promise { } } setEventDisabled(false); - await Misc.sleep(0); await ThemePicker.refreshButtons(); await UpdateConfig.loadPromise; } From bce3e127a02bff8f0b9b5be091dd68c1b983d6cc Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 19 Feb 2024 14:46:02 +0100 Subject: [PATCH 13/20] fix: use selected typing speed unit on personal best popup (fehmer) (#5070) * fix: Use selected typing speed unit on personal best popup * refactor * refactor * test coverage * use Format in more places * Make config mockable * dependency injection * wip * fix * test * touch --- frontend/__tests__/utils/format.spec.ts | 201 ++++++++++++++++++++ frontend/src/ts/account/pb-tables.ts | 70 ++----- frontend/src/ts/elements/leaderboards.ts | 41 ++-- frontend/src/ts/elements/modes-notice.ts | 42 ++-- frontend/src/ts/pages/account.ts | 162 +++------------- frontend/src/ts/popups/pb-tables-popup.ts | 15 +- frontend/src/ts/test/live-burst.ts | 9 +- frontend/src/ts/test/live-wpm.ts | 10 +- frontend/src/ts/test/result.ts | 82 ++++---- frontend/src/ts/test/test-ui.ts | 6 +- frontend/src/ts/types/types.d.ts | 1 - frontend/src/ts/utils/format.ts | 67 +++++++ frontend/src/ts/utils/typing-speed-units.ts | 10 - frontend/static/html/popups.html | 2 +- 14 files changed, 416 insertions(+), 302 deletions(-) create mode 100644 frontend/__tests__/utils/format.spec.ts create mode 100644 frontend/src/ts/utils/format.ts diff --git a/frontend/__tests__/utils/format.spec.ts b/frontend/__tests__/utils/format.spec.ts new file mode 100644 index 000000000000..b7ebdd5cdbb5 --- /dev/null +++ b/frontend/__tests__/utils/format.spec.ts @@ -0,0 +1,201 @@ +import { Formatting } from "../../src/ts/utils/format"; +import * as MockConfig from "../../src/ts/config"; +import DefaultConfig from "../../src/ts/constants/default-config"; + +describe("format.ts", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + describe("typingsSpeed", () => { + it("should format with typing speed and decimalPlaces from configuration", () => { + //wpm, no decimals + const wpmNoDecimals = getInstance({ + typingSpeedUnit: "wpm", + alwaysShowDecimalPlaces: false, + }); + expect(wpmNoDecimals.typingSpeed(12.5)).toEqual("13"); + expect(wpmNoDecimals.typingSpeed(0)).toEqual("0"); + + //cpm, no decimals + const cpmNoDecimals = getInstance({ + typingSpeedUnit: "cpm", + alwaysShowDecimalPlaces: false, + }); + expect(cpmNoDecimals.typingSpeed(12.5)).toEqual("63"); + expect(cpmNoDecimals.typingSpeed(0)).toEqual("0"); + + //wpm, with decimals + const wpmWithDecimals = getInstance({ + typingSpeedUnit: "wpm", + alwaysShowDecimalPlaces: true, + }); + expect(wpmWithDecimals.typingSpeed(12.5)).toEqual("12.50"); + expect(wpmWithDecimals.typingSpeed(0)).toEqual("0.00"); + + //cpm, with decimals + const cpmWithDecimals = getInstance({ + typingSpeedUnit: "cpm", + alwaysShowDecimalPlaces: true, + }); + expect(cpmWithDecimals.typingSpeed(12.5)).toEqual("62.50"); + expect(cpmWithDecimals.typingSpeed(0)).toEqual("0.00"); + }); + + it("should format with fallback", () => { + //default fallback + const format = getInstance(); + expect(format.typingSpeed(null)).toEqual("-"); + expect(format.typingSpeed(undefined)).toEqual("-"); + + //provided fallback + expect(format.typingSpeed(null, { fallback: "none" })).toEqual("none"); + expect(format.typingSpeed(null, { fallback: "" })).toEqual(""); + expect(format.typingSpeed(undefined, { fallback: "none" })).toEqual( + "none" + ); + + expect(format.typingSpeed(undefined, { fallback: "" })).toEqual(""); + expect(format.typingSpeed(undefined, { fallback: undefined })).toEqual( + "" + ); + }); + + it("should format with decimals", () => { + //force with decimals + const wpmNoDecimals = getInstance({ + typingSpeedUnit: "wpm", + alwaysShowDecimalPlaces: false, + }); + expect( + wpmNoDecimals.typingSpeed(100, { showDecimalPlaces: true }) + ).toEqual("100.00"); + //force without decimals + const wpmWithDecimals = getInstance({ + typingSpeedUnit: "wpm", + alwaysShowDecimalPlaces: true, + }); + expect( + wpmWithDecimals.typingSpeed(100, { showDecimalPlaces: false }) + ).toEqual("100"); + }); + + it("should format with suffix", () => { + const format = getInstance({ + typingSpeedUnit: "wpm", + alwaysShowDecimalPlaces: false, + }); + expect(format.typingSpeed(100, { suffix: " raw" })).toEqual("100 raw"); + expect(format.typingSpeed(100, { suffix: undefined })).toEqual("100"); + expect(format.typingSpeed(0, { suffix: " raw" })).toEqual("0 raw"); + expect(format.typingSpeed(null, { suffix: " raw" })).toEqual("-"); + expect(format.typingSpeed(undefined, { suffix: " raw" })).toEqual("-"); + }); + }); + describe("percentage", () => { + it("should format with decimalPlaces from configuration", () => { + //no decimals + const noDecimals = getInstance({ alwaysShowDecimalPlaces: false }); + expect(noDecimals.percentage(12.5)).toEqual("13%"); + expect(noDecimals.percentage(0)).toEqual("0%"); + + //with decimals + const withDecimals = getInstance({ alwaysShowDecimalPlaces: true }); + expect(withDecimals.percentage(12.5)).toEqual("12.50%"); + expect(withDecimals.percentage(0)).toEqual("0.00%"); + }); + + it("should format with fallback", () => { + //default fallback + const format = getInstance(); + expect(format.percentage(null)).toEqual("-"); + expect(format.percentage(undefined)).toEqual("-"); + + //provided fallback + expect(format.percentage(null, { fallback: "none" })).toEqual("none"); + expect(format.percentage(null, { fallback: "" })).toEqual(""); + expect(format.percentage(undefined, { fallback: "none" })).toEqual( + "none" + ); + + expect(format.percentage(undefined, { fallback: "" })).toEqual(""); + expect(format.percentage(undefined, { fallback: undefined })).toEqual(""); + }); + + it("should format with decimals", () => { + //force with decimals + const noDecimals = getInstance({ alwaysShowDecimalPlaces: false }); + expect(noDecimals.percentage(100, { showDecimalPlaces: true })).toEqual( + "100.00%" + ); + //force without decimals + const withDecimals = getInstance({ alwaysShowDecimalPlaces: true }); + expect( + withDecimals.percentage(100, { showDecimalPlaces: false }) + ).toEqual("100%"); + }); + + it("should format with suffix", () => { + const format = getInstance({ alwaysShowDecimalPlaces: false }); + expect(format.percentage(100, { suffix: " raw" })).toEqual("100% raw"); + expect(format.percentage(100, { suffix: undefined })).toEqual("100%"); + expect(format.percentage(0, { suffix: " raw" })).toEqual("0% raw"); + expect(format.percentage(null, { suffix: " raw" })).toEqual("-"); + expect(format.percentage(undefined, { suffix: " raw" })).toEqual("-"); + }); + }); + describe("decimals", () => { + it("should format with decimalPlaces from configuration", () => { + //no decimals + const noDecimals = getInstance({ alwaysShowDecimalPlaces: false }); + expect(noDecimals.decimals(12.5)).toEqual("13"); + expect(noDecimals.decimals(0)).toEqual("0"); + + //with decimals + const withDecimals = getInstance({ alwaysShowDecimalPlaces: true }); + expect(withDecimals.decimals(12.5)).toEqual("12.50"); + expect(withDecimals.decimals(0)).toEqual("0.00"); + }); + + it("should format with fallback", () => { + //default fallback + const format = getInstance(); + expect(format.decimals(null)).toEqual("-"); + expect(format.decimals(undefined)).toEqual("-"); + + //provided fallback + expect(format.decimals(null, { fallback: "none" })).toEqual("none"); + expect(format.decimals(null, { fallback: "" })).toEqual(""); + expect(format.decimals(undefined, { fallback: "none" })).toEqual("none"); + + expect(format.decimals(undefined, { fallback: "" })).toEqual(""); + expect(format.decimals(undefined, { fallback: undefined })).toEqual(""); + }); + + it("should format with decimals", () => { + //force with decimals + const noDecimals = getInstance({ alwaysShowDecimalPlaces: false }); + expect(noDecimals.decimals(100, { showDecimalPlaces: true })).toEqual( + "100.00" + ); + //force without decimals + const withDecimals = getInstance({ alwaysShowDecimalPlaces: true }); + expect(withDecimals.decimals(100, { showDecimalPlaces: false })).toEqual( + "100" + ); + }); + + it("should format with suffix", () => { + const format = getInstance({ alwaysShowDecimalPlaces: false }); + expect(format.decimals(100, { suffix: " raw" })).toEqual("100 raw"); + expect(format.decimals(100, { suffix: undefined })).toEqual("100"); + expect(format.decimals(0, { suffix: " raw" })).toEqual("0 raw"); + expect(format.decimals(null, { suffix: " raw" })).toEqual("-"); + expect(format.decimals(undefined, { suffix: " raw" })).toEqual("-"); + }); + }); +}); + +function getInstance(config?: Partial): Formatting { + const target: SharedTypes.Config = { ...DefaultConfig, ...config }; + return new Formatting(target); +} diff --git a/frontend/src/ts/account/pb-tables.ts b/frontend/src/ts/account/pb-tables.ts index 4fd9e6f328ec..4f1bcf0d67fd 100644 --- a/frontend/src/ts/account/pb-tables.ts +++ b/frontend/src/ts/account/pb-tables.ts @@ -1,7 +1,6 @@ import Config from "../config"; -import format from "date-fns/format"; -import * as Misc from "../utils/misc"; -import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; +import dateFormat from "date-fns/format"; +import Format from "../utils/format"; function clearTables(isProfile: boolean): void { const source = isProfile ? "Profile" : "Account"; @@ -140,7 +139,6 @@ function buildPbHtml( let dateText = ""; const modeString = `${mode2} ${mode === "time" ? "seconds" : "words"}`; const speedUnit = Config.typingSpeedUnit; - const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); try { const pbData = (pbs[mode][mode2] ?? []).sort((a, b) => b.wpm - a.wpm)[0]; @@ -148,62 +146,28 @@ function buildPbHtml( const date = new Date(pbData.timestamp); if (pbData.timestamp) { - dateText = format(date, "dd MMM yyyy"); + dateText = dateFormat(date, "dd MMM yyyy"); } - let speedString: number | string = typingSpeedUnit.fromWpm(pbData.wpm); - if (Config.alwaysShowDecimalPlaces) { - speedString = Misc.roundTo2(speedString).toFixed(2); - } else { - speedString = Math.round(speedString); - } - speedString += ` ${speedUnit}`; - - let rawString: number | string = typingSpeedUnit.fromWpm(pbData.raw); - if (Config.alwaysShowDecimalPlaces) { - rawString = Misc.roundTo2(rawString).toFixed(2); - } else { - rawString = Math.round(rawString); - } - rawString += ` raw`; - - let accString: number | string = pbData.acc; - if (accString === undefined) { - accString = "-"; - } else { - if (Config.alwaysShowDecimalPlaces) { - accString = Misc.roundTo2(accString).toFixed(2); - } else { - accString = Math.floor(accString); - } - } - accString += ` acc`; - - let conString: number | string = pbData.consistency; - if (conString === undefined) { - conString = "-"; - } else { - if (Config.alwaysShowDecimalPlaces) { - conString = Misc.roundTo2(conString).toFixed(2); - } else { - conString = Math.round(conString); - } - } - conString += ` con`; - retval = `
${modeString}
-
${Math.round(typingSpeedUnit.fromWpm(pbData.wpm))}
-
${ - pbData.acc === undefined ? "-" : Math.floor(pbData.acc) + "%" - }
+
${Format.typingSpeed(pbData.wpm, { + showDecimalPlaces: false, + })}
+
${Format.percentage(pbData.acc, { + showDecimalPlaces: false, + })}
${modeString}
-
${speedString}
-
${rawString}
-
${accString}
-
${conString}
+
${Format.typingSpeed(pbData.wpm, { + suffix: ` ${speedUnit}`, + })}
+
${Format.typingSpeed(pbData.raw, { suffix: " raw" })}
+
${Format.percentage(pbData.acc, { suffix: " acc" })}
+
${Format.percentage(pbData.consistency, { + suffix: " con", + })}
${dateText}
`; } catch (e) { diff --git a/frontend/src/ts/elements/leaderboards.ts b/frontend/src/ts/elements/leaderboards.ts index 712c6707c43a..bb2e26b208d0 100644 --- a/frontend/src/ts/elements/leaderboards.ts +++ b/frontend/src/ts/elements/leaderboards.ts @@ -2,7 +2,6 @@ import Ape from "../ape"; import * as DB from "../db"; import Config from "../config"; import * as Misc from "../utils/misc"; -import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; import * as Notifications from "./notifications"; import format from "date-fns/format"; import { isAuthenticated } from "../firebase"; @@ -11,6 +10,7 @@ import { getHTMLById as getBadgeHTMLbyId } from "../controllers/badge-controller import * as ConnectionState from "../states/connection"; import * as Skeleton from "../popups/skeleton"; import { debounce } from "throttle-debounce"; +import Format from "../utils/format"; import SlimSelect from "slim-select"; const wrapperId = "leaderboardsWrapper"; @@ -165,7 +165,6 @@ function updateFooter(lb: LbKey): void { return; } - const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); if (DB.getSnapshot()?.lbOptOut === true) { $(`#leaderboardsWrapper table.${side} tfoot`).html(` @@ -211,12 +210,18 @@ function updateFooter(lb: LbKey): void { ${lbRank.rank} You${toppercent ? toppercent : ""} - ${typingSpeedUnit.fromWpm(entry.wpm).toFixed(2)}
-
${entry.acc.toFixed(2)}%
- ${typingSpeedUnit.fromWpm(entry.raw).toFixed(2)}
-
${ - entry.consistency === undefined ? "-" : entry.consistency.toFixed(2) + "%" - }
+ ${Format.typingSpeed(entry.wpm, { + showDecimalPlaces: true, + })}
+
${Format.percentage(entry.acc, { + showDecimalPlaces: true, + })}%
+ ${Format.typingSpeed(entry.raw, { + showDecimalPlaces: true, + })}
+
${Format.percentage(entry.consistency, { + showDecimalPlaces: true, + })}
${format(date, "dd MMM yyyy")}
${format(date, "HH:mm")}
@@ -296,8 +301,6 @@ async function fillTable(lb: LbKey): Promise { "No results found" ); } - - const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); const loggedInUserName = DB.getSnapshot()?.name; let html = ""; @@ -336,12 +339,18 @@ async function fillTable(lb: LbKey): Promise { ${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""} - ${typingSpeedUnit.fromWpm(entry.wpm).toFixed(2)}
-
${entry.acc.toFixed(2)}%
- ${typingSpeedUnit.fromWpm(entry.raw).toFixed(2)}
-
${ - entry.consistency === undefined ? "-" : entry.consistency.toFixed(2) + "%" - }
+ ${Format.typingSpeed(entry.wpm, { + showDecimalPlaces: true, + })}
+
${Format.percentage(entry.acc, { + showDecimalPlaces: true, + })}
+ ${Format.typingSpeed(entry.raw, { + showDecimalPlaces: true, + })}
+
${Format.percentage(entry.consistency, { + showDecimalPlaces: true, + })}
${format(date, "dd MMM yyyy")}
${format(date, "HH:mm")}
diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index d813172e6c9c..75fb6bcdf374 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -7,8 +7,8 @@ import * as TestWords from "../test/test-words"; import * as ConfigEvent from "../observables/config-event"; import { isAuthenticated } from "../firebase"; import * as CustomTextState from "../states/custom-text-name"; -import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; -import { getLanguageDisplayString, roundTo2 } from "../utils/misc"; +import { getLanguageDisplayString } from "../utils/misc"; +import Format from "../utils/format"; ConfigEvent.subscribe((eventKey) => { if ( @@ -123,14 +123,11 @@ export async function update(): Promise { Config.paceCaret !== "off" || (Config.repeatedPace && TestState.isPaceRepeat) ) { - let speed = ""; - try { - speed = ` (${roundTo2( - getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm( - PaceCaret.settings?.wpm ?? 0 - ) - )} ${Config.typingSpeedUnit})`; - } catch {} + const speed = Format.typingSpeed(PaceCaret.settings?.wpm ?? 0, { + showDecimalPlaces: true, + suffix: ` ${Config.typingSpeedUnit}`, + }); + $(".pageTest #testModesNotice").append( `
${ Config.paceCaret === "average" @@ -142,7 +139,7 @@ export async function update(): Promise { : Config.paceCaret === "daily" ? "daily" : "custom" - } pace${speed}
` + } pace ${speed}` ); } @@ -157,14 +154,11 @@ export async function update(): Promise { if (isAuthenticated() && avgWPM > 0) { const avgWPMText = ["speed", "both"].includes(Config.showAverage) - ? getTypingSpeedUnit(Config.typingSpeedUnit).convertWithUnitSuffix( - avgWPM, - Config.alwaysShowDecimalPlaces - ) + ? Format.typingSpeed(avgWPM, { suffix: ` ${Config.typingSpeedUnit}` }) : ""; const avgAccText = ["acc", "both"].includes(Config.showAverage) - ? `${avgAcc}% acc` + ? Format.percentage(avgAcc, { suffix: " acc" }) : ""; const text = `${avgWPMText} ${avgAccText}`.trim(); @@ -177,11 +171,10 @@ export async function update(): Promise { if (Config.minWpm !== "off") { $(".pageTest #testModesNotice").append( - `
min ${roundTo2( - getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm( - Config.minWpmCustomSpeed - ) - )} ${Config.typingSpeedUnit}
` + `
min ${Format.typingSpeed( + Config.minWpmCustomSpeed, + { showDecimalPlaces: true, suffix: ` ${Config.typingSpeedUnit}` } + )}
` ); } @@ -193,10 +186,9 @@ export async function update(): Promise { if (Config.minBurst !== "off") { $(".pageTest #testModesNotice").append( - `
min ${roundTo2( - getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm( - Config.minBurstCustomSpeed - ) + `
min ${Format.typingSpeed( + Config.minBurstCustomSpeed, + { showDecimalPlaces: true } )} ${Config.typingSpeedUnit} burst ${ Config.minBurst === "flex" ? "(flex)" : "" }
` diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index aed4c4e14bfe..f524bfb2e9c7 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -23,6 +23,7 @@ import * as ActivePage from "../states/active-page"; import { Auth } from "../firebase"; import * as Loader from "../elements/loader"; import * as ResultBatches from "../elements/result-batches"; +import Format from "../utils/format"; let filterDebug = false; //toggle filterdebug @@ -37,7 +38,6 @@ let filteredResults: SharedTypes.Result[] = []; let visibleTableLines = 0; function loadMoreLines(lineIndex?: number): void { - const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); if (filteredResults === undefined || filteredResults.length === 0) return; let newVisibleLines; if (lineIndex && lineIndex > visibleTableLines) { @@ -53,16 +53,6 @@ function loadMoreLines(lineIndex?: number): void { diff = "normal"; } - let raw; - try { - raw = typingSpeedUnit.fromWpm(result.rawWpm).toFixed(2); - if (raw === undefined) { - raw = "-"; - } - } catch (e) { - raw = "-"; - } - let icons = ` ${pb} - ${typingSpeedUnit.fromWpm(result.wpm).toFixed(2)} - ${raw} - ${result.acc.toFixed(2)}% - ${consistency} + ${Format.typingSpeed(result.wpm, { showDecimalPlaces: true })} + ${Format.typingSpeed(result.rawWpm, { showDecimalPlaces: true })} + ${Format.percentage(result.acc, { showDecimalPlaces: true })} + ${Format.percentage(result.consistency, { + showDecimalPlaces: true, + })} ${charStats} ${result.mode} ${result.mode2} ${icons} @@ -860,144 +846,58 @@ async function fillContent(): Promise { Misc.secondsToString(Math.round(totalSecondsFiltered), true, true) ); - let highestSpeed: number | string = typingSpeedUnit.fromWpm(topWpm); - - if (Config.alwaysShowDecimalPlaces) { - highestSpeed = Misc.roundTo2(highestSpeed).toFixed(2); - } else { - highestSpeed = Math.round(highestSpeed); - } - const speedUnit = Config.typingSpeedUnit; $(".pageAccount .highestWpm .title").text(`highest ${speedUnit}`); - $(".pageAccount .highestWpm .val").text(highestSpeed); - - let averageSpeed: number | string = typingSpeedUnit.fromWpm(totalWpm); - if (Config.alwaysShowDecimalPlaces) { - averageSpeed = Misc.roundTo2(averageSpeed / testCount).toFixed(2); - } else { - averageSpeed = Math.round(averageSpeed / testCount); - } + $(".pageAccount .highestWpm .val").text(Format.typingSpeed(topWpm)); $(".pageAccount .averageWpm .title").text(`average ${speedUnit}`); - $(".pageAccount .averageWpm .val").text(averageSpeed); - - let averageSpeedLast10: number | string = - typingSpeedUnit.fromWpm(wpmLast10total); - if (Config.alwaysShowDecimalPlaces) { - averageSpeedLast10 = Misc.roundTo2(averageSpeedLast10 / last10).toFixed(2); - } else { - averageSpeedLast10 = Math.round(averageSpeedLast10 / last10); - } + $(".pageAccount .averageWpm .val").text( + Format.typingSpeed(totalWpm / testCount) + ); $(".pageAccount .averageWpm10 .title").text( `average ${speedUnit} (last 10 tests)` ); - $(".pageAccount .averageWpm10 .val").text(averageSpeedLast10); - - let highestRawSpeed: number | string = typingSpeedUnit.fromWpm(rawWpm.max); - if (Config.alwaysShowDecimalPlaces) { - highestRawSpeed = Misc.roundTo2(highestRawSpeed).toFixed(2); - } else { - highestRawSpeed = Math.round(highestRawSpeed); - } + $(".pageAccount .averageWpm10 .val").text( + Format.typingSpeed(wpmLast10total / last10) + ); $(".pageAccount .highestRaw .title").text(`highest raw ${speedUnit}`); - $(".pageAccount .highestRaw .val").text(highestRawSpeed); - - let averageRawSpeed: number | string = typingSpeedUnit.fromWpm(rawWpm.total); - if (Config.alwaysShowDecimalPlaces) { - averageRawSpeed = Misc.roundTo2(averageRawSpeed / rawWpm.count).toFixed(2); - } else { - averageRawSpeed = Math.round(averageRawSpeed / rawWpm.count); - } + $(".pageAccount .highestRaw .val").text(Format.typingSpeed(rawWpm.max)); $(".pageAccount .averageRaw .title").text(`average raw ${speedUnit}`); - $(".pageAccount .averageRaw .val").text(averageRawSpeed); - - let averageRawSpeedLast10: number | string = typingSpeedUnit.fromWpm( - rawWpm.last10Total + $(".pageAccount .averageRaw .val").text( + Format.typingSpeed(rawWpm.total / rawWpm.count) ); - if (Config.alwaysShowDecimalPlaces) { - averageRawSpeedLast10 = Misc.roundTo2( - averageRawSpeedLast10 / rawWpm.last10Count - ).toFixed(2); - } else { - averageRawSpeedLast10 = Math.round( - averageRawSpeedLast10 / rawWpm.last10Count - ); - } $(".pageAccount .averageRaw10 .title").text( `average raw ${speedUnit} (last 10 tests)` ); - $(".pageAccount .averageRaw10 .val").text(averageRawSpeedLast10); + $(".pageAccount .averageRaw10 .val").text( + Format.typingSpeed(rawWpm.last10Total / rawWpm.last10Count) + ); $(".pageAccount .highestWpm .mode").html(topMode); $(".pageAccount .testsTaken .val").text(testCount); - let highestAcc: string | number = topAcc; - if (Config.alwaysShowDecimalPlaces) { - highestAcc = Misc.roundTo2(highestAcc).toFixed(2); - } else { - highestAcc = Math.floor(highestAcc); - } - - $(".pageAccount .highestAcc .val").text(highestAcc + "%"); - - let averageAcc: number | string = totalAcc; - if (Config.alwaysShowDecimalPlaces) { - averageAcc = Misc.roundTo2(averageAcc / testCount); - } else { - averageAcc = Math.floor(averageAcc / testCount); - } - - $(".pageAccount .avgAcc .val").text(averageAcc + "%"); - - let averageAccLast10: number | string = totalAcc10; - if (Config.alwaysShowDecimalPlaces) { - averageAccLast10 = Misc.roundTo2(averageAccLast10 / last10); - } else { - averageAccLast10 = Math.floor(averageAccLast10 / last10); - } - - $(".pageAccount .avgAcc10 .val").text(averageAccLast10 + "%"); + $(".pageAccount .highestAcc .val").text(Format.percentage(topAcc)); + $(".pageAccount .avgAcc .val").text(Format.percentage(totalAcc / testCount)); + $(".pageAccount .avgAcc10 .val").text(Format.percentage(totalAcc10 / last10)); if (totalCons === 0 || totalCons === undefined) { $(".pageAccount .avgCons .val").text("-"); $(".pageAccount .avgCons10 .val").text("-"); } else { - let highestCons: number | string = topCons; - if (Config.alwaysShowDecimalPlaces) { - highestCons = Misc.roundTo2(highestCons).toFixed(2); - } else { - highestCons = Math.round(highestCons); - } - - $(".pageAccount .highestCons .val").text(highestCons + "%"); - - let averageCons: number | string = totalCons; - if (Config.alwaysShowDecimalPlaces) { - averageCons = Misc.roundTo2(averageCons / consCount).toFixed(2); - } else { - averageCons = Math.round(averageCons / consCount); - } + $(".pageAccount .highestCons .val").text(Format.percentage(topCons)); - $(".pageAccount .avgCons .val").text(averageCons + "%"); - - let averageConsLast10: number | string = totalCons10; - if (Config.alwaysShowDecimalPlaces) { - averageConsLast10 = Misc.roundTo2( - averageConsLast10 / Math.min(last10, consCount) - ).toFixed(2); - } else { - averageConsLast10 = Math.round( - averageConsLast10 / Math.min(last10, consCount) - ); - } + $(".pageAccount .avgCons .val").text( + Format.percentage(totalCons / consCount) + ); - $(".pageAccount .avgCons10 .val").text(averageConsLast10 + "%"); + $(".pageAccount .avgCons10 .val").text( + Format.percentage(totalCons10 / Math.min(last10, consCount)) + ); } $(".pageAccount .testsStarted .val").text(`${testCount + testRestarts}`); @@ -1020,7 +920,7 @@ async function fillContent(): Promise { const plus = wpmChangePerHour > 0 ? "+" : ""; $(".pageAccount .group.chart .below .text").text( `Speed change per hour spent typing: ${ - plus + Misc.roundTo2(typingSpeedUnit.fromWpm(wpmChangePerHour)) + plus + Format.typingSpeed(wpmChangePerHour, { showDecimalPlaces: true }) } ${Config.typingSpeedUnit}` ); } diff --git a/frontend/src/ts/popups/pb-tables-popup.ts b/frontend/src/ts/popups/pb-tables-popup.ts index ae48e9574f12..c85e1baa915c 100644 --- a/frontend/src/ts/popups/pb-tables-popup.ts +++ b/frontend/src/ts/popups/pb-tables-popup.ts @@ -2,6 +2,8 @@ import * as DB from "../db"; import format from "date-fns/format"; import * as Skeleton from "./skeleton"; import { getLanguageDisplayString, isPopupVisible } from "../utils/misc"; +import Config from "../config"; +import Format from "../utils/format"; type PersonalBest = { mode2: SharedTypes.Config.Mode2; @@ -12,6 +14,9 @@ const wrapperId = "pbTablesPopupWrapper"; function update(mode: SharedTypes.Config.Mode): void { $("#pbTablesPopup table tbody").empty(); $($("#pbTablesPopup table thead tr td")[0] as HTMLElement).text(mode); + $($("#pbTablesPopup table thead tr td span.unit")[0] as HTMLElement).text( + Config.typingSpeedUnit + ); const snapshot = DB.getSnapshot(); if (!snapshot) return; @@ -56,16 +61,14 @@ function update(mode: SharedTypes.Config.Mode): void { ${mode2memory === pb.mode2 ? "" : pb.mode2} - ${pb.wpm.toFixed(2)} + ${Format.typingSpeed(pb.wpm)}
- ${pb.acc ? pb.acc + "%" : "-"} + ${Format.percentage(pb.acc)} - ${pb.raw ? pb.raw : "-"} + ${Format.typingSpeed(pb.raw)}
- ${ - pb.consistency ? pb.consistency + "%" : "-" - } + ${Format.percentage(pb.consistency)} ${pb.difficulty} ${pb.language ? getLanguageDisplayString(pb.language) : "-"} diff --git a/frontend/src/ts/test/live-burst.ts b/frontend/src/ts/test/live-burst.ts index 5fb88c28b449..0387e7e48cbb 100644 --- a/frontend/src/ts/test/live-burst.ts +++ b/frontend/src/ts/test/live-burst.ts @@ -1,15 +1,14 @@ import Config from "../config"; import * as TestState from "../test/test-state"; import * as ConfigEvent from "../observables/config-event"; -import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; +import Format from "../utils/format"; export async function update(burst: number): Promise { if (!Config.showLiveBurst) return; - burst = Math.round(getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm(burst)); + const burstText = Format.typingSpeed(burst, { showDecimalPlaces: false }); (document.querySelector("#miniTimerAndLiveWpm .burst") as Element).innerHTML = - burst.toString(); - (document.querySelector("#liveBurst") as Element).innerHTML = - burst.toString(); + burstText; + (document.querySelector("#liveBurst") as Element).innerHTML = burstText; } let state = false; diff --git a/frontend/src/ts/test/live-wpm.ts b/frontend/src/ts/test/live-wpm.ts index e36acb11a9d8..a68106e64a84 100644 --- a/frontend/src/ts/test/live-wpm.ts +++ b/frontend/src/ts/test/live-wpm.ts @@ -1,7 +1,7 @@ import Config from "../config"; import * as TestState from "../test/test-state"; import * as ConfigEvent from "../observables/config-event"; -import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; +import Format from "../utils/format"; const liveWpmElement = document.querySelector("#liveWpm") as Element; const miniLiveWpmElement = document.querySelector( @@ -13,11 +13,9 @@ export function update(wpm: number, raw: number): void { if (Config.blindMode) { number = raw; } - number = Math.round( - getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm(number) - ); - miniLiveWpmElement.innerHTML = number.toString(); - liveWpmElement.innerHTML = number.toString(); + const numberText = Format.typingSpeed(number, { showDecimalPlaces: false }); + miniLiveWpmElement.innerHTML = numberText; + liveWpmElement.innerHTML = numberText; } let state = false; diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 6289e2088338..ac1164ce4495 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -1,3 +1,4 @@ +//TODO: use Format import { Chart, type PluginChartOptions } from "chart.js"; import Config from "../config"; import * as AdController from "../controllers/ad-controller"; @@ -25,6 +26,7 @@ import * as Focus from "./focus"; import * as CustomText from "./custom-text"; import * as CustomTextState from "./../states/custom-text-name"; import * as Funbox from "./funbox/funbox"; +import Format from "../utils/format"; import confetti from "canvas-confetti"; import type { AnnotationOptions } from "chartjs-plugin-annotation"; @@ -213,24 +215,23 @@ export async function updateGraphPBLine(): Promise { function updateWpmAndAcc(): void { let inf = false; - const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); if (result.wpm >= 1000) { inf = true; } - if (Config.alwaysShowDecimalPlaces) { - $("#result .stats .wpm .top .text").text(Config.typingSpeedUnit); - if (inf) { - $("#result .stats .wpm .bottom").text("Infinite"); - } else { - $("#result .stats .wpm .bottom").text( - Misc.roundTo2(typingSpeedUnit.fromWpm(result.wpm)).toFixed(2) - ); - } - $("#result .stats .raw .bottom").text( - Misc.roundTo2(typingSpeedUnit.fromWpm(result.rawWpm)).toFixed(2) - ); + $("#result .stats .wpm .top .text").text(Config.typingSpeedUnit); + if (inf) { + $("#result .stats .wpm .bottom").text("Infinite"); + } else { + $("#result .stats .wpm .bottom").text(Format.typingSpeed(result.wpm)); + } + $("#result .stats .raw .bottom").text(Format.typingSpeed(result.rawWpm)); + $("#result .stats .acc .bottom").text( + result.acc === 100 ? "100%" : Format.percentage(result.acc) + ); + + if (Config.alwaysShowDecimalPlaces) { if (Config.typingSpeedUnit != "wpm") { $("#result .stats .wpm .bottom").attr( "aria-label", @@ -245,9 +246,6 @@ function updateWpmAndAcc(): void { $("#result .stats .raw .bottom").removeAttr("aria-label"); } - $("#result .stats .acc .bottom").text( - result.acc === 100 ? "100%" : Misc.roundTo2(result.acc).toFixed(2) + "%" - ); let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; if (result.testDuration > 61) { time = Misc.secondsToString(Misc.roundTo2(result.testDuration)); @@ -261,53 +259,47 @@ function updateWpmAndAcc(): void { ); } else { //not showing decimal places - let wpmHover = typingSpeedUnit.convertWithUnitSuffix(result.wpm, true); - let rawWpmHover = typingSpeedUnit.convertWithUnitSuffix( - result.rawWpm, - true - ); + const decimalsAndSuffix = { + showDecimalPlaces: true, + suffix: ` ${Config.typingSpeedUnit}`, + }; + let wpmHover = Format.typingSpeed(result.wpm, decimalsAndSuffix); + let rawWpmHover = Format.typingSpeed(result.rawWpm, decimalsAndSuffix); + if (Config.typingSpeedUnit != "wpm") { wpmHover += " (" + result.wpm.toFixed(2) + " wpm)"; rawWpmHover += " (" + result.rawWpm.toFixed(2) + " wpm)"; } - $("#result .stats .wpm .top .text").text(Config.typingSpeedUnit); $("#result .stats .wpm .bottom").attr("aria-label", wpmHover); - if (inf) { - $("#result .stats .wpm .bottom").text("Infinite"); - } else { - $("#result .stats .wpm .bottom").text( - Math.round(typingSpeedUnit.fromWpm(result.wpm)) - ); - } - $("#result .stats .raw .bottom").text( - Math.round(typingSpeedUnit.fromWpm(result.rawWpm)) - ); $("#result .stats .raw .bottom").attr("aria-label", rawWpmHover); - $("#result .stats .acc .bottom").text(Math.floor(result.acc) + "%"); $("#result .stats .acc .bottom").attr( "aria-label", - `${result.acc === 100 ? "100" : Misc.roundTo2(result.acc).toFixed(2)}% (${ - TestInput.accuracy.correct - } correct / ${TestInput.accuracy.incorrect} incorrect)` + `${ + result.acc === 100 + ? "100" + : Format.percentage(result.acc, { showDecimalPlaces: true }) + } (${TestInput.accuracy.correct} correct / ${ + TestInput.accuracy.incorrect + } incorrect)` ); } } function updateConsistency(): void { + $("#result .stats .consistency .bottom").text( + Format.percentage(result.consistency) + ); if (Config.alwaysShowDecimalPlaces) { - $("#result .stats .consistency .bottom").text( - Misc.roundTo2(result.consistency).toFixed(2) + "%" - ); $("#result .stats .consistency .bottom").attr( "aria-label", - `${result.keyConsistency.toFixed(2)}% key` + Format.percentage(result.keyConsistency, { + showDecimalPlaces: true, + suffix: " key", + }) ); } else { - $("#result .stats .consistency .bottom").text( - Math.round(result.consistency) + "%" - ); $("#result .stats .consistency .bottom").attr( "aria-label", `${result.consistency}% (${result.keyConsistency}% key)` @@ -327,6 +319,7 @@ function updateTime(): void { "aria-label", `${result.afkDuration}s afk ${afkSecondsPercent}%` ); + if (Config.alwaysShowDecimalPlaces) { let time = Misc.roundTo2(result.testDuration).toFixed(2) + "s"; if (result.testDuration > 61) { @@ -416,11 +409,10 @@ export async function updateCrown(): Promise { Config.lazyMode, Config.funbox ); - const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); pbDiff = Math.abs(result.wpm - lpb); $("#result .stats .wpm .crown").attr( "aria-label", - "+" + Misc.roundTo2(typingSpeedUnit.fromWpm(pbDiff)) + "+" + Format.typingSpeed(pbDiff, { showDecimalPlaces: true }) ); } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 9a0aaac080ca..794f14aa9ae4 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -22,6 +22,7 @@ import { debounce } from "throttle-debounce"; import * as ResultWordHighlight from "../elements/result-word-highlight"; import * as ActivePage from "../states/active-page"; import html2canvas from "html2canvas"; +import Format from "../utils/format"; const debouncedZipfCheck = debounce(250, async () => { const supports = await Misc.checkIfLanguageSupportsZipf(Config.language); @@ -1299,9 +1300,8 @@ $(".pageTest #resultWordsHistory").on("mouseenter", ".words .word", (e) => { .replace(/>/g, ">")}
- ${Math.round( - getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm(burst) - )}${Config.typingSpeedUnit} + ${Format.typingSpeed(burst, { showDecimalPlaces: false })} + ${Config.typingSpeedUnit}
` ); diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index 073a9e676100..1631bf42cc7b 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -441,7 +441,6 @@ declare namespace MonkeyTypes { type TypingSpeedUnitSettings = { fromWpm: (number: number) => number; toWpm: (number: number) => number; - convertWithUnitSuffix: (number: number, withDecimals: boolean) => string; fullUnitString: string; histogramDataBucketSize: number; historyStepSize: number; diff --git a/frontend/src/ts/utils/format.ts b/frontend/src/ts/utils/format.ts new file mode 100644 index 000000000000..b9c63ae4a616 --- /dev/null +++ b/frontend/src/ts/utils/format.ts @@ -0,0 +1,67 @@ +import * as Misc from "./misc"; +import Config from "../config"; +import { get as getTypingSpeedUnit } from "../utils/typing-speed-units"; + +export type FormatOptions = { + showDecimalPlaces?: boolean; + suffix?: string; + fallback?: string; +}; + +const FORMAT_DEFAULT_OPTIONS: FormatOptions = { + suffix: "", + fallback: "-", + showDecimalPlaces: undefined, +}; + +export class Formatting { + constructor(private config: SharedTypes.Config) {} + + typingSpeed( + wpm: number | null | undefined, + formatOptions: FormatOptions = FORMAT_DEFAULT_OPTIONS + ): string { + const options = { ...FORMAT_DEFAULT_OPTIONS, ...formatOptions }; + if (wpm === undefined || wpm === null) return options.fallback ?? ""; + + const result = getTypingSpeedUnit(this.config.typingSpeedUnit).fromWpm(wpm); + + return this.number(result, options); + } + percentage( + percentage: number | null | undefined, + formatOptions: FormatOptions = {} + ): string { + const options = { ...FORMAT_DEFAULT_OPTIONS, ...formatOptions }; + options.suffix = "%" + (options.suffix ?? ""); + + return this.number(percentage, options); + } + + decimals( + value: number | null | undefined, + formatOptions: FormatOptions = {} + ): string { + const options = { ...FORMAT_DEFAULT_OPTIONS, ...formatOptions }; + return this.number(value, options); + } + private number( + value: number | null | undefined, + formatOptions: FormatOptions + ): string { + if (value === undefined || value === null) + return formatOptions.fallback ?? ""; + const suffix = formatOptions.suffix ?? ""; + + if ( + formatOptions.showDecimalPlaces !== undefined + ? formatOptions.showDecimalPlaces + : this.config.alwaysShowDecimalPlaces + ) { + return Misc.roundTo2(value).toFixed(2) + suffix; + } + return Math.round(value).toString() + suffix; + } +} + +export default new Formatting(Config); diff --git a/frontend/src/ts/utils/typing-speed-units.ts b/frontend/src/ts/utils/typing-speed-units.ts index 1e031ddd5796..c31b97f7c623 100644 --- a/frontend/src/ts/utils/typing-speed-units.ts +++ b/frontend/src/ts/utils/typing-speed-units.ts @@ -1,5 +1,3 @@ -import { roundTo2 } from "../utils/misc"; - class Unit implements MonkeyTypes.TypingSpeedUnitSettings { unit: SharedTypes.Config.TypingSpeedUnit; convertFactor: number; @@ -28,14 +26,6 @@ class Unit implements MonkeyTypes.TypingSpeedUnitSettings { toWpm(val: number): number { return val / this.convertFactor; } - - convertWithUnitSuffix(wpm: number, withDecimals: boolean): string { - if (withDecimals) { - return roundTo2(this.fromWpm(wpm)).toFixed(2) + " " + this.unit; - } else { - return Math.round(this.fromWpm(wpm)) + " " + this.unit; - } - } } const typingSpeedUnits: Record = { diff --git a/frontend/static/html/popups.html b/frontend/static/html/popups.html index 37d8356374c7..4c945d213b2e 100644 --- a/frontend/static/html/popups.html +++ b/frontend/static/html/popups.html @@ -244,7 +244,7 @@ words - wpm + wpm
accuracy From 3725e50d5d6d126d627406b2b34ba69db50b839f Mon Sep 17 00:00:00 2001 From: Andrey Kuznetsov Date: Mon, 19 Feb 2024 16:53:13 +0300 Subject: [PATCH 14/20] fix(language): typos in russian_10k.json (kae) (#5082) * Update russian_10k.json - fixed typos - removed duplicates * - fixed extra typos * remove duplicates * fix(language): typos in russian_10k.json --- frontend/static/languages/russian_10k.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/static/languages/russian_10k.json b/frontend/static/languages/russian_10k.json index d937e8d2a9a9..d2d9a1e3d18e 100644 --- a/frontend/static/languages/russian_10k.json +++ b/frontend/static/languages/russian_10k.json @@ -76,7 +76,7 @@ "вызвать", "попросить", "привести", - "счет", + "счёт", "молодая", "потерять", "достать", @@ -96,7 +96,7 @@ "поверить", "взглянуть", "пришлый", - "серьезный", + "серьёзный", "создать", "уехать", "повернуться", @@ -253,7 +253,6 @@ "председатель", "бороться", "изделие", - "счёт", "сельский", "революционный", "скорость", @@ -4138,7 +4137,7 @@ "поставка", "тушить", "почерк", - "почтённый", + "почтенный", "пощада", "предлагаться", "предполагаться", @@ -9116,7 +9115,7 @@ "весь", "есть", "надо", - "свое", + "своё", "идти", "ночь", "стол", @@ -9206,7 +9205,6 @@ "значить", "сторона", "человек", - "ребенок", "главный", "должный", "женщина", From 2ea40193439873650bea8679ab5c3a21543f668f Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 19 Feb 2024 14:57:20 +0100 Subject: [PATCH 15/20] feat: add copy missed words to result screen (fehmer) (#5086) * feat: Add copy missed words to result screen * remove margin * update icons --------- Co-authored-by: Miodec --- frontend/src/styles/test.scss | 1 - frontend/src/ts/test/test-ui.ts | 34 ++++++++++++++++++++-------- frontend/static/html/pages/test.html | 10 +++++++- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index cd017293b6cd..886a8cc8077e 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -567,7 +567,6 @@ margin-bottom: 1rem; .textButton { padding: 0; - margin-left: 0.5rem; } .heatmapLegend { margin-left: 0.5rem; diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 794f14aa9ae4..b489994ad183 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -1249,23 +1249,37 @@ $(".pageTest").on("click", "#saveScreenshotButton", () => { }); $(".pageTest #copyWordsListButton").on("click", async () => { + let words; + if (Config.mode === "zen") { + words = TestInput.input.history.join(" "); + } else { + words = (TestWords.words.get() as string[]) + .slice(0, TestInput.input.history.length) + .join(" "); + } + await copyToClipboard(words); +}); + +$(".pageTest #copyMissedWordsListButton").on("click", async () => { + let words; + if (Config.mode === "zen") { + words = TestInput.input.history.join(" "); + } else { + words = Object.keys(TestInput.missedWords ?? {}).join(" "); + } + await copyToClipboard(words); +}); + +async function copyToClipboard(content: string): Promise { try { - let words; - if (Config.mode === "zen") { - words = TestInput.input.history.join(" "); - } else { - words = (TestWords.words.get() as string[]) - .slice(0, TestInput.input.history.length) - .join(" "); - } - await navigator.clipboard.writeText(words); + await navigator.clipboard.writeText(content); Notifications.add("Copied to clipboard", 0, { duration: 2, }); } catch (e) { Notifications.add("Could not copy to clipboard: " + e, -1); } -}); +} $(".pageTest #toggleBurstHeatmap").on("click", async () => { UpdateConfig.setBurstHeatmap(!Config.burstHeatmap); diff --git a/frontend/static/html/pages/test.html b/frontend/static/html/pages/test.html index 9e85d5677c58..e2416baf1b25 100644 --- a/frontend/static/html/pages/test.html +++ b/frontend/static/html/pages/test.html @@ -303,7 +303,15 @@ data-balloon-pos="up" style="display: inline-block" > - + + + + Date: Mon, 19 Feb 2024 05:58:43 -0800 Subject: [PATCH 16/20] impr(funbox): add 46 group languages to wikipedia funbox (RealCyGuy) (#5078) --- frontend/src/ts/test/wikipedia.ts | 199 +++++++++++++++++++++++++++++- 1 file changed, 196 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/test/wikipedia.ts b/frontend/src/ts/test/wikipedia.ts index 40b6ba963c96..481c35c8f277 100644 --- a/frontend/src/ts/test/wikipedia.ts +++ b/frontend/src/ts/test/wikipedia.ts @@ -4,7 +4,62 @@ import { Section } from "../utils/misc"; export async function getTLD( languageGroup: MonkeyTypes.LanguageGroup -): Promise<"en" | "es" | "fr" | "de" | "pt" | "it" | "nl" | "pl"> { +): Promise< + | "en" + | "es" + | "fr" + | "de" + | "pt" + | "ar" + | "it" + | "la" + | "af" + | "ko" + | "ru" + | "pl" + | "cs" + | "sk" + | "uk" + | "lt" + | "id" + | "el" + | "tr" + | "th" + | "ta" + | "sl" + | "hr" + | "nl" + | "da" + | "hu" + | "no" + | "nn" + | "he" + | "ms" + | "ro" + | "fi" + | "et" + | "cy" + | "fa" + | "kk" + | "vi" + | "sv" + | "sr" + | "ka" + | "ca" + | "bg" + | "eo" + | "bn" + | "ur" + | "hy" + | "my" + | "hi" + | "mk" + | "uz" + | "be" + | "az" + | "lv" + | "eu" +> { // language group to tld switch (languageGroup.name) { case "english": @@ -22,15 +77,153 @@ export async function getTLD( case "portuguese": return "pt"; + case "arabic": + return "ar"; + case "italian": return "it"; - case "dutch": - return "nl"; + case "latin": + return "la"; + + case "afrikaans": + return "af"; + + case "korean": + return "ko"; + + case "russian": + return "ru"; case "polish": return "pl"; + case "czech": + return "cs"; + + case "slovak": + return "sk"; + + case "ukrainian": + return "uk"; + + case "lithuanian": + return "lt"; + + case "indonesian": + return "id"; + + case "greek": + return "el"; + + case "turkish": + return "tr"; + + case "thai": + return "th"; + + case "tamil": + return "ta"; + + case "slovenian": + return "sl"; + + case "croatian": + return "hr"; + + case "dutch": + return "nl"; + + case "danish": + return "da"; + + case "hungarian": + return "hu"; + + case "norwegian_bokmal": + return "no"; + + case "norwegian_nynorsk": + return "nn"; + + case "hebrew": + return "he"; + + case "malay": + return "ms"; + + case "romanian": + return "ro"; + + case "finnish": + return "fi"; + + case "estonian": + return "et"; + + case "welsh": + return "cy"; + + case "persian": + return "fa"; + + case "kazakh": + return "kk"; + + case "vietnamese": + return "vi"; + + case "swedish": + return "sv"; + + case "serbian": + return "sr"; + + case "georgian": + return "ka"; + + case "catalan": + return "ca"; + + case "bulgarian": + return "bg"; + + case "esperanto": + return "eo"; + + case "bangla": + return "bn"; + + case "urdu": + return "ur"; + + case "armenian": + return "hy"; + + case "myanmar": + return "my"; + + case "hindi": + return "hi"; + + case "macedonian": + return "mk"; + + case "uzbek": + return "uz"; + + case "belarusian": + return "be"; + + case "azerbaijani": + return "az"; + + case "latvian": + return "lv"; + + case "euskera": + return "eu"; + default: return "en"; } From a5bbdec90b48ae7c7619cafe827342bb1e11ad7d Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 19 Feb 2024 14:59:30 +0100 Subject: [PATCH 17/20] impr: provide all-time LB results during LB update (fehmer) (#5074) Try to provide LB results during the LB update. There is a very small time-frame where already running queries might fail during the update. For now we keep the 503 error in this cases and monitor how often this happens on production. --- backend/src/dal/leaderboards.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/backend/src/dal/leaderboards.ts b/backend/src/dal/leaderboards.ts index d4a62334e33c..8f4a9f383a9c 100644 --- a/backend/src/dal/leaderboards.ts +++ b/backend/src/dal/leaderboards.ts @@ -13,19 +13,28 @@ export async function get( skip: number, limit = 50 ): Promise { - if (leaderboardUpdating[`${language}_${mode}_${mode2}`]) return false; + //if (leaderboardUpdating[`${language}_${mode}_${mode2}`]) return false; + if (limit > 50 || limit <= 0) limit = 50; if (skip < 0) skip = 0; - const preset = await db - .collection( - `leaderboards.${language}.${mode}.${mode2}` - ) - .find() - .sort({ rank: 1 }) - .skip(skip) - .limit(limit) - .toArray(); - return preset; + try { + const preset = await db + .collection( + `leaderboards.${language}.${mode}.${mode2}` + ) + .find() + .sort({ rank: 1 }) + .skip(skip) + .limit(limit) + .toArray(); + return preset; + } catch (e) { + if (e.error === 175) { + //QueryPlanKilled, collection was removed during the query + return false; + } + throw e; + } } type GetRankResponse = { From bcf7d66db097e2fef017c2e5d563c00fa1185dd4 Mon Sep 17 00:00:00 2001 From: fitzsim Date: Mon, 19 Feb 2024 14:00:56 +0000 Subject: [PATCH 18/20] impr(funbox): add ` (grave accent, 96) and ~ (tilde, 126) to specials (#5073) --- frontend/src/ts/utils/misc.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 1841927ba2e5..3065683bb7e8 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -689,6 +689,8 @@ export function getSpecials(): string { const randLen = randomIntFromRange(1, 7); let ret = ""; const specials = [ + "`", + "~", "!", "@", "#", From 0986d6189ff540f784efc2b355e2ddb43cc016ec Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 19 Feb 2024 15:02:51 +0100 Subject: [PATCH 19/20] impr: add testWords and wordsHistory to copy result stats (#5085) * feat: add testWords and wordsHistory to copy result stats * fix --- frontend/src/ts/test/test-stats.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 6a316ddc5192..9ea34a76fe5e 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -60,6 +60,8 @@ export function getStats(): unknown { accuracy: TestInput.accuracy, keypressTimings: TestInput.keypressTimings, keyOverlap: TestInput.keyOverlap, + wordsHistory: TestWords.words.list.slice(0, TestInput.input.history.length), + inputHistory: TestInput.input.history, }; try { From 4cf0f014763652bb6846a2d3fcdb926d4f341724 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Feb 2024 17:04:20 +0100 Subject: [PATCH 20/20] add fe ts dep --- frontend/package-lock.json | 10 +++++----- frontend/package.json | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 991bc11f1ad9..3fd6522b3481 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -80,6 +80,7 @@ "ts-jest": "29.1.2", "ts-loader": "9.2.6", "ts-node-dev": "2.0.0", + "typescript": "5.3.3", "webpack": "5.72.0", "webpack-bundle-analyzer": "4.5.0", "webpack-cli": "4.10.0", @@ -21347,17 +21348,16 @@ "dev": true }, "node_modules/typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { diff --git a/frontend/package.json b/frontend/package.json index be11cf4e9abe..ff2844871cc8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -69,6 +69,7 @@ "ts-jest": "29.1.2", "ts-loader": "9.2.6", "ts-node-dev": "2.0.0", + "typescript": "5.3.3", "webpack": "5.72.0", "webpack-bundle-analyzer": "4.5.0", "webpack-cli": "4.10.0",