From d25b0d2b2702cf5b296e654d7c15611b4fa05509 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 11 Sep 2024 15:32:45 +0200 Subject: [PATCH] frontend --- .../src/ts/ape/adapters/ts-rest-adapter.ts | 2 +- .../ts/controllers/analytics-controller.ts | 12 ++++--- .../src/ts/controllers/captcha-controller.ts | 3 +- .../src/ts/controllers/pw-ad-controller.ts | 4 ++- .../src/ts/elements/account/result-filters.ts | 7 ++++ frontend/src/ts/elements/leaderboards.ts | 1 + .../ts/elements/settings/settings-group.ts | 2 +- .../src/ts/elements/test-activity-calendar.ts | 6 ++-- frontend/src/ts/elements/test-activity.ts | 3 ++ frontend/src/ts/event-handlers/global.ts | 4 +-- frontend/src/ts/modals/edit-preset.ts | 4 +-- frontend/src/ts/modals/quote-report.ts | 1 + frontend/src/ts/modals/quote-search.ts | 1 + frontend/src/ts/modals/quote-submit.ts | 1 + frontend/src/ts/modals/user-report.ts | 1 + frontend/src/ts/modals/word-filter.ts | 3 ++ frontend/src/ts/pages/account-settings.ts | 2 +- frontend/src/ts/pages/settings.ts | 4 +-- frontend/src/ts/test/poetry.ts | 2 +- frontend/src/ts/test/test-logic.ts | 4 +-- frontend/src/ts/test/wikipedia.ts | 7 ++-- .../src/ts/utils/local-storage-with-schema.ts | 2 +- frontend/src/ts/utils/misc.ts | 21 +++++++++++ frontend/src/ts/utils/results.ts | 4 +-- frontend/src/ts/utils/url-handler.ts | 35 +++++++++---------- 25 files changed, 89 insertions(+), 47 deletions(-) diff --git a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts index 47904cf51085..81e79edfdae7 100644 --- a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts +++ b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts @@ -42,7 +42,7 @@ function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{ : AbortSignal.timeout(timeout), }); - const body = await response.json(); + const body = (await response.json()) as object; if (response.status >= 400) { console.error(`${request.method} ${request.path} failed`, { status: response.status, diff --git a/frontend/src/ts/controllers/analytics-controller.ts b/frontend/src/ts/controllers/analytics-controller.ts index 90b71dab89d3..56fbc7eaf086 100644 --- a/frontend/src/ts/controllers/analytics-controller.ts +++ b/frontend/src/ts/controllers/analytics-controller.ts @@ -9,6 +9,11 @@ import { createErrorMessage } from "../utils/misc"; let analytics: AnalyticsType; +type AcceptedCookies = { + security: boolean; + analytics: boolean; +}; + export async function log( eventName: string, params?: Record @@ -21,12 +26,9 @@ export async function log( } const lsString = localStorage.getItem("acceptedCookies"); -let acceptedCookies: { - security: boolean; - analytics: boolean; -} | null; +let acceptedCookies; if (lsString !== undefined && lsString !== null && lsString !== "") { - acceptedCookies = JSON.parse(lsString); + acceptedCookies = JSON.parse(lsString) as AcceptedCookies; } else { acceptedCookies = null; } diff --git a/frontend/src/ts/controllers/captcha-controller.ts b/frontend/src/ts/controllers/captcha-controller.ts index 610580d3b111..46aee7b3766a 100644 --- a/frontend/src/ts/controllers/captcha-controller.ts +++ b/frontend/src/ts/controllers/captcha-controller.ts @@ -13,12 +13,13 @@ export function render( } //@ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const widgetId = grecaptcha.render(element, { sitekey: siteKey, callback, }); - captchas[id] = widgetId; + captchas[id] = widgetId as number; } export function reset(id: string): void { diff --git a/frontend/src/ts/controllers/pw-ad-controller.ts b/frontend/src/ts/controllers/pw-ad-controller.ts index 6c7f91f5c1b7..dd5378838182 100644 --- a/frontend/src/ts/controllers/pw-ad-controller.ts +++ b/frontend/src/ts/controllers/pw-ad-controller.ts @@ -143,8 +143,9 @@ export function init(): void { headOfDocument.appendChild(rampScript); window._pwGA4PageviewId = "".concat(Date.now()); - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unsafe-assignment window.dataLayer = window.dataLayer || []; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment window.gtag = // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions window.gtag || @@ -156,6 +157,7 @@ export function init(): void { gtag("config", "G-KETCPNHRJF", { send_page_view: false }); gtag("event", "ramp_js", { send_to: "G-KETCPNHRJF", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment pageview_id: window._pwGA4PageviewId, }); } diff --git a/frontend/src/ts/elements/account/result-filters.ts b/frontend/src/ts/elements/account/result-filters.ts index abedd150c391..c77797aefd6b 100644 --- a/frontend/src/ts/elements/account/result-filters.ts +++ b/frontend/src/ts/elements/account/result-filters.ts @@ -350,10 +350,12 @@ export function updateActive(): void { } for (const [id, select] of Object.entries(groupSelects)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const ss = select; const group = getGroup(id as ResultFiltersGroup); const everythingSelected = Object.values(group).every((v) => v === true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const newData = ss.store.getData(); const allOption = $( @@ -682,8 +684,10 @@ function adjustScrollposition( group: ResultFiltersGroup, topItem: number = 0 ): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const slimSelect = groupSelects[group]; if (slimSelect === undefined) return; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const listElement = slimSelect.render.content.list; const topListItem = listElement.children.item(topItem) as HTMLElement; @@ -779,6 +783,7 @@ export async function appendButtons( ); if (el) { el.innerHTML = html; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select groupSelects["language"] = new SlimSelect({ select: el.querySelector(".languageSelect") as HTMLSelectElement, settings: { @@ -840,6 +845,7 @@ export async function appendButtons( ); if (el) { el.innerHTML = html; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select groupSelects["funbox"] = new SlimSelect({ select: el.querySelector(".funboxSelect") as HTMLSelectElement, settings: { @@ -897,6 +903,7 @@ export async function appendButtons( ); if (el) { el.innerHTML = html; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select groupSelects["tags"] = new SlimSelect({ select: el.querySelector(".tagsSelect") as HTMLSelectElement, settings: { diff --git a/frontend/src/ts/elements/leaderboards.ts b/frontend/src/ts/elements/leaderboards.ts index 02c0810cf518..e8c23aa113b0 100644 --- a/frontend/src/ts/elements/leaderboards.ts +++ b/frontend/src/ts/elements/leaderboards.ts @@ -712,6 +712,7 @@ export function show(): void { Config.typingSpeedUnit + '
accuracy
' ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select languageSelector = new SlimSelect({ select: "#leaderboardsWrapper #leaderboards .leaderboardsTop .buttonGroup.timeRange .languageSelect", diff --git a/frontend/src/ts/elements/settings/settings-group.ts b/frontend/src/ts/elements/settings/settings-group.ts index 75bf4f20a3ab..2ae136bf52aa 100644 --- a/frontend/src/ts/elements/settings/settings-group.ts +++ b/frontend/src/ts/elements/settings/settings-group.ts @@ -120,7 +120,7 @@ export default class SettingsGroup { select.value = this.configValue as string; //@ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-assertion -- TODO: update slim-select const ss = select.slim as SlimSelect | undefined; ss?.store.setSelectedBy("value", [this.configValue as string]); ss?.render.renderValues(); diff --git a/frontend/src/ts/elements/test-activity-calendar.ts b/frontend/src/ts/elements/test-activity-calendar.ts index 95b3f08d2693..a2209ddf0b8d 100644 --- a/frontend/src/ts/elements/test-activity-calendar.ts +++ b/frontend/src/ts/elements/test-activity-calendar.ts @@ -57,9 +57,9 @@ export class TestActivityCalendar implements MonkeyTypes.TestActivityCalendar { lastDay: Date ): (number | null | undefined)[] { //fill calendar with enough values - const values: (number | null | undefined)[] = new Array( - Math.max(0, 386 - data.length) - ).fill(undefined); + const values = new Array(Math.max(0, 386 - data.length)).fill( + undefined + ) as (number | null | undefined)[]; values.push(...data); //discard values outside the calendar range diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 634edda71fda..9bf035140dce 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -18,6 +18,7 @@ export function init( } $("#testActivity").removeClass("hidden"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select yearSelector = getYearSelector(); initYearSelector("current", userSignUpDate?.getFullYear() || 2022); update(calendar); @@ -84,6 +85,7 @@ export function initYearSelector( } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const yearSelect = getYearSelector(); yearSelect.setData(years); years.length > 1 ? yearSelect.enable() : yearSelect.disable(); @@ -102,6 +104,7 @@ function updateMonths(months: MonkeyTypes.TestActivityMonth[]): void { function getYearSelector(): SlimSelect { if (yearSelector !== undefined) return yearSelector; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select yearSelector = new SlimSelect({ select: "#testActivity .yearSelect", settings: { diff --git a/frontend/src/ts/event-handlers/global.ts b/frontend/src/ts/event-handlers/global.ts index 8b91c46b2e7e..d1376761d0a8 100644 --- a/frontend/src/ts/event-handlers/global.ts +++ b/frontend/src/ts/event-handlers/global.ts @@ -46,7 +46,7 @@ window.onerror = function (message, url, line, column, error): void { window.onunhandledrejection = function (e): void { if (Misc.isDevEnvironment()) { - const message = e.reason.message ?? e.reason; + const message = (e.reason.message ?? e.reason) as string; Notifications.add(`${message}`, -1, { customTitle: "DEV: Unhandled rejection", duration: 5, @@ -54,6 +54,6 @@ window.onunhandledrejection = function (e): void { console.error(e); } void log("error", { - error: e.reason.stack ?? "", + error: (e.reason.stack ?? "") as string, }); }; diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 894230badf95..726b9349b4ae 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -67,9 +67,9 @@ async function apply(): Promise { "data-preset-id" ) as string; - const updateConfig: boolean = $("#editPresetModal .modal label input").prop( + const updateConfig = $("#editPresetModal .modal label input").prop( "checked" - ); + ) as boolean; let configChanges: MonkeyTypes.ConfigChanges = {}; diff --git a/frontend/src/ts/modals/quote-report.ts b/frontend/src/ts/modals/quote-report.ts index 6934320579e4..5b4cf7ba0966 100644 --- a/frontend/src/ts/modals/quote-report.ts +++ b/frontend/src/ts/modals/quote-report.ts @@ -46,6 +46,7 @@ export async function show( $("#quoteReportModal .reason").val("Grammatical error"); $("#quoteReportModal .comment").val(""); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select state.reasonSelect = new SlimSelect({ select: "#quoteReportModal .reason", settings: { diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index 53f95b38bed4..42456ffda0d7 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -273,6 +273,7 @@ export async function show(showOptions?: ShowOptions): Promise { $("#quoteSearchModal .goToQuoteApprove").addClass("hidden"); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select lengthSelect = new SlimSelect({ select: "#quoteSearchModal .quoteLengthFilter", diff --git a/frontend/src/ts/modals/quote-submit.ts b/frontend/src/ts/modals/quote-submit.ts index 20a6b7c58c29..3f0c1aa5e0cd 100644 --- a/frontend/src/ts/modals/quote-submit.ts +++ b/frontend/src/ts/modals/quote-submit.ts @@ -65,6 +65,7 @@ export async function show(showOptions: ShowOptions): Promise { ); await initDropdown(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select select = new SlimSelect({ select: "#quoteSubmitModal .newQuoteLanguage", }); diff --git a/frontend/src/ts/modals/user-report.ts b/frontend/src/ts/modals/user-report.ts index 8d7a7811741b..3c926afcfd15 100644 --- a/frontend/src/ts/modals/user-report.ts +++ b/frontend/src/ts/modals/user-report.ts @@ -51,6 +51,7 @@ export async function show(options: ShowOptions): Promise { "Inappropriate name"; (modalEl.querySelector(".comment") as HTMLTextAreaElement).value = ""; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select select = new SlimSelect({ select: modalEl.querySelector(".reason") as HTMLElement, settings: { diff --git a/frontend/src/ts/modals/word-filter.ts b/frontend/src/ts/modals/word-filter.ts index e954eab029c3..84b1e75d3221 100644 --- a/frontend/src/ts/modals/word-filter.ts +++ b/frontend/src/ts/modals/word-filter.ts @@ -159,18 +159,21 @@ export async function show(showOptions?: ShowOptions): Promise { void modal.show({ ...showOptions, beforeAnimation: async (modalEl) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select languageSelect = new SlimSelect({ select: "#wordFilterModal .languageInput", settings: { contentLocation: modalEl, }, }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select layoutSelect = new SlimSelect({ select: "#wordFilterModal .layoutInput", settings: { contentLocation: modal.getModal(), }, }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select presetSelect = new SlimSelect({ select: "#wordFilterModal .presetInput", settings: { diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts index 403c1bc7a5bb..2e51be5b46fc 100644 --- a/frontend/src/ts/pages/account-settings.ts +++ b/frontend/src/ts/pages/account-settings.ts @@ -183,7 +183,7 @@ export function updateUI(): void { } $(".page.pageAccountSettings").on("click", ".tabs button", (event) => { - state.activeTab = $(event.target).data("tab"); + state.activeTab = $(event.target).data("tab") as State["activeTab"]; updateTabs(); }); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 24dc593f2010..ff78a658b0d5 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -1062,7 +1062,7 @@ $(".pageSettings .section.tags").on( "click", ".tagsList .tag .tagButton", (e) => { - const target = e.currentTarget; + const target = e.currentTarget as HTMLElement; const tagid = $(target).parent(".tag").attr("data-id") as string; TagController.toggle(tagid); $(target).toggleClass("active"); @@ -1073,7 +1073,7 @@ $(".pageSettings .section.presets").on( "click", ".presetsList .preset .presetButton", async (e) => { - const target = e.currentTarget; + const target = e.currentTarget as HTMLElement; const presetid = $(target).parent(".preset").attr("data-id") as string; await PresetController.apply(presetid); void update(); diff --git a/frontend/src/ts/test/poetry.ts b/frontend/src/ts/test/poetry.ts index 5257dda1ae92..074a6e50f067 100644 --- a/frontend/src/ts/test/poetry.ts +++ b/frontend/src/ts/test/poetry.ts @@ -50,7 +50,7 @@ export async function getPoem(): Promise
{ try { const response = await axios.get(apiURL); - const poemObj: PoemObject = response.data[0]; + const poemObj = response.data[0] as PoemObject; const words: string[] = []; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 4a55a952fcf3..cd5be47cce0e 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1182,9 +1182,9 @@ async function saveResult( } if (data.insertedId !== undefined) { - const result: MonkeyTypes.FullResult = JSON.parse( + const result = JSON.parse( JSON.stringify(completedEvent) - ); + ) as MonkeyTypes.FullResult; result._id = data.insertedId; if (data.isPb !== undefined && data.isPb) { result.isPb = true; diff --git a/frontend/src/ts/test/wikipedia.ts b/frontend/src/ts/test/wikipedia.ts index 2e74959ad4d9..d34fbb58f406 100644 --- a/frontend/src/ts/test/wikipedia.ts +++ b/frontend/src/ts/test/wikipedia.ts @@ -268,7 +268,7 @@ export async function getSection(language: string): Promise
{ let pageid = 0; if (randomPostReq.status === 200) { - const postObj: Post = await randomPostReq.json(); + const postObj = (await randomPostReq.json()) as Post; sectionObj.title = postObj.title; sectionObj.author = postObj.author; pageid = postObj.pageid; @@ -286,8 +286,9 @@ export async function getSection(language: string): Promise
{ sectionReq.onload = (): void => { if (sectionReq.readyState === 4) { if (sectionReq.status === 200) { - let sectionText: string = JSON.parse(sectionReq.responseText).query - .pages[pageid.toString()].extract; + let sectionText = JSON.parse(sectionReq.responseText).query.pages[ + pageid.toString() + ].extract as string; // Converting to one paragraph sectionText = sectionText.replace(/<\/p>

+/g, " "); diff --git a/frontend/src/ts/utils/local-storage-with-schema.ts b/frontend/src/ts/utils/local-storage-with-schema.ts index 6cee133902e2..be26f34589cd 100644 --- a/frontend/src/ts/utils/local-storage-with-schema.ts +++ b/frontend/src/ts/utils/local-storage-with-schema.ts @@ -25,7 +25,7 @@ export class LocalStorageWithSchema { return this.fallback; } - let jsonParsed; + let jsonParsed: unknown; try { jsonParsed = JSON.parse(value); } catch (e) { diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 28a9d89a13ad..28e8828dac5b 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -8,6 +8,7 @@ import { Mode2, PersonalBests, } from "@monkeytype/contracts/schemas/shared"; +import { ZodError, ZodSchema } from "zod"; export function kogasa(cov: number): number { return ( @@ -684,4 +685,24 @@ export function isObject(obj: unknown): obj is Record { return typeof obj === "object" && !Array.isArray(obj) && obj !== null; } +/** + * Parse a JSON string into an object and validate it against a schema + * @param input JSON string + * @param schema Zod schema to validate the JSON against + * @returns The parsed JSON object + */ +export function parseJsonWithSchema(input: string, schema: ZodSchema): T { + try { + const jsonParsed = JSON.parse(input) as unknown; + const zodParsed = schema.parse(jsonParsed); + return zodParsed; + } catch (error) { + if (error instanceof ZodError) { + throw new Error(error.errors.map((err) => err.message).join("\n")); + } else { + throw error; + } + } +} + // DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES diff --git a/frontend/src/ts/utils/results.ts b/frontend/src/ts/utils/results.ts index e389eef9af36..29b1353832b3 100644 --- a/frontend/src/ts/utils/results.ts +++ b/frontend/src/ts/utils/results.ts @@ -19,9 +19,9 @@ export async function syncNotSignedInLastResult(uid: string): Promise { return; } - const result: MonkeyTypes.FullResult = JSON.parse( + const result = JSON.parse( JSON.stringify(notSignedInLastResult) - ); + ) as MonkeyTypes.FullResult; result._id = response.body.data.insertedId; if (response.body.data.isPb) { result.isPb = true; diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 38fef3afd7e8..dc05b8c63f61 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -65,37 +65,32 @@ export async function linkDiscord(hashOverride: string): Promise { } } +const customThemeUrlDataSchema = z.object({ + c: CustomThemeColorsSchema, + i: z.string().optional(), + s: CustomBackgroundSizeSchema.optional(), + f: CustomBackgroundFilterSchema.optional(), +}); + export function loadCustomThemeFromUrl(getOverride?: string): void { const getValue = Misc.findGetParameter("customTheme", getOverride); if (getValue === null) return; - let decoded = null; + let decoded: z.infer; try { - decoded = JSON.parse(atob(getValue)); + decoded = Misc.parseJsonWithSchema( + atob(getValue), + customThemeUrlDataSchema + ); } catch (e) { console.log("Custom theme URL decoding failed", e); Notifications.add( - "Failed to load theme from URL: could not decode theme", + "Failed to load theme from URL: " + (e as Error).message, 0 ); return; } - const decodedSchema = z.object({ - c: CustomThemeColorsSchema, - i: z.string().optional(), - s: CustomBackgroundSizeSchema.optional(), - f: CustomBackgroundFilterSchema.optional(), - }); - - const parsed = decodedSchema.safeParse(decoded); - if (!parsed.success) { - Notifications.add("Failed to load theme from URL: invalid data schema", 0); - return; - } - - decoded = parsed.data; - let colorArray: CustomThemeColors | undefined; let image: string | undefined; let size: CustomBackgroundSize | undefined; @@ -151,7 +146,9 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { const getValue = Misc.findGetParameter("testSettings", getOverride); if (getValue === null) return; - const de: SharedTestSettings = JSON.parse(decompressFromURI(getValue) ?? ""); + const de = JSON.parse( + decompressFromURI(getValue) ?? "" + ) as SharedTestSettings; const applied: Record = {};