From 2b380bb931fb749c9eacf18f2eff5849990ebf89 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 12 Dec 2025 15:16:11 +0100 Subject: [PATCH] refactor: use ElementWithUtils in page class (@fehmer) (#7223) --- frontend/__tests__/setup-tests.ts | 1 + frontend/src/ts/controllers/page-controller.ts | 18 +++++++++--------- frontend/src/ts/pages/404.ts | 3 ++- frontend/src/ts/pages/about.ts | 3 ++- frontend/src/ts/pages/account-settings.ts | 3 ++- frontend/src/ts/pages/account.ts | 3 ++- frontend/src/ts/pages/friends.ts | 5 ++--- frontend/src/ts/pages/leaderboards.ts | 3 ++- frontend/src/ts/pages/loading.ts | 3 ++- frontend/src/ts/pages/login.ts | 2 +- frontend/src/ts/pages/page.ts | 5 +++-- frontend/src/ts/pages/profile-search.ts | 2 +- frontend/src/ts/pages/profile.ts | 3 ++- frontend/src/ts/pages/settings.ts | 2 +- frontend/src/ts/pages/test.ts | 3 ++- frontend/src/ts/utils/dom.ts | 16 ++++++++++++++++ 16 files changed, 50 insertions(+), 25 deletions(-) diff --git a/frontend/__tests__/setup-tests.ts b/frontend/__tests__/setup-tests.ts index 6828d2772..0ac3acf1e 100644 --- a/frontend/__tests__/setup-tests.ts +++ b/frontend/__tests__/setup-tests.ts @@ -64,6 +64,7 @@ vi.mock("../src/ts/utils/dom", () => { getOffsetTop: vi.fn().mockReturnValue(0), getOffsetLeft: vi.fn().mockReturnValue(0), animate: vi.fn().mockResolvedValue(null), + promiseAnimate: vi.fn().mockResolvedValue(null), native: document.createElement("div"), }; }; diff --git a/frontend/src/ts/controllers/page-controller.ts b/frontend/src/ts/controllers/page-controller.ts index fbf98be33..e60461598 100644 --- a/frontend/src/ts/controllers/page-controller.ts +++ b/frontend/src/ts/controllers/page-controller.ts @@ -59,14 +59,14 @@ async function showSyncLoading({ loadingOptions: LoadingOptions[]; totalDuration: number; }): Promise { - PageLoading.page.element.removeClass("hidden").css("opacity", 0); + PageLoading.page.element.show().setStyle({ opacity: "0" }); await PageLoading.page.beforeShow({}); const fillDivider = loadingOptions.length; const fillOffset = 100 / fillDivider; //void here to run the loading promise as soon as possible - void Misc.promiseAnimate(PageLoading.page.element[0] as HTMLElement, { + void PageLoading.page.element.promiseAnimate({ opacity: "1", duration: totalDuration / 2, }); @@ -97,13 +97,13 @@ async function showSyncLoading({ } } - await Misc.promiseAnimate(PageLoading.page.element[0] as HTMLElement, { + await PageLoading.page.element.promiseAnimate({ opacity: "0", duration: totalDuration / 2, }); await PageLoading.page.afterHide(); - PageLoading.page.element.addClass("hidden"); + PageLoading.page.element.hide(); } // Global abort controller for keyframe promises @@ -206,12 +206,12 @@ export async function change( //previous page await previousPage?.beforeHide?.(); - previousPage.element.removeClass("hidden").css("opacity", 1); - await Misc.promiseAnimate(previousPage.element[0] as HTMLElement, { + previousPage.element.show().setStyle({ opacity: "1" }); + await previousPage.element.promiseAnimate({ opacity: "0", duration: totalDuration / 2, }); - previousPage.element.addClass("hidden"); + previousPage.element.hide(); await previousPage?.afterHide(); // we need to evaluate and store next page loading mode in case options.loadingOptions.loadingMode is sync @@ -281,8 +281,8 @@ export async function change( }); } - nextPage.element.removeClass("hidden").css("opacity", 0); - await Misc.promiseAnimate(nextPage.element[0] as HTMLElement, { + nextPage.element.show().setStyle({ opacity: "0" }); + await nextPage.element.promiseAnimate({ opacity: "1", duration: totalDuration / 2, }); diff --git a/frontend/src/ts/pages/404.ts b/frontend/src/ts/pages/404.ts index bb67ce5b7..c081af356 100644 --- a/frontend/src/ts/pages/404.ts +++ b/frontend/src/ts/pages/404.ts @@ -1,9 +1,10 @@ import Page from "./page"; import * as Skeleton from "../utils/skeleton"; +import { qsr } from "../utils/dom"; export const page = new Page({ id: "404", - element: $(".page.page404"), + element: qsr(".page.page404"), path: "/404", afterHide: async (): Promise => { Skeleton.remove("page404"); diff --git a/frontend/src/ts/pages/about.ts b/frontend/src/ts/pages/about.ts index 6761be0b7..a82da0c53 100644 --- a/frontend/src/ts/pages/about.ts +++ b/frontend/src/ts/pages/about.ts @@ -10,6 +10,7 @@ import * as Skeleton from "../utils/skeleton"; import { TypingStats, SpeedHistogram } from "@monkeytype/schemas/public"; import { getNumberWithMagnitude, numberWithSpaces } from "../utils/numbers"; import { tryCatch } from "@monkeytype/util/trycatch"; +import { qsr } from "../utils/dom"; function reset(): void { $(".pageAbout .contributors").empty(); @@ -199,7 +200,7 @@ function getHistogramDataBucketed(data: Record): { export const page = new Page({ id: "about", - element: $(".page.pageAbout"), + element: qsr(".page.pageAbout"), path: "/about", afterHide: async (): Promise => { reset(); diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts index 534ed19bd..4d9ab4502 100644 --- a/frontend/src/ts/pages/account-settings.ts +++ b/frontend/src/ts/pages/account-settings.ts @@ -12,6 +12,7 @@ import * as BlockedUserTable from "../elements/account-settings/blocked-user-tab import * as Notifications from "../elements/notifications"; import { z } from "zod"; import * as AuthEvent from "../observables/auth-event"; +import { qsr } from "../utils/dom"; const pageElement = $(".page.pageAccountSettings"); @@ -229,7 +230,7 @@ AuthEvent.subscribe((event) => { export const page = new PageWithUrlParams({ id: "accountSettings", display: "Account Settings", - element: pageElement, + element: qsr(".page.pageAccountSettings"), path: "/account-settings", urlParamsSchema: UrlParameterSchema, afterHide: async (): Promise => { diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 5af14a451..537725739 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -35,6 +35,7 @@ import { SnapshotResult } from "../constants/default-snapshot"; import Ape from "../ape"; import { AccountChart } from "@monkeytype/schemas/configs"; import { SortedTableWithLimit } from "../utils/sorted-table"; +import { qsr } from "../utils/dom"; let filterDebug = false; //toggle filterdebug @@ -1195,7 +1196,7 @@ ConfigEvent.subscribe(({ key }) => { export const page = new Page({ id: "account", - element: $(".page.pageAccount"), + element: qsr(".page.pageAccount"), path: "/account", loadingOptions: { loadingMode: () => { diff --git a/frontend/src/ts/pages/friends.ts b/frontend/src/ts/pages/friends.ts index 31d72ad9f..6b5c9a554 100644 --- a/frontend/src/ts/pages/friends.ts +++ b/frontend/src/ts/pages/friends.ts @@ -30,8 +30,7 @@ import { Friend, UserNameSchema } from "@monkeytype/schemas/users"; import * as Loader from "../elements/loader"; import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; import { remoteValidation } from "../utils/remote-validation"; - -const pageElement = $(".page.pageFriends"); +import { qsr } from "../utils/dom"; let friendsTable: SortedTable | undefined = undefined; @@ -499,7 +498,7 @@ function update(): void { export const page = new Page({ id: "friends", display: "Friends", - element: pageElement, + element: qsr(".page.pageFriends"), path: "/friends", loadingOptions: { loadingMode: () => { diff --git a/frontend/src/ts/pages/leaderboards.ts b/frontend/src/ts/pages/leaderboards.ts index 33dfcfeba..7336c8197 100644 --- a/frontend/src/ts/pages/leaderboards.ts +++ b/frontend/src/ts/pages/leaderboards.ts @@ -44,6 +44,7 @@ import { isSafeNumber } from "@monkeytype/util/numbers"; import { Mode, Mode2, ModeSchema } from "@monkeytype/schemas/shared"; import * as ServerConfiguration from "../ape/server-configuration"; import { getAvatarElement } from "../utils/discord-avatar"; +import { qsr } from "../utils/dom"; const LeaderboardTypeSchema = z.enum(["allTime", "weekly", "daily"]); type LeaderboardType = z.infer; @@ -1489,7 +1490,7 @@ $(".page.pageLeaderboards .buttonGroup.friendsOnlyButtons").on( export const page = new PageWithUrlParams({ id: "leaderboards", - element: $(".page.pageLeaderboards"), + element: qsr(".page.pageLeaderboards"), path: "/leaderboards", urlParamsSchema: UrlParameterSchema, diff --git a/frontend/src/ts/pages/loading.ts b/frontend/src/ts/pages/loading.ts index 98580073a..4e7437fc5 100644 --- a/frontend/src/ts/pages/loading.ts +++ b/frontend/src/ts/pages/loading.ts @@ -1,6 +1,7 @@ import Page from "./page"; import * as Skeleton from "../utils/skeleton"; import { promiseAnimate } from "../utils/misc"; +import { qsr } from "../utils/dom"; const pageEl = $(".page.pageLoading"); const barEl = pageEl.find(".bar"); @@ -45,7 +46,7 @@ export async function showBar(): Promise { export const page = new Page({ id: "loading", - element: pageEl, + element: qsr(".page.pageLoading"), path: "/", afterHide: async (): Promise => { Skeleton.remove("pageLoading"); diff --git a/frontend/src/ts/pages/login.ts b/frontend/src/ts/pages/login.ts index 73c63da78..b7cd52294 100644 --- a/frontend/src/ts/pages/login.ts +++ b/frontend/src/ts/pages/login.ts @@ -208,7 +208,7 @@ new ValidatedHtmlInputElement(passwordVerifyInputEl, { export const page = new Page({ id: "login", - element: $(".page.pageLogin"), + element: qsr(".page.pageLogin"), path: "/login", afterHide: async (): Promise => { hidePreloader(); diff --git a/frontend/src/ts/pages/page.ts b/frontend/src/ts/pages/page.ts index e177c663c..70375c86f 100644 --- a/frontend/src/ts/pages/page.ts +++ b/frontend/src/ts/pages/page.ts @@ -3,6 +3,7 @@ import { safeParse as parseUrlSearchParams, serialize as serializeUrlSearchParams, } from "zod-urlsearchparams"; +import { ElementWithUtils } from "../utils/dom"; export type PageName = | "loading" @@ -69,7 +70,7 @@ export type LoadingOptions = { type PageProperties = { id: PageName; display?: string; - element: JQuery; + element: ElementWithUtils; path: string; loadingOptions?: LoadingOptions; beforeHide?: () => Promise; @@ -84,7 +85,7 @@ async function empty(): Promise { export default class Page { public id: PageName; public display: string | undefined; - public element: JQuery; + public element: ElementWithUtils; public pathname: string; public loadingOptions: LoadingOptions | undefined; diff --git a/frontend/src/ts/pages/profile-search.ts b/frontend/src/ts/pages/profile-search.ts index 684bf7a8b..1bf6dbd90 100644 --- a/frontend/src/ts/pages/profile-search.ts +++ b/frontend/src/ts/pages/profile-search.ts @@ -20,7 +20,7 @@ function disableButton(): void { export const page = new Page({ id: "profileSearch", - element: $(".page.pageProfileSearch"), + element: qsr(".page.pageProfileSearch"), path: "/profile", afterHide: async (): Promise => { Skeleton.remove("pageProfileSearch"); diff --git a/frontend/src/ts/pages/profile.ts b/frontend/src/ts/pages/profile.ts index 2ee81a646..098a5a5af 100644 --- a/frontend/src/ts/pages/profile.ts +++ b/frontend/src/ts/pages/profile.ts @@ -12,6 +12,7 @@ import * as TestActivity from "../elements/test-activity"; import { TestActivityCalendar } from "../elements/test-activity-calendar"; import { getFirstDayOfTheWeek } from "../utils/date-and-time"; import { addFriend } from "./friends"; +import { qsr } from "../utils/dom"; const firstDayOfTheWeek = getFirstDayOfTheWeek(); @@ -260,7 +261,7 @@ $(".page.pageProfile").on("click", ".profile .addFriendButton", async () => { export const page = new Page({ id: "profile", - element: $(".page.pageProfile"), + element: qsr(".page.pageProfile"), path: "/profile", afterHide: async (): Promise => { Skeleton.remove("pageProfile"); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 239323893..c6707aaf0 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -1011,7 +1011,7 @@ AuthEvent.subscribe((event) => { export const page = new PageWithUrlParams({ id: "settings", - element: $(".page.pageSettings"), + element: qsr(".page.pageSettings"), path: "/settings", urlParamsSchema: StateSchema, afterHide: async (): Promise => { diff --git a/frontend/src/ts/pages/test.ts b/frontend/src/ts/pages/test.ts index 37b37a3ac..0791fdd53 100644 --- a/frontend/src/ts/pages/test.ts +++ b/frontend/src/ts/pages/test.ts @@ -9,10 +9,11 @@ import * as Keymap from "../elements/keymap"; import * as TestConfig from "../test/test-config"; import * as ScrollToTop from "../elements/scroll-to-top"; import { blurInputElement } from "../input/input-element"; +import { qsr } from "../utils/dom"; export const page = new Page({ id: "test", - element: $(".page.pageTest"), + element: qsr(".page.pageTest"), path: "/", beforeHide: async (): Promise => { blurInputElement(); diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index f47fa6e6b..467a7eef2 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -514,6 +514,22 @@ export class ElementWithUtils { animate(animationParams: AnimationParams): JSAnimation { return animejsAnimate(this.native, animationParams); } + + /** + * Animate the element using Anime.js + * @param animationParams The Anime.js animation parameters + */ + async promiseAnimate(animationParams: AnimationParams): Promise { + return new Promise((resolve) => { + animejsAnimate(this.native, { + ...animationParams, + onComplete: (self, e) => { + animationParams.onComplete?.(self, e); + resolve(); + }, + }); + }); + } } /**