diff --git a/frontend/src/ts/controllers/profile-search-controller.ts b/frontend/src/ts/controllers/profile-search-controller.ts deleted file mode 100644 index 2294d634eb98..000000000000 --- a/frontend/src/ts/controllers/profile-search-controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { InputIndicator } from "../elements/input-indicator"; -import { sleep } from "../utils/misc"; -import Ape from "../ape"; -import { navigate } from "../controllers/route-controller"; -import * as Skeleton from "../utils/skeleton"; - -const searchIndicator = new InputIndicator( - $(".page.pageProfileSearch .search input"), - { - notFound: { - icon: "fa-user-slash", - level: -1, - }, - error: { - icon: "fa-times", - level: -1, - }, - checking: { - icon: "fa-circle-notch", - spinIcon: true, - level: 1, - }, - } -); - -function disableInputs(): void { - $(".page.pageProfileSearch .search button").addClass("disabled"); - $(".page.pageProfileSearch .search input").attr("disabled", "disabled"); -} - -function enableInputs(): void { - $(".page.pageProfileSearch .search button").removeClass("disabled"); - $(".page.pageProfileSearch .search input").removeAttr("disabled"); -} - -function areInputsDisabled(): boolean { - return ( - $(".page.pageProfileSearch .search input").attr("disabled") !== undefined - ); -} - -function focusInput(): void { - $(".page.pageProfileSearch .search input").trigger("focus"); -} - -async function lookupProfile(): Promise { - searchIndicator.hide(); - const name = $(".page.pageProfileSearch .search input").val() as string; - if (name === "") return; - - searchIndicator.show("checking"); - disableInputs(); - - await sleep(500); - - const response = await Ape.users.getProfile({ params: { uidOrName: name } }); - enableInputs(); - if (response.status === 404) { - focusInput(); - searchIndicator.show("notFound", "User not found"); - return; - } else if (response.status !== 200) { - focusInput(); - searchIndicator.show("error", `Error: ${response.body.message}`); - return; - } - searchIndicator.hide(); - await navigate(`/profile/${name}`, { - data: response.body.data, - }); -} - -$(".page.pageProfileSearch form").on("submit", (e) => { - e.preventDefault(); - if (areInputsDisabled()) return; - void lookupProfile(); -}); - -Skeleton.save("pageProfileSearch"); diff --git a/frontend/src/ts/controllers/route-controller.ts b/frontend/src/ts/controllers/route-controller.ts index 2c0eda4b504f..1aa6eab0af00 100644 --- a/frontend/src/ts/controllers/route-controller.ts +++ b/frontend/src/ts/controllers/route-controller.ts @@ -6,6 +6,7 @@ import { isFunboxActive } from "../test/funbox/list"; import * as TestState from "../test/test-state"; import * as Notifications from "../elements/notifications"; import { LoadingOptions } from "../pages/page"; +import * as NavigationEvent from "../observables/navigation-event"; //source: https://www.youtube.com/watch?v=OstALBk-jTc // https://www.youtube.com/watch?v=OstALBk-jTc @@ -249,3 +250,7 @@ document.addEventListener("DOMContentLoaded", () => { } }); }); + +NavigationEvent.subscribe((it) => { + void navigate(it.url, { data: it.data }); +}); diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts index ca12b70244d1..31d232bf1f0f 100644 --- a/frontend/src/ts/index.ts +++ b/frontend/src/ts/index.ts @@ -37,7 +37,6 @@ import { egVideoListener } from "./popups/video-ad-popup"; import "./states/connection"; import "./test/tts"; import "./elements/fps-counter"; -import "./controllers/profile-search-controller"; import { isDevEnvironment, addToGlobal } from "./utils/misc"; import * as VersionButton from "./elements/version-button"; import * as Focus from "./test/focus"; diff --git a/frontend/src/ts/observables/navigation-event.ts b/frontend/src/ts/observables/navigation-event.ts new file mode 100644 index 000000000000..42241636204f --- /dev/null +++ b/frontend/src/ts/observables/navigation-event.ts @@ -0,0 +1,23 @@ +type NavigationEvent = { + url: string; + data?: unknown; +}; + +type SubscribeFunction = (event: NavigationEvent) => void; + +const subscribers: SubscribeFunction[] = []; + +export function subscribe(fn: SubscribeFunction): void { + subscribers.push(fn); +} + +export function dispatch(event: NavigationEvent): void { + subscribers.forEach((fn) => { + try { + fn(event); + } catch (e) { + console.error("Navigation event subscriber threw an error"); + console.error(e); + } + }); +} diff --git a/frontend/src/ts/pages/profile-search.ts b/frontend/src/ts/pages/profile-search.ts index dcf52ea05c3a..7fea826ba13d 100644 --- a/frontend/src/ts/pages/profile-search.ts +++ b/frontend/src/ts/pages/profile-search.ts @@ -1,5 +1,24 @@ import Page from "./page"; import * as Skeleton from "../utils/skeleton"; +import Ape from "../ape"; +import { + ValidatedHtmlInputElement, + validateWithIndicator, +} from "../elements/input-validation"; +import { UserNameSchema, UserProfile } from "@monkeytype/schemas/users"; +import { remoteValidation } from "../utils/remote-validation"; +import * as NavigationEvent from "../observables/navigation-event"; + +let nameInputEl: ValidatedHtmlInputElement | null = null; +let lastProfile: UserProfile | null = null; + +function enableButton(): void { + $('.page.pageProfileSearch button[type="submit"]').prop("disabled", false); +} + +function disableButton(): void { + $('.page.pageProfileSearch button[type="submit"]').prop("disabled", true); +} export const page = new Page({ id: "profileSearch", @@ -10,9 +29,54 @@ export const page = new Page({ }, beforeShow: async (): Promise => { Skeleton.append("pageProfileSearch", "main"); - $(".page.pageProfileSearch input").val(""); + + if (nameInputEl === null) { + nameInputEl = validateWithIndicator( + document.querySelector( + ".page.pageProfileSearch input" + ) as HTMLInputElement, + { + schema: UserNameSchema, + isValid: remoteValidation( + async (name) => + Ape.users.getProfile({ params: { uidOrName: name } }), + { + check: (data) => { + lastProfile = data; + return true; + }, + on4xx: () => "Unknown user", + } + ), + callback: (result) => { + if (result.status === "success") { + enableButton(); + } else { + disableButton(); + lastProfile = null; + } + }, + } + ); + } + + nameInputEl.setValue(null); + disableButton(); }, afterShow: async (): Promise => { $(".page.pageProfileSearch input").trigger("focus"); }, }); + +$(".page.pageProfileSearch form").on("submit", (e) => { + e.preventDefault(); + if (lastProfile === null) return; + NavigationEvent.dispatch({ + url: `/profile/${lastProfile.name}`, + data: lastProfile, + }); +}); + +$(() => { + Skeleton.save("pageProfileSearch"); +});