diff --git a/frontend/__tests__/controllers/preset-controller.spec.ts b/frontend/__tests__/controllers/preset-controller.spec.ts index baef3596b82f..816e9edab7e3 100644 --- a/frontend/__tests__/controllers/preset-controller.spec.ts +++ b/frontend/__tests__/controllers/preset-controller.spec.ts @@ -9,7 +9,7 @@ import * as ConfigUtils from "../../src/ts/config/utils"; import * as Persistence from "../../src/ts/config/persistence"; import * as Notifications from "../../src/ts/states/notifications"; import * as TestLogic from "../../src/ts/test/test-logic"; -import * as TagController from "../../src/ts/controllers/tag-controller"; +import * as Tags from "../../src/ts/collections/tags"; describe("PresetController", () => { describe("apply", () => { @@ -34,12 +34,9 @@ describe("PresetController", () => { "showSuccessNotification", ); const testRestartMock = vi.spyOn(TestLogic, "restart"); - const tagControllerClearMock = vi.spyOn(TagController, "clear"); - const tagControllerSetMock = vi.spyOn(TagController, "set"); - const tagControllerSaveActiveMock = vi.spyOn( - TagController, - "saveActiveToLocalStorage", - ); + const tagsClearMock = vi.spyOn(Tags, "clearActiveTags"); + const tagsSetMock = vi.spyOn(Tags, "setTagActive"); + const tagsSaveActiveMock = vi.spyOn(Tags, "saveActiveToLocalStorage"); beforeEach(() => { [ @@ -49,9 +46,9 @@ describe("PresetController", () => { configGetConfigChangesMock, notificationAddMock, testRestartMock, - tagControllerClearMock, - tagControllerSetMock, - tagControllerSaveActiveMock, + tagsClearMock, + tagsSetMock, + tagsSaveActiveMock, ].forEach((it) => it.mockClear()); configApplyMock.mockResolvedValue(); @@ -66,7 +63,7 @@ describe("PresetController", () => { //THEN expect(configApplyMock).toHaveBeenCalledWith(preset.config); - expect(tagControllerClearMock).toHaveBeenCalled(); + expect(tagsClearMock).toHaveBeenCalled(); expect(testRestartMock).toHaveBeenCalled(); expect(notificationAddMock).toHaveBeenCalledWith("Preset applied", { durationMs: 2000, @@ -84,20 +81,10 @@ describe("PresetController", () => { await PresetController.apply(preset._id); //THEN - expect(tagControllerClearMock).toHaveBeenCalled(); - expect(tagControllerSetMock).toHaveBeenNthCalledWith( - 1, - "tagOne", - true, - false, - ); - expect(tagControllerSetMock).toHaveBeenNthCalledWith( - 2, - "tagTwo", - true, - false, - ); - expect(tagControllerSaveActiveMock).toHaveBeenCalled(); + expect(tagsClearMock).toHaveBeenCalled(); + expect(tagsSetMock).toHaveBeenNthCalledWith(1, "tagOne", true, false); + expect(tagsSetMock).toHaveBeenNthCalledWith(2, "tagTwo", true, false); + expect(tagsSaveActiveMock).toHaveBeenCalled(); }); it("should ignore unknown preset", async () => { @@ -145,20 +132,10 @@ describe("PresetController", () => { await PresetController.apply(preset._id); //THEN - expect(tagControllerClearMock).toHaveBeenCalled(); - expect(tagControllerSetMock).toHaveBeenNthCalledWith( - 1, - "tagOne", - true, - false, - ); - expect(tagControllerSetMock).toHaveBeenNthCalledWith( - 2, - "tagTwo", - true, - false, - ); - expect(tagControllerSaveActiveMock).toHaveBeenCalled(); + expect(tagsClearMock).toHaveBeenCalled(); + expect(tagsSetMock).toHaveBeenNthCalledWith(1, "tagOne", true, false); + expect(tagsSetMock).toHaveBeenNthCalledWith(2, "tagTwo", true, false); + expect(tagsSaveActiveMock).toHaveBeenCalled(); }); const givenPreset = (partialPreset: Partial): Preset => { diff --git a/frontend/src/ts/collections/tags.ts b/frontend/src/ts/collections/tags.ts new file mode 100644 index 000000000000..4f9f93ad6614 --- /dev/null +++ b/frontend/src/ts/collections/tags.ts @@ -0,0 +1,463 @@ +import { UserTag } from "@monkeytype/schemas/users"; +import { queryCollectionOptions } from "@tanstack/query-db-collection"; +import { createCollection, useLiveQuery } from "@tanstack/solid-db"; +import { z } from "zod"; +import Ape from "../ape"; +import { queryClient } from "../queries"; +import { baseKey } from "../queries/utils/keys"; +import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; +import { IdSchema } from "@monkeytype/schemas/util"; +import { SnapshotResult } from "../constants/default-snapshot"; +import { + Mode, + Mode2, + PersonalBest, + PersonalBests, +} from "@monkeytype/schemas/shared"; +import { Difficulty } from "@monkeytype/schemas/configs"; +import { Language } from "@monkeytype/schemas/languages"; + +export type TagItem = UserTag & { active: boolean; display: string }; + +const queryKeys = { + root: () => [...baseKey("tags", { isUserSpecific: true })], +}; + +function toTagItem(tag: UserTag): TagItem { + return { + ...tag, + active: false, + display: tag.name.replaceAll("_", " "), + }; +} + +let seedData: TagItem[] = []; + +const tagsCollection = createCollection( + queryCollectionOptions({ + staleTime: Infinity, + startSync: true, + queryKey: queryKeys.root(), + + queryClient, + getKey: (it) => it._id, + onUpdate: async ({ transaction }) => { + const mutation = transaction.mutations[0]; + if (mutation === undefined) return { refetch: false }; + + const action = (mutation.metadata as Record)?.[ + "action" + ] as string; + + if (action === "updateTagName") { + const response = await Ape.users.editTag({ + body: { + tagId: mutation.key as string, + newName: mutation.modified.name, + }, + }); + if (response.status !== 200) { + throw new Error(`Failed to update tag: ${response.body.message}`); + } + } else if (action === "clearTagPBs") { + const response = await Ape.users.deleteTagPersonalBest({ + params: { tagId: mutation.key as string }, + }); + if (response.status !== 200) { + throw new Error(`Failed to clear tag PBs: ${response.body.message}`); + } + } + + return { refetch: false }; + }, + queryFn: async () => { + return seedData; + }, + onInsert: async ({ transaction }) => { + const newItems = transaction.mutations.map((m) => m.modified); + + const serverItems = await Promise.all( + newItems.map(async (it) => { + const response = await Ape.users.createTag({ + body: { tagName: it.name }, + }); + if (response.status !== 200) { + throw new Error(`Failed to add tag: ${response.body.message}`); + } + return toTagItem(response.body.data); + }), + ); + + tagsCollection.utils.writeBatch(() => { + serverItems.forEach((it) => tagsCollection.utils.writeInsert(it)); + }); + return { refetch: false }; + }, + onDelete: async ({ transaction }) => { + const ids = transaction.mutations.map((it) => it.key as string); + + await Promise.all( + ids.map(async (id) => { + const response = await Ape.users.deleteTag({ + params: { tagId: id }, + }); + if (response.status !== 200) { + throw new Error(`Failed to delete tag: ${response.body.message}`); + } + }), + ); + + tagsCollection.utils.writeBatch(() => { + ids.forEach((id) => tagsCollection.utils.writeDelete(id)); + }); + return { refetch: false }; + }, + }), +); + +// oxlint-disable-next-line typescript/explicit-function-return-type +export const useTagsLiveQuery = () => + useLiveQuery((q) => { + return q.from({ + collection: tagsCollection, + }); + }); + +// --- CRUD helpers --- + +export async function insertTag(tagName: string): Promise { + const transaction = tagsCollection.insert({ + _id: crypto.randomUUID(), + name: tagName, + personalBests: { time: {}, words: {}, quote: {}, zen: {}, custom: {} }, + active: false, + display: tagName.replaceAll("_", " "), + }); + await transaction.isPersisted.promise; +} + +export async function updateTagName( + tagId: string, + newName: string, +): Promise { + const transaction = tagsCollection.update( + tagId, + { metadata: { action: "updateTagName" } }, + (tag) => { + tag.name = newName; + tag.display = newName.replaceAll("_", " "); + }, + ); + await transaction.isPersisted.promise; +} + +export async function clearTagPBs(tagId: string): Promise { + const transaction = tagsCollection.update( + tagId, + { metadata: { action: "clearTagPBs" } }, + (tag) => { + tag.personalBests = { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; + }, + ); + await transaction.isPersisted.promise; +} + +export async function deleteTag(tagId: string): Promise { + const transaction = tagsCollection.delete(tagId); + await transaction.isPersisted.promise; +} + +export function getTags(): TagItem[] { + return [...tagsCollection.values()]; +} + +export function getTag(id: string): TagItem | undefined { + return tagsCollection.get(id); +} + +export function getActiveTags(): TagItem[] { + return [...tagsCollection.values()].filter((tag) => tag.active); +} + +export function seedFromUserData(userTags: UserTag[]): void { + const activeIds = activeTagsLS.get(); + + seedData = userTags + .map((tag) => ({ + ...toTagItem(tag), + active: activeIds.includes(tag._id), + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + tagsCollection.utils.writeBatch(() => { + tagsCollection.forEach((tag) => { + tagsCollection.utils.writeDelete(tag._id); + }); + seedData.forEach((item) => { + tagsCollection.utils.writeInsert(item); + }); + }); +} + +// --- Active state --- + +const activeTagsLS = new LocalStorageWithSchema({ + key: "activeTags", + schema: z.array(IdSchema), + fallback: [], +}); + +export function saveActiveToLocalStorage(): void { + const activeIds: string[] = []; + tagsCollection.forEach((t) => { + if (t.active) activeIds.push(t._id); + }); + activeTagsLS.set(activeIds); +} + +export function toggleTagActive(tagId: string, nosave = false): void { + const tag = tagsCollection.get(tagId); + if (tag === undefined) return; + tagsCollection.utils.writeUpdate({ ...tag, active: !tag.active }); + if (!nosave) saveActiveToLocalStorage(); +} + +export function setTagActive( + tagId: string, + state: boolean, + nosave = false, +): void { + const tag = tagsCollection.get(tagId); + if (tag === undefined) return; + tagsCollection.utils.writeUpdate({ ...tag, active: state }); + if (!nosave) saveActiveToLocalStorage(); +} + +export function clearActiveTags(nosave = false): void { + tagsCollection.utils.writeBatch(() => { + tagsCollection.forEach((tag) => { + if (tag.active) { + tagsCollection.utils.writeUpdate({ ...tag, active: false }); + } + }); + }); + if (!nosave) saveActiveToLocalStorage(); +} + +// --- Personal bests --- + +export function getLocalTagPB( + tagId: string, + mode: M, + mode2: Mode2, + punctuation: boolean, + numbers: boolean, + language: string, + difficulty: Difficulty, + lazyMode: boolean, +): number { + const tag = getTag(tagId); + if (tag === undefined) return 0; + + const personalBests = (tag.personalBests?.[mode]?.[mode2] ?? + []) as PersonalBest[]; + + return ( + personalBests.find( + (pb) => + (pb.punctuation ?? false) === punctuation && + (pb.numbers ?? false) === numbers && + pb.difficulty === difficulty && + pb.language === language && + (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)), + )?.wpm ?? 0 + ); +} + +export function saveLocalTagPB( + tagId: string, + mode: M, + mode2: Mode2, + punctuation: boolean, + numbers: boolean, + language: Language, + difficulty: Difficulty, + lazyMode: boolean, + wpm: number, + acc: number, + raw: number, + consistency: number, +): void { + if (mode === "quote") return; + + const collectionTag = tagsCollection.get(tagId); + if (collectionTag === undefined) return; + const tag = structuredClone(collectionTag); + + tag.personalBests ??= { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; + + tag.personalBests[mode] ??= { + [mode2]: [], + }; + + tag.personalBests[mode][mode2] ??= + [] as unknown as PersonalBests[M][Mode2]; + + try { + let found = false; + + (tag.personalBests[mode][mode2] as unknown as PersonalBest[]).forEach( + (pb) => { + if ( + (pb.punctuation ?? false) === punctuation && + (pb.numbers ?? false) === numbers && + pb.difficulty === difficulty && + pb.language === language && + (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)) + ) { + found = true; + pb.wpm = wpm; + pb.acc = acc; + pb.raw = raw; + pb.timestamp = Date.now(); + pb.consistency = consistency; + pb.lazyMode = lazyMode; + } + }, + ); + if (!found) { + (tag.personalBests[mode][mode2] as unknown as PersonalBest[]).push({ + language, + difficulty, + lazyMode, + punctuation, + numbers, + wpm, + acc, + raw, + timestamp: Date.now(), + consistency, + }); + } + } catch { + tag.personalBests = { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; + tag.personalBests[mode][mode2] = [ + { + language, + difficulty, + lazyMode, + punctuation, + numbers, + wpm, + acc, + raw, + timestamp: Date.now(), + consistency, + }, + ] as unknown as PersonalBests[M][Mode2]; + } + + // using utils.writeUpdate instead of collection.update because we dont need to send this to the API + // the result saving already updates the tag pb in the db + tagsCollection.utils.writeUpdate(tag); +} + +export function updateLocalTagPB( + tagId: string, + mode: M, + mode2: Mode2, + punctuation: boolean, + numbers: boolean, + language: Language, + difficulty: Difficulty, + lazyMode: boolean, + results: SnapshotResult[], +): void { + const tag = getTag(tagId); + if (tag === undefined) return; + + const pb = { + wpm: 0, + acc: 0, + rawWpm: 0, + consistency: 0, + }; + + results.forEach((result) => { + if (result.tags.includes(tagId) && result.wpm > pb.wpm) { + if ( + result.mode === mode && + result.mode2 === mode2 && + result.punctuation === punctuation && + result.numbers === numbers && + result.language === language && + result.difficulty === difficulty && + result.lazyMode === lazyMode + ) { + pb.wpm = result.wpm; + pb.acc = result.acc; + pb.rawWpm = result.rawWpm; + pb.consistency = result.consistency; + } + } + }); + + saveLocalTagPB( + tagId, + mode, + mode2, + punctuation, + numbers, + language, + difficulty, + lazyMode, + pb.wpm, + pb.acc, + pb.rawWpm, + pb.consistency, + ); +} + +export function getActiveTagsPB( + mode: M, + mode2: Mode2, + punctuation: boolean, + numbers: boolean, + language: string, + difficulty: Difficulty, + lazyMode: boolean, +): number { + let tagPbWpm = 0; + for (const tag of getActiveTags()) { + const currTagPB = getLocalTagPB( + tag._id, + mode, + mode2, + punctuation, + numbers, + language, + difficulty, + lazyMode, + ); + if (currTagPB > tagPbWpm) tagPbWpm = currTagPB; + } + return tagPbWpm; +} diff --git a/frontend/src/ts/commandline/lists/tags.ts b/frontend/src/ts/commandline/lists/tags.ts index 4ea7b7ea1792..e2fb468938f7 100644 --- a/frontend/src/ts/commandline/lists/tags.ts +++ b/frontend/src/ts/commandline/lists/tags.ts @@ -1,7 +1,11 @@ -import * as DB from "../../db"; import * as EditTagsPopup from "../../modals/edit-tag"; import * as ModesNotice from "../../elements/modes-notice"; -import * as TagController from "../../controllers/tag-controller"; +import { + getTags, + getTag, + clearActiveTags, + toggleTagActive, +} from "../../collections/tags"; import { Config } from "../../config/store"; import * as PaceCaret from "../../test/pace-caret"; import { isAuthenticated } from "../../states/core"; @@ -28,30 +32,17 @@ const commands: Command[] = [ ]; function update(): void { - const snapshot = DB.getSnapshot(); + const tags = getTags(); subgroup.list = []; - if ( - snapshot !== undefined && - snapshot.tags !== undefined && - snapshot.tags.length > 0 - ) { + if (tags.length > 0) { subgroup.list.push({ id: "clearTags", display: `Clear tags`, icon: "fa-times", sticky: true, exec: async (): Promise => { - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - - snapshot.tags = snapshot.tags?.map((tag) => { - tag.active = false; - - return tag; - }); - - DB.setSnapshot(snapshot); + clearActiveTags(); if ( Config.paceCaret === "average" || Config.paceCaret === "tagPb" || @@ -60,23 +51,19 @@ function update(): void { await PaceCaret.init(); } void ModesNotice.update(); - TagController.saveActiveToLocalStorage(); }, }); - for (const tag of snapshot.tags) { + for (const tag of tags) { subgroup.list.push({ id: "toggleTag" + tag._id, display: tag.display, sticky: true, active: () => { - return ( - DB.getSnapshot()?.tags?.find((t) => t._id === tag._id)?.active ?? - false - ); + return getTag(tag._id)?.active ?? false; }, exec: async (): Promise => { - TagController.toggle(tag._id); + toggleTagActive(tag._id); if ( Config.paceCaret === "average" || diff --git a/frontend/src/ts/constants/default-snapshot.ts b/frontend/src/ts/constants/default-snapshot.ts index 7a0ecec16eff..0818ed4ee397 100644 --- a/frontend/src/ts/constants/default-snapshot.ts +++ b/frontend/src/ts/constants/default-snapshot.ts @@ -2,7 +2,6 @@ import { ResultFilters, User, UserProfileDetails, - UserTag, } from "@monkeytype/schemas/users"; import { getDefaultConfig } from "./default-config"; import { Mode } from "@monkeytype/schemas/shared"; @@ -16,11 +15,6 @@ import { Preset } from "@monkeytype/schemas/presets"; import { Language } from "@monkeytype/schemas/languages"; import { ConnectionStatus } from "@monkeytype/schemas/connections"; -export type SnapshotUserTag = UserTag & { - active?: boolean; - display: string; -}; - export type SnapshotResult = Omit< Result, | "_id" @@ -78,7 +72,6 @@ export type Snapshot = Omit< filterPresets: ResultFilters[]; isPremium: boolean; streakHourOffset?: number; - tags: SnapshotUserTag[]; presets: SnapshotPreset[]; results?: SnapshotResult[]; xp: number; @@ -107,7 +100,6 @@ const defaultSnap = { config: getDefaultConfig(), customThemes: [], presets: [], - tags: [], banned: undefined, verified: undefined, lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } }, diff --git a/frontend/src/ts/controllers/preset-controller.ts b/frontend/src/ts/controllers/preset-controller.ts index fce7a6c02732..7475af259362 100644 --- a/frontend/src/ts/controllers/preset-controller.ts +++ b/frontend/src/ts/controllers/preset-controller.ts @@ -8,9 +8,14 @@ import { showSuccessNotification, } from "../states/notifications"; import * as TestLogic from "../test/test-logic"; -import * as TagController from "./tag-controller"; +import { + clearActiveTags, + setTagActive, + saveActiveToLocalStorage, +} from "../collections/tags"; import { SnapshotPreset } from "../constants/default-snapshot"; import { saveFullConfigToLocalStorage } from "../config/persistence"; +import * as ModesNotice from "../elements/modes-notice"; export async function apply(_id: string): Promise { const snapshot = DB.getSnapshot(); @@ -34,14 +39,15 @@ export async function apply(_id: string): Promise { !isPartialPreset(presetToApply) || presetToApply.settingGroups?.includes("behavior") ) { - TagController.clear(true); + clearActiveTags(true); if (presetToApply.config.tags) { for (const tagId of presetToApply.config.tags) { - TagController.set(tagId, true, false); + setTagActive(tagId, true, false); } - TagController.saveActiveToLocalStorage(); + saveActiveToLocalStorage(); } } + void ModesNotice.update(); TestLogic.restart(); showSuccessNotification("Preset applied", { durationMs: 2000 }); saveFullConfigToLocalStorage(); diff --git a/frontend/src/ts/controllers/tag-controller.ts b/frontend/src/ts/controllers/tag-controller.ts deleted file mode 100644 index c1a414c9eba0..000000000000 --- a/frontend/src/ts/controllers/tag-controller.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { z } from "zod"; -import * as DB from "../db"; -import * as ModesNotice from "../elements/modes-notice"; -import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; -import { IdSchema } from "@monkeytype/schemas/util"; -import { authEvent } from "../events/auth"; - -const activeTagsLS = new LocalStorageWithSchema({ - key: "activeTags", - schema: z.array(IdSchema), - fallback: [], -}); - -export function saveActiveToLocalStorage(): void { - const tags: string[] = []; - - DB.getSnapshot()?.tags?.forEach((tag) => { - if (tag.active === true) { - tags.push(tag._id); - } - }); - - activeTagsLS.set(tags); -} - -export function clear(nosave = false): void { - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - - snapshot.tags = snapshot.tags?.map((tag) => { - tag.active = false; - - return tag; - }); - - DB.setSnapshot(snapshot); - void ModesNotice.update(); - if (!nosave) saveActiveToLocalStorage(); -} - -export function set(tagid: string, state: boolean, nosave = false): void { - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - - snapshot.tags = snapshot.tags?.map((tag) => { - if (tag._id === tagid) { - tag.active = state; - } - - return tag; - }); - - DB.setSnapshot(snapshot); - void ModesNotice.update(); - if (!nosave) saveActiveToLocalStorage(); -} - -export function toggle(tagid: string, nosave = false): void { - DB.getSnapshot()?.tags?.forEach((tag) => { - if (tag._id === tagid) { - if (tag.active === undefined) { - tag.active = true; - } else { - tag.active = !tag.active; - } - } - }); - void ModesNotice.update(); - if (!nosave) saveActiveToLocalStorage(); -} - -export function loadActiveFromLocalStorage(): void { - const newTags = activeTagsLS.get(); - for (const tag of newTags) { - toggle(tag, true); - } - saveActiveToLocalStorage(); -} - -authEvent.subscribe((event) => { - if (event.type === "snapshotUpdated" && event.data.isInitial) { - loadActiveFromLocalStorage(); - } -}); diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index d07e2f126c58..6d2830a5ad9b 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -25,7 +25,6 @@ import { Snapshot, SnapshotPreset, SnapshotResult, - SnapshotUserTag, } from "./constants/default-snapshot"; import { getFirstDayOfTheWeek } from "./utils/date-and-time"; import { Language } from "@monkeytype/schemas/languages"; @@ -42,6 +41,7 @@ import { import { XpBreakdown } from "@monkeytype/schemas/results"; import { setXpBarData } from "./states/header"; import { FunboxMetadata } from "@monkeytype/funbox"; +import { getActiveTags, seedFromUserData } from "./collections/tags"; let dbSnapshot: Snapshot | undefined; const firstDayOfTheWeek = getFirstDayOfTheWeek(); @@ -196,40 +196,7 @@ export async function initSnapshot(): Promise { snap.customThemes = userData.customThemes ?? []; - // 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 PersonalBests] ??= {}; - // } - // }); - - // snap.tags = userDataTags; - - snap.tags = - userData.tags?.map((tag) => ({ - ...tag, - display: tag.name.replace(/_/g, " "), - })) ?? []; - - snap.tags = snap.tags?.sort((a, b) => { - if (a.name > b.name) { - return 1; - } else if (a.name < b.name) { - return -1; - } else { - return 0; - } - }); + seedFromUserData(userData.tags ?? []); if (presetsData !== undefined && presetsData !== null) { const presetsWithDisplay = presetsData.map((preset) => { @@ -429,10 +396,8 @@ export async function getUserAverage10( function cont(): [number, number] { const activeTagIds: string[] = []; - snapshot?.tags?.forEach((tag) => { - if (tag.active === true) { - activeTagIds.push(tag._id); - } + getActiveTags().forEach((tag) => { + activeTagIds.push(tag._id); }); let wpmSum = 0; @@ -514,10 +479,8 @@ export async function getUserDailyBest( function cont(): number { const activeTagIds: string[] = []; - snapshot?.tags?.forEach((tag) => { - if (tag.active === true) { - activeTagIds.push(tag._id); - } + getActiveTags().forEach((tag) => { + activeTagIds.push(tag._id); }); let bestWpm = 0; @@ -564,37 +527,6 @@ export async function getUserDailyBest( return retval; } -export async function getActiveTagsPB( - mode: M, - mode2: Mode2, - punctuation: boolean, - numbers: boolean, - language: string, - difficulty: Difficulty, - lazyMode: boolean, -): Promise { - const snapshot = getSnapshot(); - if (!snapshot) return 0; - - let tagPbWpm = 0; - for (const tag of snapshot.tags) { - if (!tag.active) continue; - const currTagPB = await getLocalTagPB( - tag._id, - mode, - mode2, - punctuation, - numbers, - language, - difficulty, - lazyMode, - ); - if (currTagPB > tagPbWpm) tagPbWpm = currTagPB; - } - - return tagPbWpm; -} - export async function getLocalPB( mode: M, mode2: Mode2, @@ -700,164 +632,7 @@ function saveLocalPB( } } -export async function getLocalTagPB( - tagId: string, - mode: M, - mode2: Mode2, - punctuation: boolean, - numbers: boolean, - language: string, - difficulty: Difficulty, - lazyMode: boolean, -): Promise { - if (dbSnapshot === null) return 0; - - let ret = 0; - - const filteredtag = (getSnapshot()?.tags ?? []).find((t) => t._id === tagId); - - if (filteredtag === undefined) return ret; - - filteredtag.personalBests ??= { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; - - filteredtag.personalBests[mode] ??= { - [mode2]: [], - }; - - filteredtag.personalBests[mode][mode2] ??= - [] as unknown as PersonalBests[M][Mode2]; - - const personalBests = (filteredtag.personalBests[mode][mode2] ?? - []) as PersonalBest[]; - - ret = - personalBests.find( - (pb) => - (pb.punctuation ?? false) === punctuation && - (pb.numbers ?? false) === numbers && - pb.difficulty === difficulty && - pb.language === language && - (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)), - )?.wpm ?? 0; - - return ret; -} - -export async function saveLocalTagPB( - tagId: string, - mode: M, - mode2: Mode2, - punctuation: boolean, - numbers: boolean, - language: Language, - difficulty: Difficulty, - lazyMode: boolean, - wpm: number, - acc: number, - raw: number, - consistency: number, -): Promise { - if (!dbSnapshot) return; - if (mode === "quote") return; - function cont(): void { - const filteredtag = dbSnapshot?.tags?.find( - (t) => t._id === tagId, - ) as SnapshotUserTag; - - filteredtag.personalBests ??= { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; - - filteredtag.personalBests[mode] ??= { - [mode2]: [], - }; - - filteredtag.personalBests[mode][mode2] ??= - [] as unknown as PersonalBests[M][Mode2]; - - try { - let found = false; - - ( - filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] - ).forEach((pb) => { - if ( - (pb.punctuation ?? false) === punctuation && - (pb.numbers ?? false) === numbers && - pb.difficulty === difficulty && - pb.language === language && - (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)) - ) { - found = true; - pb.wpm = wpm; - pb.acc = acc; - pb.raw = raw; - pb.timestamp = Date.now(); - pb.consistency = consistency; - pb.lazyMode = lazyMode; - } - }); - if (!found) { - //nothing found - ( - filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] - ).push({ - language, - difficulty, - lazyMode, - punctuation, - numbers, - wpm, - acc, - raw, - timestamp: Date.now(), - consistency, - }); - } - } catch (e) { - //that mode or mode2 is not found - filteredtag.personalBests = { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; - filteredtag.personalBests[mode][mode2] = [ - { - language: language, - difficulty: difficulty, - lazyMode: lazyMode, - punctuation: punctuation, - numbers: numbers, - wpm: wpm, - acc: acc, - raw: raw, - timestamp: Date.now(), - consistency: consistency, - }, - ] as unknown as PersonalBests[M][Mode2]; - } - } - - if (dbSnapshot !== null) { - cont(); - } - - return; -} - -export function deleteLocalTag(tagId: string): void { +export function removeTagFromResults(tagId: string): void { getSnapshot()?.results?.forEach((result) => { const tagIndex = result.tags.indexOf(tagId); if (tagIndex > -1) { @@ -866,64 +641,6 @@ export function deleteLocalTag(tagId: string): void { }); } -export async function updateLocalTagPB( - tagId: string, - mode: M, - mode2: Mode2, - punctuation: boolean, - numbers: boolean, - language: Language, - difficulty: Difficulty, - lazyMode: boolean, -): Promise { - if (dbSnapshot === null) return; - - const filteredtag = (getSnapshot()?.tags ?? []).find((t) => t._id === tagId); - - if (filteredtag === undefined) return; - - const pb = { - wpm: 0, - acc: 0, - rawWpm: 0, - consistency: 0, - }; - - getSnapshot()?.results?.forEach((result) => { - if (result.tags.includes(tagId) && result.wpm > pb.wpm) { - if ( - result.mode === mode && - result.mode2 === mode2 && - result.punctuation === punctuation && - result.numbers === numbers && - result.language === language && - result.difficulty === difficulty && - result.lazyMode === lazyMode - ) { - pb.wpm = result.wpm; - pb.acc = result.acc; - pb.rawWpm = result.rawWpm; - pb.consistency = result.consistency; - } - } - }); - - await saveLocalTagPB( - tagId, - mode, - mode2, - punctuation, - numbers, - language, - difficulty, - lazyMode, - pb.wpm, - pb.acc, - pb.rawWpm, - pb.consistency, - ); -} - export async function updateLbMemory( mode: M, mode2: Mode2 | undefined, diff --git a/frontend/src/ts/elements/account/result-filters.ts b/frontend/src/ts/elements/account/result-filters.ts index e2ccf7ecc702..c20d2d910740 100644 --- a/frontend/src/ts/elements/account/result-filters.ts +++ b/frontend/src/ts/elements/account/result-filters.ts @@ -20,7 +20,7 @@ import { import { LocalStorageWithSchema } from "../../utils/local-storage-with-schema"; import defaultResultFilters from "../../constants/default-result-filters"; import { getAllFunboxes } from "@monkeytype/funbox"; -import { Snapshot } from "../../constants/default-snapshot"; +import { getTags, getActiveTags, getTag } from "../../collections/tags"; import { LanguageList } from "../../constants/languages"; import { authEvent } from "../../events/auth"; import { sanitize } from "../../utils/sanitize"; @@ -276,11 +276,7 @@ function setAllFilters(group: ResultFiltersGroup, value: boolean): void { } export function loadTags(): void { - const snapshot = DB.getSnapshot(); - - if (snapshot === undefined) return; - - snapshot.tags.forEach((tag) => { + getTags().forEach((tag) => { defaultResultFilters.tags[tag._id] = true; }); } @@ -431,13 +427,7 @@ export function updateActive(): void { ret += aboveChartDisplay.tags?.array ?.map((id) => { if (id === "none") return id; - const snapshot = DB.getSnapshot(); - if (snapshot === undefined) return id; - const name = snapshot.tags?.find((t) => t._id === id); - if (name !== undefined) { - return snapshot.tags?.find((t) => t._id === id)?.display; - } - return name; + return getTag(id)?.display ?? id; }) .join(", "); } else { @@ -676,11 +666,9 @@ qs(".pageAccount .topFilters button.currentConfigFilter")?.on("click", () => { filters.tags["none"] = true; - DB.getSnapshot()?.tags?.forEach((tag) => { - if (tag.active === true) { - filters.tags["none"] = false; - filters.tags[tag._id] = true; - } + getActiveTags().forEach((tag) => { + filters.tags["none"] = false; + filters.tags[tag._id] = true; }); filters.date.all = true; @@ -771,13 +759,9 @@ let selectChangeCallbackFn: () => void = () => { }; export function updateTagsDropdownOptions(): void { - const snapshot = DB.getSnapshot(); - - if (snapshot === undefined) { - return; - } + const tags = getTags(); - const newTags = snapshot.tags.filter( + const newTags = tags.filter( (it) => defaultResultFilters.tags[it._id] === undefined, ); if (newTags.length > 0) { @@ -807,7 +791,7 @@ export function updateTagsDropdownOptions(): void { html += ""; html += ""; - for (const tag of snapshot.tags) { + for (const tag of tags) { html += ``; } @@ -819,10 +803,7 @@ let buttonsAppended = false; export async function appendDropdowns( selectChangeCallback: () => void, ): Promise { - //snapshot at this point is guaranteed to exist - const snapshot = DB.getSnapshot() as Snapshot; - - tagDropdownUpdate(snapshot); + tagDropdownUpdate(); if (buttonsAppended) return; @@ -895,12 +876,12 @@ export async function appendDropdowns( buttonsAppended = true; } -function tagDropdownUpdate(snapshot: Snapshot): void { +function tagDropdownUpdate(): void { const tagsSection = qs( ".pageAccount .content .filterButtons .buttonsAndTitle.tags", ); - if (snapshot.tags.length === 0) { + if (getTags().length === 0) { tagsSection?.hide(); if (groupSelects["tags"]) { groupSelects["tags"].destroy(); diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index eda13c5988d0..93d26d8415fc 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -2,6 +2,7 @@ import * as PaceCaret from "../test/pace-caret"; import * as TestState from "../test/test-state"; import * as DB from "../db"; import * as Last10Average from "../elements/last-10-average"; +import { getActiveTags } from "../collections/tags"; import { Config } from "../config/store"; import * as TestWords from "../test/test-words"; import { configEvent, type ConfigEventKey } from "../events/config"; @@ -302,10 +303,8 @@ export async function update(): Promise { let tagsString = ""; try { - DB.getSnapshot()?.tags?.forEach((tag) => { - if (tag.active === true) { - tagsString += tag.display + ", "; - } + getActiveTags().forEach((tag) => { + tagsString += tag.display + ", "; }); if (tagsString !== "") { diff --git a/frontend/src/ts/event-handlers/test.ts b/frontend/src/ts/event-handlers/test.ts index a55218f6ab3a..6c86716eaab2 100644 --- a/frontend/src/ts/event-handlers/test.ts +++ b/frontend/src/ts/event-handlers/test.ts @@ -1,7 +1,7 @@ import * as Commandline from "../commandline/commandline"; import { Config } from "../config/store"; -import * as DB from "../db"; import * as EditResultTagsModal from "../modals/edit-result-tags"; +import { getTags } from "../collections/tags"; import * as TestWords from "../test/test-words"; import { showNoticeNotification, @@ -33,7 +33,7 @@ testPage?.onChild("click", "#testModesNotice .textButton", async (event) => { }); testPage?.onChild("click", ".tags .editTagsButton", () => { - if ((DB.getSnapshot()?.tags?.length ?? 0) > 0) { + if (getTags().length > 0) { const resultid = qs(".pageTest .tags .editTagsButton")?.getAttribute("data-result-id") ?? ""; diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 6b4d3f6e54d7..e8a88ffbe3e4 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -1,5 +1,6 @@ import Ape from "../ape"; import * as DB from "../db"; +import { getActiveTags } from "../collections/tags"; import { showLoaderBar, hideLoaderBar } from "../states/loader-bar"; import * as Settings from "../pages/settings"; import { @@ -397,11 +398,7 @@ function getConfigChanges(): Partial { state.presetType === "partial" ? getPartialConfigChanges(getConfigChangesFromConfig()) : getConfigChangesFromConfig(); - const tags = DB.getSnapshot()?.tags ?? []; - - const activeTagIds: string[] = tags - .filter((tag) => tag.active) - .map((tag) => tag._id); + const activeTagIds: string[] = getActiveTags().map((tag) => tag._id); const setTags: boolean = state.presetType === "full" || state.checkboxes.get("behavior") === true; diff --git a/frontend/src/ts/modals/edit-result-tags.ts b/frontend/src/ts/modals/edit-result-tags.ts index 2a844bb4b43d..6da97a3c3bc8 100644 --- a/frontend/src/ts/modals/edit-result-tags.ts +++ b/frontend/src/ts/modals/edit-result-tags.ts @@ -10,6 +10,7 @@ import * as AccountPage from "../pages/account"; import { areUnsortedArraysEqual } from "../utils/arrays"; import * as TestResult from "../test/result"; import AnimatedModal from "../utils/animated-modal"; +import { getTags, getTag, updateLocalTagPB } from "../collections/tags"; type State = { resultId: string; @@ -64,14 +65,11 @@ function appendButtons(): void { return; } - const tagIds = new Set([ - ...(DB.getSnapshot()?.tags.map((tag) => tag._id) ?? []), - ...state.tags, - ]); + const tagIds = new Set([...getTags().map((tag) => tag._id), ...state.tags]); buttonsEl.empty(); for (const tagId of tagIds) { - const tag = DB.getSnapshot()?.tags.find((tag) => tag._id === tagId); + const tag = getTag(tagId); const button = document.createElement("button"); button.classList.add("toggleTag"); button.setAttribute("data-tag-id", tagId); @@ -126,6 +124,8 @@ async function save(): Promise { showSuccessNotification("Tags updated", { durationMs: 2000 }); + const results = DB.getSnapshot()?.results ?? []; + DB.getSnapshot()?.results?.forEach((result) => { if (result._id === state.resultId) { const tagsToUpdate = [ @@ -134,7 +134,7 @@ async function save(): Promise { ]; result.tags = state.tags; tagsToUpdate.forEach((tag) => { - void DB.updateLocalTagPB( + updateLocalTagPB( tag, result.mode, result.mode2, @@ -143,6 +143,7 @@ async function save(): Promise { result.language, result.difficulty, result.lazyMode, + results, ); }); } diff --git a/frontend/src/ts/modals/edit-tag.ts b/frontend/src/ts/modals/edit-tag.ts index d42507fd7d76..2a6a62bb47e0 100644 --- a/frontend/src/ts/modals/edit-tag.ts +++ b/frontend/src/ts/modals/edit-tag.ts @@ -1,15 +1,19 @@ -import Ape from "../ape"; import * as DB from "../db"; import * as Settings from "../pages/settings"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { SimpleModal, TextInput } from "../elements/simple-modal"; import { TagNameSchema } from "@monkeytype/schemas/users"; -import { SnapshotUserTag } from "../constants/default-snapshot"; import { IsValidResponse } from "../types/validation"; +import { + insertTag, + deleteTag, + updateTagName, + clearTagPBs, +} from "../collections/tags"; import { normalizeName } from "../utils/strings"; -function getTagFromSnapshot(tagId: string): SnapshotUserTag | undefined { - return DB.getSnapshot()?.tags.find((tag) => tag._id === tagId); +function errorMessage(e: unknown): string { + return e instanceof Error ? e.message : String(e); } const tagNameValidation = async (tagName: string): Promise => { @@ -33,32 +37,17 @@ const actionModals: Record = { buttonText: "add", execFn: async (_thisPopup, propTagName) => { const tagName = TagNameSchema.parse(normalizeName(propTagName)); - const response = await Ape.users.createTag({ body: { tagName } }); - if (response.status !== 200) { + try { + await insertTag(tagName); + } catch (e) { return { status: "error", - message: - "Failed to add tag: " + - response.body.message.replace(tagName, propTagName), - notificationOptions: { response }, + message: "Failed to add tag: " + errorMessage(e), }; } - DB.getSnapshot()?.tags?.push({ - display: tagName.replace(/_/g, " "), - name: tagName, - _id: response.body.data._id, - personalBests: { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }, - }); void Settings.update(); - return { status: "success", message: `Tag added` }; }, }), @@ -80,25 +69,15 @@ const actionModals: Record = { const tagName = TagNameSchema.parse(normalizeName(propTagName)); const tagId = _thisPopup.parameters[1] as string; - const response = await Ape.users.editTag({ - body: { tagId, newName: tagName }, - }); - - if (response.status !== 200) { + try { + await updateTagName(tagId, tagName); + } catch (e) { return { status: "error", - message: "Failed to edit tag", - notificationOptions: { response }, + message: "Failed to update tag: " + errorMessage(e), }; } - const matchingTag = getTagFromSnapshot(tagId); - - if (matchingTag !== undefined) { - matchingTag.name = tagName; - matchingTag.display = tagName.replace(/_/g, " "); - } - void Settings.update(); return { status: "success", message: `Tag updated` }; @@ -113,23 +92,17 @@ const actionModals: Record = { }, execFn: async (_thisPopup) => { const tagId = _thisPopup.parameters[1] as string; - const response = await Ape.users.deleteTag({ params: { tagId } }); - if (response.status !== 200) { + try { + await deleteTag(tagId); + } catch (e) { return { status: "error", - message: "Failed to remove tag", - notificationOptions: { response }, + message: "Failed to remove tag: " + errorMessage(e), }; } - const snapshot = DB.getSnapshot(); - if (snapshot?.tags) { - snapshot.tags = snapshot.tags.filter((it) => it._id !== tagId); - } - - DB.deleteLocalTag(tagId); - + DB.removeTagFromResults(tagId); void Settings.update(); return { status: "success", message: `Tag removed` }; @@ -144,27 +117,13 @@ const actionModals: Record = { }, execFn: async (_thisPopup) => { const tagId = _thisPopup.parameters[1] as string; - const response = await Ape.users.deleteTagPersonalBest({ - params: { tagId }, - }); - if (response.status !== 200) { + try { + await clearTagPBs(tagId); + } catch (e) { return { status: "error", - message: "Failed to clear tag pb", - notificationOptions: { response }, - }; - } - - const matchingTag = getTagFromSnapshot(tagId); - - if (matchingTag !== undefined) { - matchingTag.personalBests = { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, + message: "Failed to clear tag PBs: " + errorMessage(e), }; } diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 9232daddee18..f49d7356b10d 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -1,5 +1,6 @@ import * as DB from "../db"; import * as ResultFilters from "../elements/account/result-filters"; +import { getTags, getTag } from "../collections/tags"; import * as ChartController from "../controllers/chart-controller"; import { Config } from "../config/store"; @@ -123,11 +124,10 @@ function buildResultRow(result: SnapshotResult): HTMLTableRowElement { if (result.tags !== undefined && result.tags.length > 0) { tagNames = ""; result.tags.forEach((tag) => { - DB.getSnapshot()?.tags?.forEach((snaptag) => { - if (tag === snaptag._id) { - tagNames += snaptag.display + ", "; - } - }); + const localTag = getTag(tag); + if (localTag !== undefined) { + tagNames += localTag.display + ", "; + } }); tagNames = tagNames.substring(0, tagNames.length - 2); } @@ -424,22 +424,20 @@ async function fillContent(): Promise { let tagHide = true; if (result.tags === undefined || result.tags.length === 0) { //no tags, show when no tag is enabled - if ((DB.getSnapshot()?.tags?.length ?? 0) > 0) { + if (getTags().length > 0) { if (ResultFilters.getFilter("tags", "none")) tagHide = false; } else { tagHide = false; } } else { //tags exist - const validTags = DB.getSnapshot()?.tags?.map((t) => t._id); - - if (validTags === undefined) return; + const validTags = getTags().map((t) => t._id); result.tags.forEach((tag) => { //check if i even need to check tags anymore if (!tagHide) return; //check if tag is valid - if (validTags?.includes(tag)) { + if (validTags.includes(tag)) { //tag valid, check if filter is on if (ResultFilters.getFilter("tags", tag)) tagHide = false; } else { @@ -983,11 +981,10 @@ export function updateTagsForResult(resultId: string, tagIds: string[]): void { if (tagIds.length > 0) { for (const tag of tagIds) { - DB.getSnapshot()?.tags?.forEach((snaptag) => { - if (tag === snaptag._id) { - tagNames.push(snaptag.display); - } - }); + const localTag = getTag(tag); + if (localTag !== undefined) { + tagNames.push(localTag.display); + } } } diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 390b55f1597a..a14abbf14b6e 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -8,7 +8,11 @@ import * as Misc from "../utils/misc"; import * as Strings from "../utils/strings"; import * as DB from "../db"; import * as Funbox from "../test/funbox/funbox"; -import * as TagController from "../controllers/tag-controller"; +import { + getTags, + toggleTagActive, + useTagsLiveQuery, +} from "../collections/tags"; import * as PresetController from "../controllers/preset-controller"; import * as ThemePicker from "../elements/settings/theme-picker"; import { @@ -51,6 +55,8 @@ import { authEvent } from "../events/auth"; import * as FpsLimitSection from "../elements/settings/fps-limit-section"; import { qs, qsa, qsr, onDOMReady } from "../utils/dom"; import { showPopup } from "../modals/simple-modals-base"; +import { createEffectOn } from "../hooks/effects"; +import { createMemo } from "solid-js"; let settingsInitialized = false; @@ -516,10 +522,14 @@ function setActiveFunboxButton(): void { } } +const tagsQuery = useTagsLiveQuery(); +const activeTags = createMemo(() => tagsQuery().filter((tag) => tag.active)); +createEffectOn(activeTags, refreshTagsSettingsSection); + function refreshTagsSettingsSection(): void { if (isAuthenticated() && DB.getSnapshot()) { const tagsEl = qs(".pageSettings .section.tags .tagsList")?.empty(); - DB.getSnapshot()?.tags?.forEach((tag) => { + getTags().forEach((tag) => { // let tagPbString = "No PB found"; // if (tag.pb !== undefined && tag.pb > 0) { // tagPbString = `PB: ${tag.pb}`; @@ -786,7 +796,7 @@ qs(".pageSettings .section.tags")?.onChild( (e) => { const target = e.childTarget as HTMLElement; const tagid = target.parentElement?.getAttribute("data-id") as string; - TagController.toggle(tagid); + toggleTagActive(tagid); target.classList.toggle("active"); }, ); diff --git a/frontend/src/ts/states/snapshot.ts b/frontend/src/ts/states/snapshot.ts index 9b7eb087219c..21ccc6845ef6 100644 --- a/frontend/src/ts/states/snapshot.ts +++ b/frontend/src/ts/states/snapshot.ts @@ -5,7 +5,7 @@ import { Mode } from "@monkeytype/schemas/shared"; export type MiniSnapshot = Omit< Snapshot, - "results" | "tags" | "presets" | "filterPresets" + "results" | "presets" | "filterPresets" >; const [snapshot, updateSnapshot] = createStore<{ value: MiniSnapshot | undefined; diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index d934c71c0264..0471480d7b61 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -1,6 +1,7 @@ import * as TestWords from "./test-words"; import { Config } from "../config/store"; import * as DB from "../db"; +import { getActiveTagsPB } from "../collections/tags"; import * as Misc from "../utils/misc"; import * as TestState from "./test-state"; import { configEvent } from "../events/config"; @@ -72,7 +73,7 @@ export async function init(): Promise { ) )?.wpm ?? 0; } else if (Config.paceCaret === "tagPb") { - wpm = await DB.getActiveTagsPB( + wpm = getActiveTagsPB( Config.mode, mode2, Config.punctuation, diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 89cdf90de22a..185d6d950883 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -45,7 +45,14 @@ import Ape from "../ape"; import { CompletedEvent } from "@monkeytype/schemas/results"; import { getActiveFunboxes, isFunboxActiveWithProperty } from "./funbox/list"; import { getFunbox } from "@monkeytype/funbox"; -import { SnapshotUserTag } from "../constants/default-snapshot"; +import { + getActiveTags, + getTags, + getTag, + getLocalTagPB, + saveLocalTagPB, + type TagItem, +} from "../collections/tags"; import { Language } from "@monkeytype/schemas/languages"; import { canQuickRestart as canQuickRestartFn } from "../utils/quick-restart"; import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; @@ -662,15 +669,8 @@ export function showConfetti(): void { } async function updateTags(dontSave: boolean): Promise { - const activeTags: SnapshotUserTag[] = []; - const userTagsCount = DB.getSnapshot()?.tags?.length ?? 0; - try { - DB.getSnapshot()?.tags?.forEach((tag) => { - if (tag.active === true) { - activeTags.push(tag); - } - }); - } catch (e) {} + const activeTags: TagItem[] = getActiveTags(); + const userTagsCount = getTags().length; if (userTagsCount === 0) { qs("#result .stats .tags")?.hide(); @@ -697,7 +697,7 @@ async function updateTags(dontSave: boolean): Promise { let annotationSide: LabelPosition = "start"; let labelAdjust = 15; for (const tag of activeTags) { - const tpb = await DB.getLocalTagPB( + const tpb = getLocalTagPB( tag._id, Config.mode, result.mode2, @@ -718,7 +718,7 @@ async function updateTags(dontSave: boolean): Promise { ) { if (tpb < result.wpm) { //new pb for that tag - await DB.saveLocalTagPB( + saveLocalTagPB( tag._id, Config.mode, result.mode2, @@ -1268,11 +1268,10 @@ export function updateTagsAfterEdit( if (tagIds.length > 0) { for (const tag of tagIds) { - DB.getSnapshot()?.tags?.forEach((snaptag) => { - if (tag === snaptag._id) { - tagNames.push(snaptag.display); - } - }); + const localTag = getTag(tag); + if (localTag !== undefined) { + tagNames.push(localTag.display); + } } } diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 3ce10c09a91a..f3cf09a5b114 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -22,6 +22,7 @@ import * as Caret from "./caret"; import * as TestTimer from "./test-timer"; import * as DB from "../db"; import * as Replay from "./replay"; +import { getActiveTags } from "../collections/tags"; import * as TodayTracker from "./today-tracker"; import * as ChallengeContoller from "../controllers/challenge-controller"; import { clearQuoteStats } from "../states/quote-rate"; @@ -810,12 +811,7 @@ function buildCompletedEvent( } //tags - const activeTagsIds: string[] = []; - for (const tag of DB.getSnapshot()?.tags ?? []) { - if (tag.active === true) { - activeTagsIds.push(tag._id); - } - } + const activeTagsIds: string[] = getActiveTags().map((tag) => tag._id); const duration = parseFloat(stats.time.toString()); const afkDuration = TestStats.calculateAfkSeconds(duration); diff --git a/frontend/static/contributors.json b/frontend/static/contributors.json index da49e06dd08d..dde685fa4dd4 100644 --- a/frontend/static/contributors.json +++ b/frontend/static/contributors.json @@ -1160,4 +1160,4 @@ "Jeroen Meijer (Jay)", "Jierie Ezequiel Jacla", "Joel Bradshaw" -] \ No newline at end of file +]