diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html index 216026bc2132..b3b2bd386e6e 100644 --- a/frontend/src/html/popups.html +++ b/frontend/src/html/popups.html @@ -58,35 +58,6 @@ - - - - - - words - - wpm - - accuracy - - - raw - - consistency - - difficulty - language - punctuation - numbers - lazy mode - date - - - - - - - Practice words diff --git a/frontend/src/styles/popups.scss b/frontend/src/styles/popups.scss index b5178db43a68..d14736c2d1c5 100644 --- a/frontend/src/styles/popups.scss +++ b/frontend/src/styles/popups.scss @@ -259,74 +259,6 @@ body.darkMode { } } -#pbTablesModal { - .modal { - max-width: 100%; - overflow-y: scroll; - table { - border-spacing: 0; - border-collapse: collapse; - color: var(--text-color); - - tbody { - clip-path: inset(0); - } - - td { - padding: 0.5rem 0.5rem; - } - - .modesticky { - position: sticky; - top: calc(1rem - 2px); - z-index: 2; - } - - thead { - color: var(--sub-color); - font-size: 0.75rem; - position: sticky; - top: -2rem; - background-color: var(--bg-color) !important; - z-index: 3; - } - - tbody tr.odd { - background: var(--sub-alt-color); - } - - tbody tr.even { - background: var(--bg-color); - } - - td.infoIcons span { - margin: 0 0.1rem; - } - .miniResultChartButton { - opacity: 0.25; - transition: 0.25s; - cursor: pointer; - &:hover { - opacity: 1; - } - } - .sub { - opacity: 0.5; - } - td { - text-align: right; - } - td:nth-child(6), - td:nth-child(7) { - text-align: center; - } - tbody td:nth-child(1) { - font-size: 1.5rem; - } - } - } -} - #importExportSettingsModal { .modal { max-width: 900px; diff --git a/frontend/src/ts/components/modals/Modals.tsx b/frontend/src/ts/components/modals/Modals.tsx index 44de2d887a34..5fa0b3aec832 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 { PbTablesModal } from "./PbTablesModal"; import { AddPresetModal } from "./preset/AddPresetModal"; import { EditPresetModal } from "./preset/EditPresetModal"; import { QuoteRateModal } from "./QuoteRateModal"; @@ -31,6 +32,7 @@ export function Modals(): JSXElement { + diff --git a/frontend/src/ts/components/modals/PbTablesModal.mock.ts b/frontend/src/ts/components/modals/PbTablesModal.mock.ts new file mode 100644 index 000000000000..6a7c80b7981a --- /dev/null +++ b/frontend/src/ts/components/modals/PbTablesModal.mock.ts @@ -0,0 +1,578 @@ +import { type PersonalBests } from "@monkeytype/schemas/shared"; + +export const USE_MOCK_PB_DATA = true; + +export const MOCK_PERSONAL_BESTS: Pick = { + time: { + "15": [ + { + wpm: 147, + raw: 152, + acc: 99.4, + consistency: 91.8, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 22, 10, 10), + }, + { + wpm: 144, + raw: 149, + acc: 99.1, + consistency: 89.5, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 21, 15, 42), + }, + { + wpm: 141, + raw: 146, + acc: 98.7, + consistency: 87.3, + difficulty: "expert", + language: "german", + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 20, 9, 4), + }, + { + wpm: 138, + raw: 143, + acc: 98.1, + consistency: 84.9, + difficulty: "normal", + language: "french", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 18, 18, 27), + }, + { + wpm: 134, + raw: 139, + acc: 97.9, + consistency: 82.6, + difficulty: "normal", + language: "spanish", + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 17, 8, 56), + }, + { + wpm: 131, + raw: 137, + acc: 97.2, + consistency: 80.1, + difficulty: "normal", + language: "portuguese", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 16, 21, 33), + }, + { + wpm: 128, + raw: 133, + acc: 96.8, + consistency: 78.4, + difficulty: "normal", + language: "italian", + punctuation: false, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 15, 12, 8), + }, + { + wpm: 124, + raw: 129, + acc: 96.2, + consistency: 75.2, + difficulty: "normal", + language: "english", + punctuation: true, + numbers: false, + lazyMode: true, + timestamp: Date.UTC(2026, 4, 14, 16, 44), + }, + { + wpm: 120, + raw: 126, + acc: 95.6, + consistency: 73.9, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: true, + lazyMode: true, + timestamp: 0, + }, + { + wpm: 116, + raw: 121, + acc: 94.8, + consistency: 70.7, + difficulty: "normal", + language: "english", + punctuation: false, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 11, 11, 18), + }, + ], + "30": [ + { + wpm: 142, + raw: 147, + acc: 99.2, + consistency: 90.9, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 22, 8, 15), + }, + { + wpm: 139, + raw: 144, + acc: 98.8, + consistency: 88.2, + difficulty: "expert", + language: "german", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 21, 13, 3), + }, + { + wpm: 136, + raw: 141, + acc: 98.4, + consistency: 86.5, + difficulty: "expert", + language: "english", + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 20, 17, 25), + }, + { + wpm: 132, + raw: 138, + acc: 97.9, + consistency: 83.4, + difficulty: "normal", + language: "french", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 18, 10, 59), + }, + { + wpm: 129, + raw: 135, + acc: 97.1, + consistency: 81.2, + difficulty: "normal", + language: "spanish", + punctuation: false, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 17, 19, 10), + }, + { + wpm: 125, + raw: 130, + acc: 96.7, + consistency: 78.7, + difficulty: "normal", + language: "russian", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 16, 22, 6), + }, + { + wpm: 121, + raw: 126, + acc: 95.9, + consistency: 75.8, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: true, + lazyMode: true, + timestamp: 0, + }, + { + wpm: 117, + raw: 123, + acc: 95.1, + consistency: 72.1, + difficulty: "normal", + language: "english", + punctuation: false, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 13, 7, 40), + }, + ], + "60": [ + { + wpm: 133, + raw: 138, + acc: 98.6, + consistency: 87.1, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 20, 6, 50), + }, + { + wpm: 129, + raw: 134, + acc: 98.2, + consistency: 84.2, + difficulty: "expert", + language: "german", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 18, 14, 11), + }, + { + wpm: 125, + raw: 130, + acc: 97.5, + consistency: 80.6, + difficulty: "normal", + language: "english", + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 16, 9, 29), + }, + { + wpm: 121, + raw: 126, + acc: 96.9, + consistency: 77.8, + difficulty: "normal", + language: "french", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 15, 20, 13), + }, + { + wpm: 117, + raw: 122, + acc: 96.3, + consistency: 74.5, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: true, + lazyMode: true, + timestamp: 0, + }, + { + wpm: 113, + raw: 119, + acc: 95.5, + consistency: 70.8, + difficulty: "normal", + language: "spanish", + punctuation: false, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 12, 17, 54), + }, + ], + "120": [ + { + wpm: 126, + raw: 130, + acc: 98.1, + consistency: 82.9, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 19, 12, 21), + }, + { + wpm: 122, + raw: 127, + acc: 97.2, + consistency: 79.4, + difficulty: "normal", + language: "german", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 17, 9, 5), + }, + { + wpm: 118, + raw: 123, + acc: 96.4, + consistency: 75.7, + difficulty: "normal", + language: "english", + punctuation: false, + numbers: false, + lazyMode: true, + timestamp: Date.UTC(2026, 4, 14, 18, 36), + }, + { + wpm: 114, + raw: 119, + acc: 95.8, + consistency: 72.6, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: 0, + }, + ], + }, + words: { + "10": [ + { + wpm: 151, + raw: 156, + acc: 99.5, + consistency: 92.4, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 22, 11, 4), + }, + { + wpm: 147, + raw: 152, + acc: 99.1, + consistency: 89.8, + difficulty: "expert", + language: "german", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 21, 16, 55), + }, + { + wpm: 143, + raw: 148, + acc: 98.7, + consistency: 87.2, + difficulty: "expert", + language: "french", + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 20, 8, 22), + }, + { + wpm: 139, + raw: 144, + acc: 98.3, + consistency: 84.6, + difficulty: "normal", + language: "english", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 19, 20, 9), + }, + { + wpm: 134, + raw: 139, + acc: 97.8, + consistency: 81.3, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: true, + lazyMode: true, + timestamp: 0, + }, + { + wpm: 129, + raw: 135, + acc: 96.9, + consistency: 77.5, + difficulty: "normal", + language: "spanish", + punctuation: false, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 15, 13, 40), + }, + ], + "25": [ + { + wpm: 145, + raw: 150, + acc: 99.3, + consistency: 90.6, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 22, 7, 48), + }, + { + wpm: 141, + raw: 146, + acc: 98.9, + consistency: 88.3, + difficulty: "expert", + language: "german", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 20, 14, 6), + }, + { + wpm: 137, + raw: 142, + acc: 98.2, + consistency: 84.8, + difficulty: "normal", + language: "french", + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 18, 10, 50), + }, + { + wpm: 132, + raw: 137, + acc: 97.4, + consistency: 80.7, + difficulty: "normal", + language: "english", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 16, 18, 18), + }, + { + wpm: 128, + raw: 133, + acc: 96.6, + consistency: 76.4, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: true, + lazyMode: true, + timestamp: 0, + }, + ], + "50": [ + { + wpm: 138, + raw: 143, + acc: 98.8, + consistency: 86.1, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 19, 9, 12), + }, + { + wpm: 133, + raw: 138, + acc: 97.9, + consistency: 82.2, + difficulty: "normal", + language: "german", + punctuation: true, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 17, 21, 41), + }, + { + wpm: 129, + raw: 134, + acc: 97.1, + consistency: 78.9, + difficulty: "normal", + language: "english", + punctuation: false, + numbers: false, + lazyMode: true, + timestamp: Date.UTC(2026, 4, 15, 11, 27), + }, + { + wpm: 124, + raw: 130, + acc: 96.1, + consistency: 74.6, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: 0, + }, + ], + "100": [ + { + wpm: 131, + raw: 136, + acc: 98.1, + consistency: 81.8, + difficulty: "expert", + language: "english", + punctuation: true, + numbers: false, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 18, 7, 17), + }, + { + wpm: 126, + raw: 131, + acc: 97.2, + consistency: 77.6, + difficulty: "normal", + language: "french", + punctuation: false, + numbers: true, + lazyMode: false, + timestamp: Date.UTC(2026, 4, 14, 19, 58), + }, + { + wpm: 121, + raw: 126, + acc: 96.4, + consistency: 73.2, + difficulty: "normal", + language: undefined, + punctuation: false, + numbers: false, + lazyMode: true, + timestamp: 0, + }, + ], + }, +}; diff --git a/frontend/src/ts/components/modals/PbTablesModal.tsx b/frontend/src/ts/components/modals/PbTablesModal.tsx new file mode 100644 index 000000000000..2e07cdbdb32e --- /dev/null +++ b/frontend/src/ts/components/modals/PbTablesModal.tsx @@ -0,0 +1,166 @@ +import { Mode2, Mode, PersonalBest } from "@monkeytype/schemas/shared"; +import { format as formatDate } from "date-fns/format"; +import { createMemo, createSignal, For, JSXElement, Show } from "solid-js"; + +import { getConfig } from "../../config/store"; +import * as DB from "../../db"; +import { pbTablesMode } from "../../states/pb-tables-modal"; +import { Formatting } from "../../utils/format"; +import { getLanguageDisplayString } from "../../utils/strings"; +import { AnimatedModal } from "../common/AnimatedModal"; +import { Fa } from "../common/Fa"; +import { + Table, + TableBody, + TableHeader, + TableRow, + TableHead, + TableCell, +} from "../ui/table/Table"; +import { MOCK_PERSONAL_BESTS, USE_MOCK_PB_DATA } from "./PbTablesModal.mock"; + +type PBWithMode2 = PersonalBest & { + mode2: Mode2; +}; + +type PBRow = { + pb: PBWithMode2; + showMode2: boolean; +}; + +function buildRows(mode: Mode): PBRow[] { + const allmode2 = ( + USE_MOCK_PB_DATA + ? MOCK_PERSONAL_BESTS[mode as "time" | "words"] + : DB.getSnapshot()?.personalBests?.[mode] + ) as Record | undefined; + if (allmode2 === undefined) return []; + + const list: PBWithMode2[] = []; + Object.keys(allmode2).forEach((key) => { + let pbs = allmode2[key] ?? []; + pbs = [...pbs].sort((a, b) => b.wpm - a.wpm); + pbs.forEach((pb) => { + pb.mode2 = key; + list.push(pb); + }); + }); + + const rows: PBRow[] = []; + let currentMode2: string | undefined; + + list.forEach((pb) => { + const showMode2 = currentMode2 !== pb.mode2; + currentMode2 = pb.mode2; + rows.push({ pb, showMode2 }); + }); + + return rows; +} + +export function PbTablesModal(): JSXElement { + const [rows, setRows] = createSignal([]); + const format = createMemo(() => new Formatting(getConfig)); + const mode = createMemo(() => pbTablesMode()); + + return ( + { + setRows(buildRows(mode())); + }} + > + + + + {mode()} + + {format().typingSpeedUnit} + + accuracy + + + raw + + consistency + + difficulty + language + punctuation + numbers + lazy mode + date + + + + + {(row) => { + return ( + + }> + + {row.pb.mode2} + + + + {format().typingSpeed(row.pb.wpm)} + + + {format().accuracy(row.pb.acc)} + + + + {format().typingSpeed(row.pb.raw)} + + + {format().percentage(row.pb.consistency)} + + + {row.pb.difficulty} + + {row.pb.language + ? getLanguageDisplayString(row.pb.language) + : "-"} + + + + + + + + + + + + + + + + + + + - + - + > + } + > + {formatDate(row.pb.timestamp, "dd MMM yyyy")} + + + {formatDate(row.pb.timestamp, "HH:mm")} + + + + + ); + }} + + + + + ); +} diff --git a/frontend/src/ts/components/pages/profile/UserProfile.tsx b/frontend/src/ts/components/pages/profile/UserProfile.tsx index 3bc907fb4b1e..82abcdc1ab9c 100644 --- a/frontend/src/ts/components/pages/profile/UserProfile.tsx +++ b/frontend/src/ts/components/pages/profile/UserProfile.tsx @@ -7,7 +7,7 @@ import { formatDate } from "date-fns/format"; import { createMemo, For, JSXElement, Show } from "solid-js"; import { getConfig } from "../../../config/store"; -import * as PbTablesModal from "../../../modals/pb-tables"; +import { showPbTablesModal } from "../../../states/pb-tables-modal"; import { Formatting } from "../../../utils/format"; import { formatTopPercentage } from "../../../utils/misc"; import { Button } from "../../common/Button"; @@ -176,7 +176,7 @@ function PbTable(props: { balloon={{ text: "Show all personal bests", position: "left" }} class="h-full rounded-none rounded-r text-sub hover:text-bg" fa={{ icon: "fa-ellipsis-v" }} - onClick={() => PbTablesModal.show(props.mode)} + onClick={() => showPbTablesModal(props.mode)} /> diff --git a/frontend/src/ts/modals/pb-tables.ts b/frontend/src/ts/modals/pb-tables.ts deleted file mode 100644 index 81d0da3b7426..000000000000 --- a/frontend/src/ts/modals/pb-tables.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as DB from "../db"; -import { format } from "date-fns/format"; -import { getLanguageDisplayString } from "../utils/strings"; -import { Config } from "../config/store"; -import Format from "../singletons/format"; -import AnimatedModal from "../utils/animated-modal"; -import { Mode, Mode2, PersonalBest } from "@monkeytype/schemas/shared"; - -type PBWithMode2 = { - mode2: Mode2; -} & PersonalBest; - -function update(mode: Mode): void { - const modalEl = modal.getModal(); - - const tableEl = modalEl.qs("table"); - tableEl?.qsa("tbody").remove(); - modalEl.qs("thead td:first-child")?.setText(mode); - modalEl.qs("thead span.unit")?.setText(Config.typingSpeedUnit); - - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - - const allmode2 = snapshot.personalBests?.[mode] as - | Record - | undefined; - - if (allmode2 === undefined) return; - - const list: PBWithMode2[] = []; - Object.keys(allmode2).forEach(function (key) { - let pbs = allmode2[key] ?? []; - pbs = pbs.sort(function (a, b) { - return b.wpm - a.wpm; - }); - pbs.forEach(function (pb) { - pb.mode2 = key; - list.push(pb); - }); - }); - - let mode2memory: Mode2 | null = null; - let currentTbody: HTMLTableSectionElement | null = null; - let rowIndex: number = 1; - - list.forEach((pb) => { - const isNewGroup = mode2memory !== pb.mode2 || currentTbody === null; - if (isNewGroup) { - currentTbody = document.createElement("tbody"); - tableEl?.append(currentTbody); - } - let dateText = `--`; - const date = new Date(pb.timestamp); - if (pb.timestamp) { - dateText = `${format(date, "dd MMM yyyy")}${format( - date, - "HH:mm", - )}`; - } - currentTbody?.insertAdjacentHTML( - "beforeend", - ` - - ${ - isNewGroup - ? ` - - ${pb.mode2} - - ` - : "" - } - - ${Format.typingSpeed(pb.wpm)} - - ${Format.accuracy(pb.acc)} - - - ${Format.typingSpeed(pb.raw)} - - ${Format.percentage(pb.consistency)} - - ${pb.difficulty} - ${pb.language ? getLanguageDisplayString(pb.language) : "-"} - ${pb.punctuation ? '' : ""} - ${pb.numbers ? '' : ""} - ${pb.lazyMode ? '' : ""} - ${dateText} - - `, - ); - mode2memory = pb.mode2; - rowIndex++; - }); -} - -export function show(mode: Mode): void { - void modal.show({ - beforeAnimation: async () => { - update(mode); - }, - }); -} - -const modal = new AnimatedModal({ - dialogId: "pbTablesModal", -}); diff --git a/frontend/src/ts/states/modals.ts b/frontend/src/ts/states/modals.ts index 2fc42734ceeb..c6cffb19e40c 100644 --- a/frontend/src/ts/states/modals.ts +++ b/frontend/src/ts/states/modals.ts @@ -24,6 +24,7 @@ export type ModalId = | "ShareTestSettings" | "CustomWordAmount" | "MobileTestConfig" + | "PbTables" | "MiniResultChartModal" | "Cookies" | "AddPresetModal" diff --git a/frontend/src/ts/states/pb-tables-modal.ts b/frontend/src/ts/states/pb-tables-modal.ts new file mode 100644 index 000000000000..310149d1ecfa --- /dev/null +++ b/frontend/src/ts/states/pb-tables-modal.ts @@ -0,0 +1,14 @@ +import { createSignal } from "solid-js"; + +import { Mode } from "@monkeytype/schemas/shared"; + +import { showModal } from "./modals"; + +const [pbTablesMode, setPbTablesMode] = createSignal("time"); + +export { pbTablesMode }; + +export function showPbTablesModal(mode: Mode): void { + setPbTablesMode(mode); + showModal("PbTables"); +}