From 11cfed8074b5028eb8ccf1dcb1f7a0591203204a Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 18 May 2026 14:30:40 +0200 Subject: [PATCH 01/21] wip --- frontend/src/html/pages/test.html | 2 + frontend/src/ts/collections/results.ts | 26 +- frontend/src/ts/collections/tags.ts | 11 + frontend/src/ts/commandline/lists/bail-out.ts | 4 +- .../ts/components/modals/CustomTextModal.tsx | 18 +- .../components/modals/SaveCustomTextModal.tsx | 4 +- .../ts/components/modals/SavedTextsModal.tsx | 6 +- frontend/src/ts/components/mount.tsx | 2 + .../test/modes-notice/Last10Average.tsx | 45 ++ .../test/modes-notice/TestModesNotice.tsx | 402 ++++++++++++++++++ frontend/src/ts/elements/last-10-average.ts | 4 +- frontend/src/ts/elements/modes-notice.ts | 13 +- frontend/src/ts/input/handlers/keydown.ts | 4 +- .../src/ts/legacy-states/custom-text-name.ts | 18 - frontend/src/ts/states/core.ts | 12 + frontend/src/ts/states/test.ts | 7 +- frontend/src/ts/test/pace-caret.ts | 4 +- frontend/src/ts/test/practise-words.ts | 4 +- frontend/src/ts/test/result.ts | 5 +- frontend/src/ts/test/test-logic.ts | 31 +- frontend/src/ts/test/test-state.ts | 5 - frontend/src/ts/test/words-generator.ts | 9 +- frontend/src/ts/ui.ts | 9 +- 23 files changed, 559 insertions(+), 86 deletions(-) create mode 100644 frontend/src/ts/components/test/modes-notice/Last10Average.tsx create mode 100644 frontend/src/ts/components/test/modes-notice/TestModesNotice.tsx delete mode 100644 frontend/src/ts/legacy-states/custom-text-name.ts diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index 49f01e1ba8a9..dd649439c014 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -22,6 +22,8 @@
Time left to memorise all words: 0s
Time left to memorise all words: 0s
+ +
diff --git a/frontend/src/ts/collections/results.ts b/frontend/src/ts/collections/results.ts index e3841319f411..2b07d4c549d7 100644 --- a/frontend/src/ts/collections/results.ts +++ b/frontend/src/ts/collections/results.ts @@ -24,16 +24,16 @@ import { queryOptions } from "@tanstack/solid-query"; import { Accessor } from "solid-js"; import Ape from "../ape"; import { SnapshotResult } from "../constants/default-snapshot"; +import { createEffectOn } from "../hooks/effects"; import { queryClient } from "../queries"; import { baseKey } from "../queries/utils/keys"; +import { isAuthenticated } from "../states/core"; +import { getLastResult, setLastResult } from "../states/snapshot"; import { - __nonReactive as tagsNonReactive, reconcileLocalTagPB, saveLocalTagPB, + __nonReactive as tagsNonReactive, } from "./tags"; -import { isAuthenticated } from "../states/core"; -import { createEffectOn } from "../hooks/effects"; -import { getLastResult, setLastResult } from "../states/snapshot"; import { applyIdWorkaround } from "./utils/misc"; export type ResultsQueryState = { @@ -549,7 +549,23 @@ export type CurrentSettingsFilter = { lazyMode: boolean; }; -export async function getUserAverage10( +// oxlint-disable-next-line typescript/explicit-function-return-type +export function useUserAverage10LiveQuery(options: CurrentSettingsFilter) { + return useLiveQuery((q) => + q + .from({ + //we use sub-query to filter first and then aggregate + last10: buildSettingsResultsQuery(options, { + tagIds: tagsNonReactive.getActiveTags().map((it) => it._id), + }) + .orderBy(({ r }) => r.timestamp, "desc") + .limit(10), + }) + .select(({ last10 }) => ({ wpm: avg(last10.wpm), acc: avg(last10.acc) })), + ); +} + +export async function getUserAverage10Once( options: CurrentSettingsFilter, ): Promise<{ wpm: number; acc: number }> { //exit early if there is no user. Don't init the result collection diff --git a/frontend/src/ts/collections/tags.ts b/frontend/src/ts/collections/tags.ts index 5142acebbae3..89aea960d867 100644 --- a/frontend/src/ts/collections/tags.ts +++ b/frontend/src/ts/collections/tags.ts @@ -3,6 +3,7 @@ import { queryCollectionOptions } from "@tanstack/query-db-collection"; import { createCollection, createOptimisticAction, + eq, useLiveQuery, } from "@tanstack/solid-db"; import { z } from "zod"; @@ -60,6 +61,16 @@ export function useTagsLiveQuery() { }); } +// oxlint-disable-next-line typescript/explicit-function-return-type +export function useActiveTagsLiveQuery() { + return useLiveQuery((q) => { + return q + .from({ tag: tagsCollection }) + .where(({ tag }) => eq(tag.active, true)) + .orderBy(({ tag }) => tag.name, "asc"); + }); +} + type ActionType = { insertTag: { name: string; diff --git a/frontend/src/ts/commandline/lists/bail-out.ts b/frontend/src/ts/commandline/lists/bail-out.ts index 319701b3c6df..a650d5c1d0e0 100644 --- a/frontend/src/ts/commandline/lists/bail-out.ts +++ b/frontend/src/ts/commandline/lists/bail-out.ts @@ -1,13 +1,13 @@ import { Config } from "../../config/store"; +import { getCustomTextIndicator } from "../../states/core"; import * as CustomText from "../../test/custom-text"; import * as TestLogic from "../../test/test-logic"; import * as TestState from "../../test/test-state"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; import { Command, CommandsSubgroup } from "../types"; function canBailOut(): boolean { return ( - (Config.mode === "custom" && CustomTextState.isCustomTextLong() === true) || + (Config.mode === "custom" && getCustomTextIndicator()?.isLong === true) || (Config.mode === "custom" && (CustomText.getLimitMode() === "word" || CustomText.getLimitMode() === "section") && diff --git a/frontend/src/ts/components/modals/CustomTextModal.tsx b/frontend/src/ts/components/modals/CustomTextModal.tsx index c743f897f984..421aebdc3709 100644 --- a/frontend/src/ts/components/modals/CustomTextModal.tsx +++ b/frontend/src/ts/components/modals/CustomTextModal.tsx @@ -8,7 +8,10 @@ import type { FaSolidIcon } from "../../types/font-awesome"; import { setConfig } from "../../config/setters"; import { Config } from "../../config/store"; import { restartTestEvent } from "../../events/test"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; +import { + getCustomTextIndicator, + setCustomTextIndicator, +} from "../../states/core"; import { hideModalAndClearChain, showModal } from "../../states/modals"; import { showNoticeNotification, @@ -276,7 +279,7 @@ export function CustomTextModal(): JSXElement { }); }); - setLongTextWarning(CustomTextState.isCustomTextLong() ?? false); + setLongTextWarning(getCustomTextIndicator()?.isLong ?? false); setChallengeWarning(getLoadedChallenge() !== null); }; @@ -285,8 +288,8 @@ export function CustomTextModal(): JSXElement { if (data === null) return; setIncomingChainedData(null); - if (data.long !== true && CustomTextState.isCustomTextLong()) { - CustomTextState.setCustomTextName("", undefined); + if (data.long !== true && getCustomTextIndicator()?.isLong) { + setCustomTextIndicator(undefined); showNoticeNotification("Disabled long custom text progress tracking", { durationMs: 5000, }); @@ -358,11 +361,8 @@ export function CustomTextModal(): JSXElement { if (e.code === "Enter" && e.ctrlKey) { void form.handleSubmit(); } - if ( - CustomTextState.isCustomTextLong() && - CustomTextState.getCustomTextName() !== "" - ) { - CustomTextState.setCustomTextName("", undefined); + if (getCustomTextIndicator()?.isLong) { + setCustomTextIndicator(undefined); setLongTextWarning(false); showNoticeNotification("Disabled long custom text progress tracking", { durationMs: 5000, diff --git a/frontend/src/ts/components/modals/SaveCustomTextModal.tsx b/frontend/src/ts/components/modals/SaveCustomTextModal.tsx index 45574dd00434..bd8a549d986e 100644 --- a/frontend/src/ts/components/modals/SaveCustomTextModal.tsx +++ b/frontend/src/ts/components/modals/SaveCustomTextModal.tsx @@ -2,7 +2,7 @@ import { createForm } from "@tanstack/solid-form"; import { Accessor, JSXElement } from "solid-js"; import { z } from "zod"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; +import { setCustomTextIndicator } from "../../states/core"; import { hideModal } from "../../states/modals"; import { showNoticeNotification, @@ -42,7 +42,7 @@ export function SaveCustomTextModal(props: { const saved = CustomText.setCustomText(value.name, text, value.isLong); if (saved) { - CustomTextState.setCustomTextName(value.name, value.isLong); + setCustomTextIndicator(value); showSuccessNotification("Custom text saved"); hideModal("SaveCustomText"); } else { diff --git a/frontend/src/ts/components/modals/SavedTextsModal.tsx b/frontend/src/ts/components/modals/SavedTextsModal.tsx index 230bbb1c2a4c..f51416cd02bc 100644 --- a/frontend/src/ts/components/modals/SavedTextsModal.tsx +++ b/frontend/src/ts/components/modals/SavedTextsModal.tsx @@ -1,6 +1,6 @@ import { createSignal, For, Index, JSXElement, Setter, Show } from "solid-js"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; +import { setCustomTextIndicator } from "../../states/core"; import { hideModal } from "../../states/modals"; import { showSimpleModal } from "../../states/simple-modal"; import * as CustomText from "../../test/custom-text"; @@ -40,7 +40,7 @@ export function SavedTextsModal(props: { }; const handleNameClick = (name: string, long: boolean) => { - CustomTextState.setCustomTextName(name, long); + setCustomTextIndicator({ name, isLong: long }); const text = getSavedText(name, long); props.setChainedData({ text, long }); hideModal("SavedTexts"); @@ -53,7 +53,7 @@ export function SavedTextsModal(props: { buttonText: "delete", execFn: async () => { CustomText.deleteCustomText(name, long); - CustomTextState.setCustomTextName("", undefined); + setCustomTextIndicator(undefined); refresh(); return { status: "success", diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx index 5d547416884c..f36075da6280 100644 --- a/frontend/src/ts/components/mount.tsx +++ b/frontend/src/ts/components/mount.tsx @@ -20,6 +20,7 @@ import { ProfilePage } from "./pages/profile/ProfilePage"; import { ProfileSearchPage } from "./pages/profile/ProfileSearchPage"; import { TestConfig } from "./pages/test/TestConfig"; import { Popups } from "./popups/Popups"; +import { TestModesNotice } from "./test/modes-notice/TestModesNotice"; const components: Record JSXElement> = { footer: () =>