diff --git a/.github/workflows/monkey-ci.yml b/.github/workflows/monkey-ci.yml index b7183c5ebfe7..d86fe4352310 100644 --- a/.github/workflows/monkey-ci.yml +++ b/.github/workflows/monkey-ci.yml @@ -49,7 +49,7 @@ jobs: - 'backend/**/*.{ts,js,json,lua,css,html}' - 'backend/package.json' fe-src: - - 'frontend/**/*.{ts,scss}' + - 'frontend/**/*.{ts,scss,html}' - 'frontend/package.json' pkg-src: - 'packages/**/*' diff --git a/frontend/scripts/fontawesome.ts b/frontend/scripts/fontawesome.ts index 809a71a89411..3cd3763201c9 100644 --- a/frontend/scripts/fontawesome.ts +++ b/frontend/scripts/fontawesome.ts @@ -99,7 +99,9 @@ export function getFontawesomeConfig(debug = false): FontawesomeConfig { (it) => !(solid.includes(it) || regular.includes(it) || brands.includes(it)) ); if (leftOvers.length !== 0) { - throw new Error("unknown icons: " + leftOvers.toString()); + throw new Error( + "Fontawesome failed with unknown icons: " + leftOvers.toString() + ); } if (debug) { diff --git a/frontend/src/html/pages/account.html b/frontend/src/html/pages/account.html index c08e4bb308c0..b1d7a856c0f7 100644 --- a/frontend/src/html/pages/account.html +++ b/frontend/src/html/pages/account.html @@ -685,10 +685,19 @@ - wpm - raw - accuracy - + + wpm + + + raw + + + accuracy + + consistency punctuation --> info tags - + date - diff --git a/frontend/src/styles/account.scss b/frontend/src/styles/account.scss index 189fb9091de0..693b08be1221 100644 --- a/frontend/src/styles/account.scss +++ b/frontend/src/styles/account.scss @@ -440,18 +440,6 @@ } } } - -.headerSorted { - font-weight: bold; -} - -.sortable:hover { - cursor: pointer; - -webkit-user-select: none; - user-select: none; - background-color: var(--sub-alt-color); -} - .testActivity { // width: max-content; // justify-self: center; diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 8584771b83f8..82a880c86e4b 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -34,6 +34,7 @@ import defaultResultFilters from "../constants/default-result-filters"; import { SnapshotResult } from "../constants/default-snapshot"; import Ape from "../ape"; import { AccountChart } from "@monkeytype/schemas/configs"; +import { SortedTableWithLimit } from "../utils/sorted-table"; let filterDebug = false; //toggle filterdebug @@ -47,6 +48,7 @@ export function toggleFilterDebug(): void { let filteredResults: SnapshotResult[] = []; let visibleTableLines = 0; let testActivityEl: HTMLElement | null; +let historyTable: SortedTableWithLimit>; function loadMoreLines(lineIndex?: number): void { if (filteredResults === undefined || filteredResults.length === 0) return; @@ -56,104 +58,115 @@ function loadMoreLines(lineIndex?: number): void { } else { newVisibleLines = visibleTableLines + 10; } - for (let i = visibleTableLines; i < newVisibleLines; i++) { - const result = filteredResults[i]; - if (!result) continue; - let diff = result.difficulty; - if (diff === undefined) { - diff = "normal"; - } - let icons = ``; - - if (diff === "normal") { - icons += ``; - } else if (diff === "expert") { - icons += ``; - } else if (diff === "master") { - icons += ``; - } + visibleTableLines = newVisibleLines; + if (visibleTableLines >= filteredResults.length) { + $(".pageAccount .loadMoreButton").addClass("hidden"); + } else { + $(".pageAccount .loadMoreButton").removeClass("hidden"); + } - if (result.punctuation) { - icons += ``; - } + historyTable.setLimit(newVisibleLines); + historyTable.updateBody(); +} - if (result.numbers) { - icons += ``; - } +function buildResultRow(result: SnapshotResult): HTMLTableRowElement { + let diff = result.difficulty; + if (diff === undefined) { + diff = "normal"; + } - if (result.blindMode) { - icons += ``; - } + let icons = ``; + + if (diff === "normal") { + icons += ``; + } else if (diff === "expert") { + icons += ``; + } else if (diff === "master") { + icons += ``; + } - if (result.lazyMode) { - icons += ``; - } + if (result.punctuation) { + icons += ``; + } - if (result.funbox !== undefined && result.funbox.length > 0) { - icons += ``; - } + if (result.numbers) { + icons += ``; + } - if (result.chartData === "toolong" || result.testDuration > 122) { - icons += ``; - } else { - icons += ``; - } + if (result.blindMode) { + icons += ``; + } + + if (result.lazyMode) { + icons += ``; + } - let tagNames = "no tags"; + if (result.funbox !== undefined && result.funbox.length > 0) { + icons += ``; + } - 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 + ", "; - } - }); + if (result.chartData === "toolong" || result.testDuration > 122) { + icons += ``; + } else { + icons += ``; + } + + let tagNames = "no tags"; + + 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 + ", "; + } }); - tagNames = tagNames.substring(0, tagNames.length - 2); - } + }); + tagNames = tagNames.substring(0, tagNames.length - 2); + } - let restags; - if (result.tags === undefined) { - restags = "[]"; - } else { - restags = JSON.stringify(result.tags); - } + let restags; + if (result.tags === undefined) { + restags = "[]"; + } else { + restags = JSON.stringify(result.tags); + } - const isActive = result.tags !== undefined && result.tags.length > 0; - const icon = - result.tags !== undefined && result.tags.length > 1 - ? "fa-tags" - : "fa-tag"; - - const resultTagsButton = ``; - - let pb = ""; - if (result.isPb) { - pb = ''; - } else { - pb = ""; - } + const isActive = result.tags !== undefined && result.tags.length > 0; + const icon = + result.tags !== undefined && result.tags.length > 1 ? "fa-tags" : "fa-tag"; + + const resultTagsButton = ``; - const charStats = result.charStats.join("/"); + let pb = ""; + if (result.isPb) { + pb = ''; + } else { + pb = ""; + } + + const charStats = result.charStats.join("/"); + + const mode2 = result.mode === "custom" ? "" : result.mode2; - const mode2 = result.mode === "custom" ? "" : result.mode2; + const date = new Date(result.timestamp); - const date = new Date(result.timestamp); - $(".pageAccount .history table tbody").append(` - + const element = document.createElement("tr"); + element.classList.add("resultRow"); + element.dataset["id"] = result._id; + element.innerHTML = ` ${pb} ${Format.typingSpeed(result.wpm, { showDecimalPlaces: true })} ${Format.typingSpeed(result.rawWpm, { showDecimalPlaces: true })} @@ -168,14 +181,9 @@ function loadMoreLines(lineIndex?: number): void { ${format(date, "dd MMM yyyy")}
${format(date, "HH:mm")} - `); - } - visibleTableLines = newVisibleLines; - if (visibleTableLines >= filteredResults.length) { - $(".pageAccount .loadMoreButton").addClass("hidden"); - } else { - $(".pageAccount .loadMoreButton").removeClass("hidden"); - } + `; + + return element; } async function updateChartColors(): Promise { @@ -188,7 +196,8 @@ async function updateChartColors(): Promise { } function reset(): void { - $(".pageAccount .history table tbody").empty(); + historyTable.setData([]); + historyTable.updateBody(); ChartController.accountHistogram.getDataset("count").data = []; ChartController.accountActivity.getDataset("count").data = []; @@ -667,6 +676,8 @@ async function fillContent(): Promise { totalWpm += result.wpm; }); + historyTable.setData(filteredResults); + $(".pageAccount .group.history table thead tr td:nth-child(2)").text( Config.typingSpeedUnit ); @@ -1027,81 +1038,6 @@ export function updateTagsForResult(resultId: string, tagIds: string[]): void { } } -function sortAndRefreshHistory( - keyString: string, - headerClass: string, - forceDescending: null | boolean = null -): void { - // Removes styling from previous sorting requests: - $("td").removeClass("headerSorted"); - $("td").children("i").remove(); - $(headerClass).addClass("headerSorted"); - - if (filteredResults.length < 2) return; - - const key = keyString as keyof (typeof filteredResults)[0]; - - // This allows to reverse the sorting order when clicking multiple times on the table header - let descending = true; - if (forceDescending !== null) { - if (forceDescending) { - $(headerClass).append( - '' - ); - } else { - descending = false; - $(headerClass).append( - '' - ); - } - } else if ( - parseInt(filteredResults?.[0]?.[key] as string) <= - parseInt(filteredResults?.[filteredResults.length - 1]?.[key] as string) - ) { - descending = true; - $(headerClass).append( - '' - ); - } else { - descending = false; - $(headerClass).append(''); - } - - const temp: SnapshotResult[] = []; - const parsedIndexes: number[] = []; - - while (temp.length < filteredResults.length) { - let lowest = Number.MAX_VALUE; - let highest = -1; - let idx = -1; - - // for (let i = 0; i < filteredResults.length; i++) { - for (const [i, result] of filteredResults.entries()) { - //find the lowest wpm with index not already parsed - if (!descending) { - if ((result[key] as number) <= lowest && !parsedIndexes.includes(i)) { - lowest = result[key] as number; - idx = i; - } - } else { - if ((result[key] as number) >= highest && !parsedIndexes.includes(i)) { - highest = result[key] as number; - idx = i; - } - } - } - - // @ts-expect-error temp - temp.push(filteredResults[idx]); - parsedIndexes.push(idx); - } - filteredResults = temp; - - $(".pageAccount .history table tbody").empty(); - visibleTableLines = 0; - loadMoreLines(); -} - $(".pageAccount button.toggleResultsOnChart").on("click", () => { const newValue = [...Config.accountChart] as AccountChart; newValue[0] = newValue[0] === "on" ? "off" : "on"; @@ -1135,7 +1071,15 @@ $(".pageAccount #accountHistoryChart").on("click", () => { loadMoreLines(index); if (window === undefined) return; const windowHeight = $(window).height() ?? 0; - const offset = $(`#result-${index}`).offset()?.top ?? 0; + + const resultId = filteredResults[index]?._id; + if (resultId === undefined) { + throw new Error("Cannot find result for index " + index); + } + const element = $(`.resultRow[data-id="${resultId}"`); + $(".resultRow").removeClass("active"); + + const offset = element.offset()?.top ?? 0; const scrollTo = offset - windowHeight / 2; $([document.documentElement, document.body]) .stop(true) @@ -1144,7 +1088,6 @@ $(".pageAccount #accountHistoryChart").on("click", () => { { duration: Misc.applyReducedMotion(500), done: () => { - const element = $(`#result-${index}`); $(".resultRow").removeClass("active"); requestAnimationFrame(() => element.addClass("active")); }, @@ -1154,13 +1097,11 @@ $(".pageAccount #accountHistoryChart").on("click", () => { $(".pageAccount").on("click", ".miniResultChartButton", async (event) => { const target = $(event.currentTarget); + const resultId: string = target.parents("tr").data("id") as string; if (target.hasClass("loading")) return; if (target.hasClass("disabled")) return; - const filteredId = target.attr("filteredResultsId"); - if (filteredId === undefined) return; - - const result = filteredResults[parseInt(filteredId)]; + const result = filteredResults.find((it) => it._id === resultId); if (result === undefined) return; let chartData = result.chartData as ChartData; @@ -1212,56 +1153,6 @@ $(".pageAccount").on("click", ".miniResultChartButton", async (event) => { MiniResultChartModal.show(chartData); }); -$(".pageAccount .group.history").on("click", ".history-wpm-header", () => { - sortAndRefreshHistory("wpm", ".history-wpm-header"); -}); - -$(".pageAccount .group.history").on("click", ".history-raw-header", () => { - sortAndRefreshHistory("rawWpm", ".history-raw-header"); -}); - -$(".pageAccount .group.history").on("click", ".history-acc-header", () => { - sortAndRefreshHistory("acc", ".history-acc-header"); -}); - -$(".pageAccount .group.history").on( - "click", - ".history-correct-chars-header", - () => { - sortAndRefreshHistory("correctChars", ".history-correct-chars-header"); - } -); - -$(".pageAccount .group.history").on( - "click", - ".history-incorrect-chars-header", - () => { - sortAndRefreshHistory("incorrectChars", ".history-incorrect-chars-header"); - } -); - -$(".pageAccount .group.history").on( - "click", - ".history-consistency-header", - () => { - sortAndRefreshHistory("consistency", ".history-consistency-header"); - } -); - -$(".pageAccount .group.history").on("click", ".history-date-header", () => { - sortAndRefreshHistory("timestamp", ".history-date-header"); -}); - -// Resets sorting to by date' when applying filers (normal or advanced) -$(".pageAccount .group.history").on( - "click", - ".buttonsAndTitle .buttons button", - () => { - // We want to 'force' descending sort: - sortAndRefreshHistory("timestamp", ".history-date-header", true); - } -); - $(".pageAccount .group.topFilters, .pageAccount .filterButtons").on( "click", "button", @@ -1368,6 +1259,18 @@ export const page = new Page({ snapshot !== undefined ? new Date(snapshot.addedAt).getFullYear() : 2020 ); + if (historyTable === undefined) { + historyTable = new SortedTableWithLimit>({ + limit: 10, + table: ".pageAccount .content .history table", + data: filteredResults, + buildRow: (val) => { + return buildResultRow(val); + }, + initialSort: { property: "timestamp", descending: true }, + }); + } + await update().then(() => { void updateChartColors(); $(".pageAccount .content .accountVerificatinNotice").remove(); diff --git a/frontend/vite.config.prod.js b/frontend/vite.config.prod.js index c08de26295bc..94acf26ac503 100644 --- a/frontend/vite.config.prod.js +++ b/frontend/vite.config.prod.js @@ -9,7 +9,13 @@ import { checker } from "vite-plugin-checker"; import { writeFileSync } from "fs"; // eslint-disable-next-line import/no-unresolved import UnpluginInjectPreload from "unplugin-inject-preload/vite"; -import { readdirSync, readFileSync, statSync } from "node:fs"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + statSync, +} from "node:fs"; import { ViteMinifyPlugin } from "vite-plugin-minify"; import { sentryVitePlugin } from "@sentry/vite-plugin"; import { getFontsConig } from "./vite.config"; @@ -68,9 +74,14 @@ export default { apply: "build", closeBundle() { + const distPath = path.resolve(__dirname, "dist"); + if (!existsSync(distPath)) { + mkdirSync(distPath, { recursive: true }); + } + const version = CLIENT_VERSION; const versionJson = JSON.stringify({ version }); - const versionPath = path.resolve(__dirname, "dist/version.json"); + const versionPath = path.resolve(distPath, "version.json"); writeFileSync(versionPath, versionJson); }, },