diff --git a/frontend/src/styles/popups.scss b/frontend/src/styles/popups.scss
index b5178db43a68..f746eb9f9a05 100644
--- a/frontend/src/styles/popups.scss
+++ b/frontend/src/styles/popups.scss
@@ -81,39 +81,6 @@ body.darkMode {
}
}
-#practiseWordsModal {
- .modal {
- max-width: 400px;
-
- .group {
- width: 100%;
- display: grid;
- .title {
- color: var(--sub-color);
- text-transform: lowercase;
- }
- .sub {
- color: var(--text-color);
- margin-top: 0.25rem;
- margin-bottom: 0.5rem;
- }
- .buttonGroup {
- display: flex;
- gap: 0.5rem;
- width: 100%;
- button {
- flex-grow: 1;
- }
- }
- }
-
- .text {
- font-size: 1rem;
- color: var(--text-color);
- }
- }
-}
-
#simpleModal {
.modal {
max-width: 500px;
diff --git a/frontend/src/ts/commandline/lists/result-screen.ts b/frontend/src/ts/commandline/lists/result-screen.ts
index 1debee3ac804..b060b2994894 100644
--- a/frontend/src/ts/commandline/lists/result-screen.ts
+++ b/frontend/src/ts/commandline/lists/result-screen.ts
@@ -1,6 +1,6 @@
import * as TestLogic from "../../test/test-logic";
import * as TestUI from "../../test/test-ui";
-import * as PractiseWordsModal from "../../modals/practise-words";
+import { showModal } from "../../states/modals";
import {
showErrorNotification,
showSuccessNotification,
@@ -50,11 +50,8 @@ const practiceSubgroup: CommandsSubgroup = {
id: "practiseWordsCustom",
display: "custom...",
opensModal: true,
- exec: (options): void => {
- PractiseWordsModal.show({
- animationMode: "modalOnly",
- modalChain: options.commandlineModal,
- });
+ exec: (): void => {
+ showModal("PractiseWords");
},
},
],
diff --git a/frontend/src/ts/components/modals/Modals.tsx b/frontend/src/ts/components/modals/Modals.tsx
index 44de2d887a34..074e6d0b9fd0 100644
--- a/frontend/src/ts/components/modals/Modals.tsx
+++ b/frontend/src/ts/components/modals/Modals.tsx
@@ -6,6 +6,7 @@ import { CustomTestDurationModal } from "./CustomTestDurationModal";
import { CustomTextModal } from "./CustomTextModal";
import { CustomWordAmountModal } from "./CustomWordAmountModal";
import { MobileTestConfigModal } from "./MobileTestConfigModal";
+import { PractiseWordsModal } from "./PractiseWordsModal";
import { AddPresetModal } from "./preset/AddPresetModal";
import { EditPresetModal } from "./preset/EditPresetModal";
import { QuoteRateModal } from "./QuoteRateModal";
@@ -33,6 +34,7 @@ export function Modals(): JSXElement {
+
diff --git a/frontend/src/ts/components/modals/PractiseWordsModal.tsx b/frontend/src/ts/components/modals/PractiseWordsModal.tsx
new file mode 100644
index 000000000000..efcef620d02c
--- /dev/null
+++ b/frontend/src/ts/components/modals/PractiseWordsModal.tsx
@@ -0,0 +1,114 @@
+import { AnyFieldApi, createForm } from "@tanstack/solid-form";
+import { For, JSXElement } from "solid-js";
+
+import { hideModalAndClearChain } from "../../states/modals";
+import * as PractiseWords from "../../test/practise-words";
+import * as TestLogic from "../../test/test-logic";
+import { cn } from "../../utils/cn";
+import { AnimatedModal } from "../common/AnimatedModal";
+import { Button } from "../common/Button";
+import { Fa } from "../common/Fa";
+
+export function PractiseWordsModal(): JSXElement {
+ const form = createForm(() => ({
+ defaultValues: {
+ missed: "words" as "off" | "words" | "biwords",
+ slow: false,
+ },
+ onSubmit: ({ value }) => {
+ PractiseWords.init(value.missed, value.slow);
+ hideModalAndClearChain("PractiseWords");
+ TestLogic.restart({ practiseMissed: true });
+ },
+ }));
+
+ const canStart = form.useStore(
+ (state) => state.values.missed !== "off" || state.values.slow,
+ );
+
+ return (
+
+
+
+ );
+}
+
+type ButtonGroupOption
= { value: T; label: string };
+
+function ButtonGroup(props: {
+ field: () => AnyFieldApi;
+ options: readonly ButtonGroupOption[];
+ class?: string;
+}): JSXElement {
+ return (
+
+
+ {(opt) => (
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/ts/event-handlers/test.ts b/frontend/src/ts/event-handlers/test.ts
index 78746fc0a4bc..3afb22b7d174 100644
--- a/frontend/src/ts/event-handlers/test.ts
+++ b/frontend/src/ts/event-handlers/test.ts
@@ -9,7 +9,7 @@ import {
} from "../states/notifications";
import { showQuoteRateModal } from "../states/quote-rate";
import { showQuoteReportModal } from "../states/quote-report";
-import * as PractiseWordsModal from "../modals/practise-words";
+import { showModal } from "../states/modals";
import { navigate } from "../controllers/route-controller";
import { getMode2 } from "../utils/misc";
import { ConfigKey } from "@monkeytype/schemas/configs";
@@ -67,7 +67,7 @@ testPage?.onChild("click", "#practiseWordsButton", () => {
showNoticeNotification("Practice words is unsupported in zen mode");
return;
}
- PractiseWordsModal.show();
+ showModal("PractiseWords");
});
qs(".pageTest #dailyLeaderboardRank")?.on("click", async () => {
diff --git a/frontend/src/ts/modals/practise-words.ts b/frontend/src/ts/modals/practise-words.ts
deleted file mode 100644
index bf05f050fa96..000000000000
--- a/frontend/src/ts/modals/practise-words.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
-import * as PractiseWords from "../test/practise-words";
-import * as TestLogic from "../test/test-logic";
-import { ElementWithUtils } from "../utils/dom";
-
-type State = {
- missed: "off" | "words" | "biwords";
- slow: boolean;
-};
-
-const state: State = {
- missed: "words",
- slow: false,
-};
-
-function updateUI(): void {
- const modalEl = modal.getModal();
- modalEl.qsa(`.group[data-id="missed"] button`).removeClass("active");
- modalEl
- .qs(`.group[data-id="missed"] button[value="${state.missed}"]`)
- ?.addClass("active");
-
- modalEl.qsa(`.group[data-id="slow"] button`).removeClass("active");
- modalEl
- .qs(`.group[data-id="slow"] button[value="${state.slow}"]`)
- ?.addClass("active");
-
- if (state.missed === "off" && !state.slow) {
- modalEl.qs(`.start`)?.disable();
- } else {
- modalEl.qs(`.start`)?.enable();
- }
-}
-
-async function setup(modalEl: ElementWithUtils): Promise {
- modalEl.qsa(".group[data-id='missed'] button").on("click", (e) => {
- state.missed = (e.currentTarget as HTMLButtonElement).value as
- | "off"
- | "words"
- | "biwords";
- updateUI();
- });
-
- modalEl.qsa(".group[data-id='slow'] button").on("click", (e) => {
- state.slow = (e.currentTarget as HTMLButtonElement).value === "true";
- updateUI();
- });
-
- modalEl.qs(".start")?.on("click", () => {
- apply();
- });
-
- modalEl.on("submit", (e) => {
- e.preventDefault();
- apply();
- });
-
- updateUI();
-}
-
-export function show(showOptions?: ShowOptions): void {
- void modal.show(showOptions);
-}
-
-function hide(clearChain = false): void {
- void modal.hide({
- clearModalChain: clearChain,
- });
-}
-
-function apply(): void {
- PractiseWords.init(state.missed, state.slow);
- hide(true);
- TestLogic.restart({
- practiseMissed: true,
- });
-}
-
-const modal = new AnimatedModal({
- dialogId: "practiseWordsModal",
- setup,
-});
diff --git a/frontend/src/ts/states/modals.ts b/frontend/src/ts/states/modals.ts
index 2fc42734ceeb..fc7b2efdcf33 100644
--- a/frontend/src/ts/states/modals.ts
+++ b/frontend/src/ts/states/modals.ts
@@ -25,6 +25,7 @@ export type ModalId =
| "CustomWordAmount"
| "MobileTestConfig"
| "MiniResultChartModal"
+ | "PractiseWords"
| "Cookies"
| "AddPresetModal"
| "EditPresetModal"