From cc50db890bc46d5e9522d2aee4a87cba7fb59cc0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 12:36:58 +0100 Subject: [PATCH 01/24] solid --- frontend/src/index.html | 1 - .../src/ts/ape/adapters/ts-rest-adapter.ts | 8 +- frontend/src/ts/auth.ts | 15 +- .../ts/components/layout/overlays/Banners.tsx | 101 +++++++ .../components/layout/overlays/Overlays.tsx | 2 + frontend/src/ts/controllers/ad-controller.ts | 7 +- frontend/src/ts/elements/merch-banner.ts | 18 +- frontend/src/ts/elements/notifications.ts | 264 +++++------------- frontend/src/ts/elements/psa.ts | 70 ++--- .../src/ts/hooks/useTailwindBreakpoints.ts | 59 ++++ frontend/src/ts/observables/banner-event.ts | 18 -- frontend/src/ts/states/connection.ts | 20 +- frontend/src/ts/stores/banners.ts | 35 +++ 13 files changed, 325 insertions(+), 293 deletions(-) create mode 100644 frontend/src/ts/components/layout/overlays/Banners.tsx create mode 100644 frontend/src/ts/hooks/useTailwindBreakpoints.ts delete mode 100644 frontend/src/ts/observables/banner-event.ts create mode 100644 frontend/src/ts/stores/banners.ts diff --git a/frontend/src/index.html b/frontend/src/index.html index bfe7dd8ca2e1..bd0cff291cdd 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -7,7 +7,6 @@
-
+ ); +} + +export function Banners(): JSXElement { + const [ref, element] = useRefWithUtils(); + + const updateMargin = (): void => { + const height = element()?.getOffsetHeight() ?? 0; + qsr("#app").setStyle({ + paddingTop: height + convertRemToPixels(2) + "px", + }); + }; + + const debouncedMarginUpdate = debounce(100, updateMargin); + + onMount(() => { + window.addEventListener("resize", debouncedMarginUpdate); + }); + + onCleanup(() => { + window.removeEventListener("resize", debouncedMarginUpdate); + }); + + createEffect(on(() => getBanners().length, updateMargin)); + + return ( +
+ {(banner) => } +
+ ); +} diff --git a/frontend/src/ts/components/layout/overlays/Overlays.tsx b/frontend/src/ts/components/layout/overlays/Overlays.tsx index 5ebb9e1ee0fe..0d65c39b1f88 100644 --- a/frontend/src/ts/components/layout/overlays/Overlays.tsx +++ b/frontend/src/ts/components/layout/overlays/Overlays.tsx @@ -2,10 +2,12 @@ import { JSXElement } from "solid-js"; import { TailwindMediaQueryDebugger } from "../../utils/TailwindMediaQueryDebugger"; import { LoaderBar } from "./LoaderBar"; import { FpsCounter } from "./FpsCounter"; +import { Banners } from "./Banners"; export function Overlays(): JSXElement { return ( <> + diff --git a/frontend/src/ts/controllers/ad-controller.ts b/frontend/src/ts/controllers/ad-controller.ts index 4aa48f47e001..6058543a15aa 100644 --- a/frontend/src/ts/controllers/ad-controller.ts +++ b/frontend/src/ts/controllers/ad-controller.ts @@ -2,12 +2,13 @@ import { debounce } from "throttle-debounce"; // import * as Numbers from "@monkeytype/util/numbers"; import * as ConfigEvent from "../observables/config-event"; -import * as BannerEvent from "../observables/banner-event"; import Config from "../config"; import * as TestState from "../test/test-state"; import * as EG from "./eg-ad-controller"; import * as PW from "./pw-ad-controller"; import { onDOMReady, qs } from "../utils/dom"; +import { createEffect, on } from "solid-js"; +import { getBanners } from "../stores/banners"; const breakpoint = 900; let widerThanBreakpoint = true; @@ -309,9 +310,7 @@ ConfigEvent.subscribe(({ key, newValue }) => { } }); -BannerEvent.subscribe(() => { - updateVerticalMargin(); -}); +createEffect(on(() => getBanners().length, updateVerticalMargin)); onDOMReady(() => { updateBreakpoint(true); diff --git a/frontend/src/ts/elements/merch-banner.ts b/frontend/src/ts/elements/merch-banner.ts index 9cf21f622b70..3a6357263b3a 100644 --- a/frontend/src/ts/elements/merch-banner.ts +++ b/frontend/src/ts/elements/merch-banner.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; -import * as Notifications from "./notifications"; +import { addBanner } from "../stores/banners"; const closed = new LocalStorageWithSchema({ key: "merchBannerClosed3", @@ -10,15 +10,15 @@ const closed = new LocalStorageWithSchema({ export function showIfNotClosedBefore(): void { if (!closed.get()) { - Notifications.addBanner( - `New merch store now open, including a limited edition metal keycap! monkeytype.store`, - 1, - "./images/merch3.png", - false, - () => { + addBanner({ + level: 1, + icon: "fa fa-fw fa-shopping-bag", + allowHtml: true, + text: `New merch store now open, including a limited edition metal keycap! monkeytype.store`, + imagePath: "./images/merch3.png", + onClose: () => { closed.set(true); }, - true, - ); + }); } } diff --git a/frontend/src/ts/elements/notifications.ts b/frontend/src/ts/elements/notifications.ts index e85593a5d57d..f471aa663a3b 100644 --- a/frontend/src/ts/elements/notifications.ts +++ b/frontend/src/ts/elements/notifications.ts @@ -1,31 +1,18 @@ -import { debounce } from "throttle-debounce"; import * as Misc from "../utils/misc"; -import * as BannerEvent from "../observables/banner-event"; import * as NotificationEvent from "../observables/notification-event"; -import { convertRemToPixels } from "../utils/numbers"; import { animate } from "animejs"; import { qsr } from "../utils/dom"; import { CommonResponsesType } from "@monkeytype/contracts/util/api"; const notificationCenter = qsr("#notificationCenter"); const notificationCenterHistory = notificationCenter.qsr(".history"); -const bannerCenter = qsr("#bannerCenter"); -const app = qsr("#app"); const clearAllButton = notificationCenter.qsr(".clearAll"); -function updateMargin(): void { - const height = bannerCenter.native.offsetHeight; - app.setStyle({ paddingTop: height + convertRemToPixels(2) + "px" }); - notificationCenter.setStyle({ marginTop: height + "px" }); -} - let visibleStickyNotifications = 0; let id = 0; -type NotificationType = "notification" | "banner" | "psa"; class Notification { id: number; - type: NotificationType; message: string; level: number; important: boolean; @@ -34,7 +21,6 @@ class Notification { customIcon?: string; closeCallback: () => void; constructor( - type: NotificationType, message: string, level: number, important: boolean | undefined, @@ -46,23 +32,20 @@ class Notification { }, allowHTML?: boolean, ) { - this.type = type; this.message = allowHTML ? message : Misc.escapeHTML(message); this.level = level; this.important = important ?? false; - if (type === "banner" || type === "psa") { - this.duration = duration as number; - } else { - if (duration === undefined) { - if (level === -1) { - this.duration = 0; - } else { - this.duration = 3000; - } + + if (duration === undefined) { + if (level === -1) { + this.duration = 0; } else { - this.duration = duration * 1000; + this.duration = 3000; } + } else { + this.duration = duration * 1000; } + this.customTitle = customTitle; this.customIcon = customIcon; this.id = id++; @@ -94,112 +77,52 @@ class Notification { title = this.customTitle; } - if (this.type === "banner" || this.type === "psa") { - icon = ``; - } - if (this.customIcon !== undefined) { icon = ``; } - if (this.type === "notification") { - // moveCurrentToHistory(); - if (this.duration === 0) { - visibleStickyNotifications++; - updateClearAllButton(); - } + // moveCurrentToHistory(); + if (this.duration === 0) { + visibleStickyNotifications++; + updateClearAllButton(); + } - notificationCenterHistory.prependHtml(` + notificationCenterHistory.prependHtml(`
${icon}
${title}
${this.message}
`); - const notif = notificationCenter.qs(`.notif[id='${this.id}']`); - if (notif === null) return; + const notif = notificationCenter.qs(`.notif[id='${this.id}']`); + if (notif === null) return; - const notifHeight = notif.native.offsetHeight; - const duration = Misc.applyReducedMotion(250); + const notifHeight = notif.native.offsetHeight; + const duration = Misc.applyReducedMotion(250); - animate(notif.native, { - opacity: [0, 1], - duration: duration / 2, - delay: duration / 2, - }); - notif?.on("click", () => { - this.hide(); - this.closeCallback(); - if (this.duration === 0) { - visibleStickyNotifications--; - } - updateClearAllButton(); - }); - - animate(notificationCenterHistory.native, { - marginTop: { - from: "-=" + notifHeight, - to: 0, - }, - duration: duration / 2, - }); - notif?.on("hover", () => { - notif?.toggleClass("hover"); - }); - } else if (this.type === "banner" || this.type === "psa") { - let leftside = `
${icon}
`; - - let withImage = false; - if (/images\/.*/.test(this.customIcon as string)) { - withImage = true; - leftside = `
`; + animate(notif.native, { + opacity: [0, 1], + duration: duration / 2, + delay: duration / 2, + }); + notif?.on("click", () => { + this.hide(); + this.closeCallback(); + if (this.duration === 0) { + visibleStickyNotifications--; } + updateClearAllButton(); + }); + + animate(notificationCenterHistory.native, { + marginTop: { + from: "-=" + notifHeight, + to: 0, + }, + duration: duration / 2, + }); + notif?.on("hover", () => { + notif?.toggleClass("hover"); + }); - bannerCenter.prependHtml(` -
-
- ${leftside} -
- ${this.message} -
- ${ - this.duration >= 0 - ? ` -
- -
- ` - : `
${icon}
` - } -
-
- `); - updateMargin(); - BannerEvent.dispatch(); - if (this.duration >= 0) { - bannerCenter - .qsa( - `.banner[id='${this.id}'] .closeButton, .psa[id='${this.id}'] .closeButton`, - ) - .on("click", () => { - this.hide(); - this.closeCallback(); - }); - } - // NOTE: This need to be changed if the update banner text is changed - if (/please ()?refresh/i.test(this.message)) { - // add pointer when refresh is needed - bannerCenter - .qsa(`.banner[id='${this.id}'], .psa[id='${this.id}']`) - .addClass("clickable"); - // refresh on clicking banner - bannerCenter - .qsa(`.banner[id='${this.id}'], .psa[id='${this.id}']`) - .on("click", () => { - window.location.reload(); - }); - } - } if (this.duration > 0) { setTimeout(() => { this.hide(); @@ -207,39 +130,31 @@ class Notification { } } hide(): void { - if (this.type === "notification") { - const notif = notificationCenter.qs(`.notif[id='${this.id}']`); + const notif = notificationCenter.qs(`.notif[id='${this.id}']`); - if (notif === null) return; + if (notif === null) return; - const duration = Misc.applyReducedMotion(250); + const duration = Misc.applyReducedMotion(250); - animate(notif.native, { - opacity: { - to: 0, - duration: duration, - }, - height: { - to: 0, - duration: duration / 2, - delay: duration / 2, - }, - marginBottom: { - to: 0, - duration: duration / 2, - delay: duration / 2, - }, - onComplete: () => { - notif.remove(); - }, - }); - } else if (this.type === "banner" || this.type === "psa") { - bannerCenter - .qsa(`.banner[id='${this.id}'], .psa[id='${this.id}']`) - .remove(); - updateMargin(); - BannerEvent.dispatch(); - } + animate(notif.native, { + opacity: { + to: 0, + duration: duration, + }, + height: { + to: 0, + duration: duration / 2, + delay: duration / 2, + }, + marginBottom: { + to: 0, + duration: duration / 2, + delay: duration / 2, + }, + onComplete: () => { + notif.remove(); + }, + }); } } @@ -301,7 +216,6 @@ export function add( }); new Notification( - "notification", message, level, options.important, @@ -313,64 +227,12 @@ export function add( ).show(); } -export function addBanner( - message: string, - level = -1, - customIcon = "bullhorn", - sticky = false, - closeCallback?: () => void, - allowHTML?: boolean, -): number { - const banner = new Notification( - "banner", - message, - level, - false, - sticky ? -1 : 0, - undefined, - customIcon, - closeCallback, - allowHTML, - ); - banner.show(); - return banner.id; -} - -export function addPSA( - message: string, - level = -1, - customIcon = "bullhorn", - sticky = false, - closeCallback?: () => void, - allowHTML?: boolean, -): number { - const psa = new Notification( - "psa", - message, - level, - false, - sticky ? -1 : 0, - undefined, - customIcon, - closeCallback, - allowHTML, - ); - psa.show(); - return psa.id; -} - export function clearAllNotifications(): void { notificationCenter.qsa(".notif").remove(); visibleStickyNotifications = 0; updateClearAllButton(); } -const debouncedMarginUpdate = debounce(100, updateMargin); - -window.addEventListener("resize", () => { - debouncedMarginUpdate(); -}); - notificationCenter.qs(".clearAll")?.on("click", () => { notificationCenter.qsa(".notif").forEach((element) => { element.native.click(); diff --git a/frontend/src/ts/elements/psa.ts b/frontend/src/ts/elements/psa.ts index 673c4e3ff813..a985a46e108f 100644 --- a/frontend/src/ts/elements/psa.ts +++ b/frontend/src/ts/elements/psa.ts @@ -1,7 +1,6 @@ import Ape from "../ape"; import { isDevEnvironment } from "../utils/misc"; import { secondsToString } from "../utils/date-and-time"; -import * as Notifications from "./notifications"; import { format } from "date-fns/format"; import * as Alerts from "./alerts"; import { PSA } from "@monkeytype/schemas/psas"; @@ -11,6 +10,7 @@ import { IdSchema } from "@monkeytype/schemas/util"; import { tryCatch } from "@monkeytype/util/trycatch"; import { isSafeNumber } from "@monkeytype/util/numbers"; import * as AuthEvent from "../observables/auth-event"; +import { addBanner } from "../stores/banners"; const confirmedPSAs = new LocalStorageWithSchema({ key: "confirmedPSAs", @@ -37,12 +37,11 @@ async function getLatest(): Promise { if (response.status === 500) { if (isDevEnvironment()) { - Notifications.addPSA( - "Dev Info: Backend server not running", - 0, - "exclamation-triangle", - false, - ); + addBanner({ + level: 0, + text: "Dev Info: Backend server not running", + icon: "fas fa-exclamation-triangle", + }); } else { type InstatusSummary = { page: { @@ -93,35 +92,29 @@ async function getLatest(): Promise { maintenanceData[0] !== undefined && maintenanceData[0].status === "INPROGRESS" ) { - Notifications.addPSA( - `Server is currently offline for scheduled maintenance. Check the status page for more info.`, - -1, - "bullhorn", - true, - undefined, - true, - ); + addBanner({ + level: -1, + text: `Server is currently offline for scheduled maintenance. Check the status page for more info.`, + icon: "fas fa-bullhorn", + allowHtml: true, + }); } else { - Notifications.addPSA( - "Looks like the server is experiencing unexpected down time.
Check the status page for more information.", - -1, - "exclamation-triangle", - false, - undefined, - true, - ); + addBanner({ + level: -1, + text: "Looks like the server is experiencing unexpected down time.
Check the status page for more information.", + icon: "fas fa-exclamation-triangle", + allowHtml: true, + }); } } return null; } else if (response.status === 503) { - Notifications.addPSA( - "Server is currently under maintenance. Check the status page for more info.", - -1, - "bullhorn", - true, - undefined, - true, - ); + addBanner({ + level: -1, + text: "Server is currently under maintenance. Check the status page for more info.", + icon: "fas fa-bullhorn", + allowHtml: true, + }); return null; } else if (response.status !== 200) { return null; @@ -166,16 +159,15 @@ export async function show(): Promise { return; } - Notifications.addPSA( - psa.message, - psa.level, - "bullhorn", - psa.sticky, - () => { + addBanner({ + level: (psa.level ?? 0) as -1 | 0 | 1, + text: psa.message, + icon: "fas fa-bullhorn", + important: psa.sticky ?? false, + onClose: () => { setMemory(psa._id); }, - true, - ); + }); }); } diff --git a/frontend/src/ts/hooks/useTailwindBreakpoints.ts b/frontend/src/ts/hooks/useTailwindBreakpoints.ts new file mode 100644 index 000000000000..c155b2fbcc16 --- /dev/null +++ b/frontend/src/ts/hooks/useTailwindBreakpoints.ts @@ -0,0 +1,59 @@ +import { Accessor, createSignal, onCleanup, onMount } from "solid-js"; +import { debounce } from "throttle-debounce"; + +type Breakpoints = { + xxs: boolean; + xs: boolean; + sm: boolean; + md: boolean; + lg: boolean; + xl: boolean; + "2xl": boolean; +}; + +export function useTailwindBreakpoints( + debounceMs = 125, +): Accessor { + const [breakpoints, setBreakpoints] = createSignal( + undefined, + ); + + const updateBreakpoints = (): void => { + const styles = getComputedStyle(document.documentElement); + + const breakpoints = { + xxs: parseInt(styles.getPropertyValue("--breakpoint-xxs")), + xs: parseInt(styles.getPropertyValue("--breakpoint-xs")), + sm: parseInt(styles.getPropertyValue("--breakpoint-sm")), + md: parseInt(styles.getPropertyValue("--breakpoint-md")), + lg: parseInt(styles.getPropertyValue("--breakpoint-lg")), + xl: parseInt(styles.getPropertyValue("--breakpoint-xl")), + "2xl": parseInt(styles.getPropertyValue("--breakpoint-2xl")), + }; + + const currentWidth = window.innerWidth; + + setBreakpoints({ + xxs: true, + xs: currentWidth >= breakpoints.xs, + sm: currentWidth >= breakpoints.sm, + md: currentWidth >= breakpoints.md, + lg: currentWidth >= breakpoints.lg, + xl: currentWidth >= breakpoints.xl, + "2xl": currentWidth >= breakpoints["2xl"], + }); + }; + + const debouncedUpdate = debounce(debounceMs, updateBreakpoints); + + onMount(() => { + updateBreakpoints(); + window.addEventListener("resize", debouncedUpdate); + }); + + onCleanup(() => { + window.removeEventListener("resize", debouncedUpdate); + }); + + return breakpoints; +} diff --git a/frontend/src/ts/observables/banner-event.ts b/frontend/src/ts/observables/banner-event.ts deleted file mode 100644 index 6d6b72ccdca9..000000000000 --- a/frontend/src/ts/observables/banner-event.ts +++ /dev/null @@ -1,18 +0,0 @@ -type SubscribeFunction = () => void; - -const subscribers: SubscribeFunction[] = []; - -export function subscribe(fn: SubscribeFunction): void { - subscribers.push(fn); -} - -export function dispatch(): void { - subscribers.forEach((fn) => { - try { - fn(); - } catch (e) { - console.error("Banner event subscriber threw an error"); - console.error(e); - } - }); -} diff --git a/frontend/src/ts/states/connection.ts b/frontend/src/ts/states/connection.ts index 9ceb511887d0..180b703a7735 100644 --- a/frontend/src/ts/states/connection.ts +++ b/frontend/src/ts/states/connection.ts @@ -2,7 +2,8 @@ import { debounce } from "throttle-debounce"; import * as Notifications from "../elements/notifications"; import * as ConnectionEvent from "../observables/connection-event"; import * as TestState from "../test/test-state"; -import { qs, onDOMReady } from "../utils/dom"; +import { onDOMReady } from "../utils/dom"; +import { addBanner, removeBanner } from "../stores/banners"; let state = navigator.onLine; @@ -16,16 +17,15 @@ let bannerAlreadyClosed = false; export function showOfflineBanner(): void { if (bannerAlreadyClosed) return; - noInternetBannerId ??= Notifications.addPSA( - "No internet connection", - 0, - "exclamation-triangle", - false, - () => { + noInternetBannerId ??= addBanner({ + level: 0, + text: "No internet connection", + icon: "fas fa-exclamation-triangle", + onClose: () => { bannerAlreadyClosed = true; noInternetBannerId = undefined; }, - ); + }); } const throttledHandleState = debounce(5000, () => { @@ -34,9 +34,7 @@ const throttledHandleState = debounce(5000, () => { Notifications.add("You're back online", 1, { customTitle: "Connection", }); - qs( - `#bannerCenter .psa.notice[id="${noInternetBannerId}"] .closeButton`, - )?.dispatch("click"); + removeBanner(noInternetBannerId); } bannerAlreadyClosed = false; } else if (!TestState.isActive) { diff --git a/frontend/src/ts/stores/banners.ts b/frontend/src/ts/stores/banners.ts new file mode 100644 index 000000000000..b6e328dc5946 --- /dev/null +++ b/frontend/src/ts/stores/banners.ts @@ -0,0 +1,35 @@ +import { createStore } from "solid-js/store"; + +export type Banner = { + id: number; + level: -1 | 0 | 1; + icon?: string; + imagePath?: string; + text: string; + important?: boolean; + allowHtml?: boolean; + onClose?: () => void; +}; + +let id = 0; +const [banners, setBanners] = createStore([]); + +export function addBanner(banner: Omit): number { + const newid = id++; + setBanners((prev) => [...prev, { ...banner, id: newid }]); + return newid; +} + +export function removeBanner(bannerId: number): void { + const banner = getBanner(bannerId); + banner?.onClose?.(); + setBanners((prev) => prev.filter((banner) => banner.id !== bannerId)); +} + +export function getBanner(bannerId: number): Banner | undefined { + return banners.find((banner) => banner.id === bannerId); +} + +export function getBanners(): Banner[] { + return banners; +} From fe1fdc072c26e999cdb126c1ee92be5434fa1734 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 19 Jan 2026 12:55:35 +0100 Subject: [PATCH 02/24] Update frontend/src/ts/components/layout/overlays/Banners.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontend/src/ts/components/layout/overlays/Banners.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index 5f0aa073fb24..64964b5b3228 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -76,8 +76,12 @@ export function Banners(): JSXElement { const updateMargin = (): void => { const height = element()?.getOffsetHeight() ?? 0; + const offset = height + convertRemToPixels(2); qsr("#app").setStyle({ - paddingTop: height + convertRemToPixels(2) + "px", + paddingTop: offset + "px", + }); + qsr("#notificationCenter").setStyle({ + marginTop: offset + "px", }); }; From b4c786703c27c235a074c21ea284ed28e645c9bb Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 19 Jan 2026 12:55:49 +0100 Subject: [PATCH 03/24] Update frontend/src/ts/elements/merch-banner.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontend/src/ts/elements/merch-banner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/elements/merch-banner.ts b/frontend/src/ts/elements/merch-banner.ts index 3a6357263b3a..64d9735a117b 100644 --- a/frontend/src/ts/elements/merch-banner.ts +++ b/frontend/src/ts/elements/merch-banner.ts @@ -12,7 +12,7 @@ export function showIfNotClosedBefore(): void { if (!closed.get()) { addBanner({ level: 1, - icon: "fa fa-fw fa-shopping-bag", + icon: "fas fa-fw fa-shopping-bag", allowHtml: true, text: `New merch store now open, including a limited edition metal keycap! monkeytype.store`, imagePath: "./images/merch3.png", From 1d3f94f1154af149b18f5a684d23fad7c36f2c57 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 19 Jan 2026 12:56:35 +0100 Subject: [PATCH 04/24] Update frontend/src/ts/states/connection.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontend/src/ts/states/connection.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/ts/states/connection.ts b/frontend/src/ts/states/connection.ts index 180b703a7735..10347cede235 100644 --- a/frontend/src/ts/states/connection.ts +++ b/frontend/src/ts/states/connection.ts @@ -35,6 +35,7 @@ const throttledHandleState = debounce(5000, () => { customTitle: "Connection", }); removeBanner(noInternetBannerId); + noInternetBannerId = undefined; } bannerAlreadyClosed = false; } else if (!TestState.isActive) { From 3e9e723448d6aae67b3b0951ab7b43b68f5cf2a8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 12:59:54 +0100 Subject: [PATCH 05/24] important hide close --- .../ts/components/layout/overlays/Banners.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index 64964b5b3228..efccfcb49ff7 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -57,15 +57,21 @@ function Banner(props: BannerType): JSXElement { then={
} else={
{props.text}
} /> - + } + else={ + + } + />
); From a0b9ab1d7a3acecf30990e2056394e93f835fb97 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:02:12 +0100 Subject: [PATCH 06/24] firebase --- frontend/src/ts/firebase.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/ts/firebase.ts b/frontend/src/ts/firebase.ts index dd2a6354201b..c906071f1626 100644 --- a/frontend/src/ts/firebase.ts +++ b/frontend/src/ts/firebase.ts @@ -22,7 +22,6 @@ import { indexedDBLocalPersistence, getAdditionalUserInfo, } from "firebase/auth"; -import * as Notifications from "./elements/notifications"; import { createErrorMessage, isDevEnvironment, @@ -35,6 +34,7 @@ import { } from "firebase/analytics"; import { tryCatch } from "@monkeytype/util/trycatch"; import { dispatch as dispatchSignUpEvent } from "./observables/google-sign-up-event"; +import { addBanner } from "./stores/banners"; let app: FirebaseApp | undefined; let Auth: AuthType | undefined; @@ -84,12 +84,10 @@ export async function init(callback: ReadyCallback): Promise { console.error("Firebase failed to initialize", e); await callback(false, null); if (isDevEnvironment()) { - Notifications.addPSA( - createErrorMessage(e, "Firebase uninitialized"), - 0, - undefined, - false, - ); + addBanner({ + level: 0, + text: "Dev Info: Firebase failed to initialize", + }); } } finally { resolveAuthPromise(); From bcc17f5a79364e51a0cd444f563f36f1e1aa9aa8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:03:14 +0100 Subject: [PATCH 07/24] icon --- frontend/src/ts/firebase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/ts/firebase.ts b/frontend/src/ts/firebase.ts index c906071f1626..42dcb220ae63 100644 --- a/frontend/src/ts/firebase.ts +++ b/frontend/src/ts/firebase.ts @@ -87,6 +87,7 @@ export async function init(callback: ReadyCallback): Promise { addBanner({ level: 0, text: "Dev Info: Firebase failed to initialize", + icon: "fas fa-exclamation-triangle", }); } } finally { From 7c757fe8cf0da60b60c2230f661e5a4cc37516c7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:07:13 +0100 Subject: [PATCH 08/24] alignment --- frontend/src/ts/components/layout/overlays/Banners.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index efccfcb49ff7..319690e550f2 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -63,7 +63,7 @@ function Banner(props: BannerType): JSXElement { else={ {" "} + to change it and learn more about why. + + ), + important: true, }); } diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index c447ab338fde..bd8efcdede70 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -32,7 +32,7 @@ function Banner(props: BannerType): JSXElement { return (
} />
} + if={props.customContent !== undefined} + then={
{props.customContent}
} else={
{props.text}
} /> monkeytype.store`, + customContent: ( + <> + New merch store now open, including a limited edition metal keycap!{" "} + + monkeytype.store + + + ), imagePath: "./images/merch3.png", onClose: () => { closed.set(true); diff --git a/frontend/src/ts/elements/psa.ts b/frontend/src/ts/elements/psa.tsx similarity index 70% rename from frontend/src/ts/elements/psa.ts rename to frontend/src/ts/elements/psa.tsx index a985a46e108f..b847d68c5fc8 100644 --- a/frontend/src/ts/elements/psa.ts +++ b/frontend/src/ts/elements/psa.tsx @@ -12,6 +12,50 @@ import { isSafeNumber } from "@monkeytype/util/numbers"; import * as AuthEvent from "../observables/auth-event"; import { addBanner } from "../stores/banners"; +addBanner({ + level: -1, + customContent: ( + <> + Server is currently offline for scheduled maintenance.{" "} + + Check the status page + {" "} + for more info. + + ), + icon: "fas fa-bullhorn", +}); + +addBanner({ + level: -1, + icon: "fas fa-exclamation-triangle", + customContent: ( + <> + Looks like the server is experiencing unexpected down time. +
+ Check the{" "} + + status page + {" "} + for more information. + + ), +}); + +addBanner({ + level: -1, + icon: "fas fa-bullhorn", + customContent: ( + <> + Server is currently under maintenance.{" "} + + Check the status page + {" "} + for more info. + + ), +}); + const confirmedPSAs = new LocalStorageWithSchema({ key: "confirmedPSAs", schema: z.array(IdSchema), @@ -94,16 +138,32 @@ async function getLatest(): Promise { ) { addBanner({ level: -1, - text: `Server is currently offline for scheduled maintenance. Check the status page for more info.`, + customContent: ( + <> + Server is currently offline for scheduled maintenance.{" "} + + Check the status page + {" "} + for more info. + + ), icon: "fas fa-bullhorn", - allowHtml: true, }); } else { addBanner({ level: -1, - text: "Looks like the server is experiencing unexpected down time.
Check the status page for more information.", icon: "fas fa-exclamation-triangle", - allowHtml: true, + customContent: ( + <> + Looks like the server is experiencing unexpected down time. +
+ Check the{" "} + + status page + {" "} + for more information. + + ), }); } } @@ -111,9 +171,16 @@ async function getLatest(): Promise { } else if (response.status === 503) { addBanner({ level: -1, - text: "Server is currently under maintenance. Check the status page for more info.", icon: "fas fa-bullhorn", - allowHtml: true, + customContent: ( + <> + Server is currently under maintenance.{" "} + + Check the status page + {" "} + for more info. + + ), }); return null; } else if (response.status !== 200) { diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index a0b2baca2d54..8c9f5e6bc616 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -46,7 +46,7 @@ import { goToPage } from "../pages/leaderboards"; import FileStorage from "../utils/file-storage"; import { z } from "zod"; import { remoteValidation } from "../utils/remote-validation"; -import { qs, qsr } from "../utils/dom"; +import { qsr } from "../utils/dom"; import { list, PopupKey, showPopup } from "./simple-modals-base"; export { list, showPopup }; @@ -1275,7 +1275,3 @@ list.lbGoToPage = new SimpleModal({ }; }, }); - -qs("#bannerCenter")?.onChild("click", ".banner .text .openNameChange", () => { - showPopup("updateName"); -}); diff --git a/frontend/src/ts/stores/banners.ts b/frontend/src/ts/stores/banners.ts index b6e328dc5946..d80a487336bf 100644 --- a/frontend/src/ts/stores/banners.ts +++ b/frontend/src/ts/stores/banners.ts @@ -1,3 +1,4 @@ +import { JSXElement } from "solid-js"; import { createStore } from "solid-js/store"; export type Banner = { @@ -5,18 +6,25 @@ export type Banner = { level: -1 | 0 | 1; icon?: string; imagePath?: string; - text: string; important?: boolean; - allowHtml?: boolean; onClose?: () => void; -}; +} & ( + | { + text: string; + customContent?: undefined; + } + | { + customContent: JSXElement; + text?: undefined; + } +); let id = 0; const [banners, setBanners] = createStore([]); export function addBanner(banner: Omit): number { const newid = id++; - setBanners((prev) => [...prev, { ...banner, id: newid }]); + setBanners((prev) => [...prev, { ...banner, id: newid } as Banner]); return newid; } From 205cdb5cad04d8f197d487a1c49c96482b34dbb3 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:36:51 +0100 Subject: [PATCH 12/24] remove test lines --- frontend/src/ts/elements/psa.tsx | 44 -------------------------------- 1 file changed, 44 deletions(-) diff --git a/frontend/src/ts/elements/psa.tsx b/frontend/src/ts/elements/psa.tsx index b847d68c5fc8..7c261537bee8 100644 --- a/frontend/src/ts/elements/psa.tsx +++ b/frontend/src/ts/elements/psa.tsx @@ -12,50 +12,6 @@ import { isSafeNumber } from "@monkeytype/util/numbers"; import * as AuthEvent from "../observables/auth-event"; import { addBanner } from "../stores/banners"; -addBanner({ - level: -1, - customContent: ( - <> - Server is currently offline for scheduled maintenance.{" "} - - Check the status page - {" "} - for more info. - - ), - icon: "fas fa-bullhorn", -}); - -addBanner({ - level: -1, - icon: "fas fa-exclamation-triangle", - customContent: ( - <> - Looks like the server is experiencing unexpected down time. -
- Check the{" "} - - status page - {" "} - for more information. - - ), -}); - -addBanner({ - level: -1, - icon: "fas fa-bullhorn", - customContent: ( - <> - Server is currently under maintenance.{" "} - - Check the status page - {" "} - for more info. - - ), -}); - const confirmedPSAs = new LocalStorageWithSchema({ key: "confirmedPSAs", schema: z.array(IdSchema), From 7eb57ec0611417882d17b84af6d78d5ba7c2fdac Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:44:48 +0100 Subject: [PATCH 13/24] move ad margin to banners --- .../ts/components/layout/overlays/Banners.tsx | 2 ++ frontend/src/ts/controllers/ad-controller.ts | 22 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index bd8efcdede70..da83b6fd3454 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -92,6 +92,8 @@ export function Banners(): JSXElement { qsr("#notificationCenter").setStyle({ marginTop: offset + "px", }); + qsr("#ad-vertical-left-wrapper").setStyle({ marginTop: offset + "px" }); + qsr("#ad-vertical-right-wrapper").setStyle({ marginTop: offset + "px" }); }; const debouncedMarginUpdate = debounce(100, updateMargin); diff --git a/frontend/src/ts/controllers/ad-controller.ts b/frontend/src/ts/controllers/ad-controller.ts index 6058543a15aa..1ce128f80ba1 100644 --- a/frontend/src/ts/controllers/ad-controller.ts +++ b/frontend/src/ts/controllers/ad-controller.ts @@ -7,8 +7,8 @@ import * as TestState from "../test/test-state"; import * as EG from "./eg-ad-controller"; import * as PW from "./pw-ad-controller"; import { onDOMReady, qs } from "../utils/dom"; -import { createEffect, on } from "solid-js"; -import { getBanners } from "../stores/banners"; +// import { createEffect, on } from "solid-js"; +// import { getBanners } from "../stores/banners"; const breakpoint = 900; let widerThanBreakpoint = true; @@ -87,12 +87,12 @@ function removeResult(): void { qs("#ad-result-small-wrapper")?.remove(); } -function updateVerticalMargin(): void { - // const height = $("#bannerCenter").height() as number; - // const margin = height + Numbers.convertRemToPixels(2) + "px"; - // $("#ad-vertical-left-wrapper").css("margin-top", margin); - // $("#ad-vertical-right-wrapper").css("margin-top", margin); -} +// function updateVerticalMargin(): void { +// const height = $("#bannerCenter").height() as number; +// const margin = height + Numbers.convertRemToPixels(2) + "px"; +// $("#ad-vertical-left-wrapper").css("margin-top", margin); +// $("#ad-vertical-right-wrapper").css("margin-top", margin); +// } function updateBreakpoint(noReinstate = false): void { const beforeUpdate = widerThanBreakpoint; @@ -287,12 +287,12 @@ export function destroyResult(): void { // $("#ad-result-small-wrapper").empty(); } -const debouncedMarginUpdate = debounce(500, updateVerticalMargin); +// const debouncedMarginUpdate = debounce(500, updateVerticalMargin); const debouncedBreakpointUpdate = debounce(500, updateBreakpoint); const debouncedBreakpoint2Update = debounce(500, updateBreakpoint2); window.addEventListener("resize", () => { - debouncedMarginUpdate(); + // debouncedMarginUpdate(); debouncedBreakpointUpdate(); debouncedBreakpoint2Update(); }); @@ -310,7 +310,7 @@ ConfigEvent.subscribe(({ key, newValue }) => { } }); -createEffect(on(() => getBanners().length, updateVerticalMargin)); +// createEffect(on(() => getBanners().length, updateVerticalMargin)); onDOMReady(() => { updateBreakpoint(true); From 640701f21a035ef7190d6fdaae9d3e66686e8aec Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:45:40 +0100 Subject: [PATCH 14/24] remove comment --- frontend/src/ts/auth.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/ts/auth.tsx b/frontend/src/ts/auth.tsx index 0b1157abc8e4..6153b7d2b9bf 100644 --- a/frontend/src/ts/auth.tsx +++ b/frontend/src/ts/auth.tsx @@ -73,7 +73,6 @@ async function getDataAndInit(): Promise { if (snapshot.needsToChangeName) { addBanner({ level: -1, - // text: "You need to update your account name. Click here to change it and learn more about why.", icon: "fas fa-exclamation-triangle", customContent: ( <> From b61b0c4666d0e8d27bdcee487f11e8b215db62aa Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:45:57 +0100 Subject: [PATCH 15/24] unused styles --- frontend/src/styles/banners.scss | 75 -------------------- frontend/src/styles/media-queries-blue.scss | 11 --- frontend/src/styles/media-queries-green.scss | 8 --- 3 files changed, 94 deletions(-) delete mode 100644 frontend/src/styles/banners.scss diff --git a/frontend/src/styles/banners.scss b/frontend/src/styles/banners.scss deleted file mode 100644 index fc169cc8ea6a..000000000000 --- a/frontend/src/styles/banners.scss +++ /dev/null @@ -1,75 +0,0 @@ -#bannerCenter { - position: fixed; - width: 100%; - z-index: 1000; - transition: opacity 0.25s; - .banner, - .psa { - background: var(--sub-color); - color: var(--bg-color); - justify-content: center; - - &.clickable { - cursor: pointer; - } - - &.withImage { - .lefticon { - display: none; - } - } - - .container { - padding-right: 0.25em; - display: grid; - grid-template-columns: auto 1fr auto; - gap: 1em; - align-items: center; - width: 100%; - justify-items: center; - .image { - height: 2.25em; - background-size: cover; - aspect-ratio: 6/1; - background-position: center; - background-repeat: no-repeat; - } - .lefticon, - .image { - grid-column: 1; - grid-row: 1; - } - .text { - margin-top: 0.5em; - margin-bottom: 0.5em; - } - .closeButton { - padding: 0.25em; - transition: 0.125s; - &:hover { - cursor: pointer; - color: var(--text-color); - } - } - } - &.good { - background: var(--main-color); - } - &.bad { - background: var(--error-color); - } - a { - color: var(--bg-color); - text-decoration: underline; - &:hover { - color: var(--text-color); - cursor: pointer; - } - } - } - - &.focus { - opacity: 0; - pointer-events: none; - } -} diff --git a/frontend/src/styles/media-queries-blue.scss b/frontend/src/styles/media-queries-blue.scss index 8723a0c5360b..afa79f435be9 100644 --- a/frontend/src/styles/media-queries-blue.scss +++ b/frontend/src/styles/media-queries-blue.scss @@ -24,17 +24,6 @@ } } } - #bannerCenter { - font-size: 0.85rem; - .banner.withImage { - .image { - display: none; - } - .lefticon { - display: block; - } - } - } header { nav { .textButton.view-account { diff --git a/frontend/src/styles/media-queries-green.scss b/frontend/src/styles/media-queries-green.scss index fce0689a95a7..eff2856b47bc 100644 --- a/frontend/src/styles/media-queries-green.scss +++ b/frontend/src/styles/media-queries-green.scss @@ -14,14 +14,6 @@ font-size: 0.7rem; --horizontalPadding: 0.6em; } - #bannerCenter { - font-size: 0.85rem; - .banner .container { - .closeButton { - padding: 0.4em; - } - } - } header { #logo { .text { From 0be91492c0e4fd95c01b4ef3dcff28b5e06649cb Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:46:04 +0100 Subject: [PATCH 16/24] unused style --- frontend/src/styles/index.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss index 25ab021f1938..c23d0df94ce1 100644 --- a/frontend/src/styles/index.scss +++ b/frontend/src/styles/index.scss @@ -24,9 +24,9 @@ } @layer custom-styles { - @import "buttons", "fonts", "404", "ads", "account", "animations", "banners", - "caret", "commandline", "core", "inputs", "keymap", "login", "monkey", - "nav", "notifications", "popups", "profile", "scroll", "settings", + @import "buttons", "fonts", "404", "ads", "account", "animations", "caret", + "commandline", "core", "inputs", "keymap", "login", "monkey", "nav", + "notifications", "popups", "profile", "scroll", "settings", "account-settings", "leaderboards", "test", "loading", "friends", "media-queries"; From 9042b6fb84311de4b3db9e4a93cd6d9bb6dd8147 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:46:35 +0100 Subject: [PATCH 17/24] not qsr --- frontend/src/ts/components/layout/overlays/Banners.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index da83b6fd3454..cfe7c0d8d104 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -13,7 +13,7 @@ import { } from "../../../stores/banners"; import { cn } from "../../../utils/cn"; import { useRefWithUtils } from "../../../hooks/useRefWithUtils"; -import { qsr } from "../../../utils/dom"; +import { qs, qsr } from "../../../utils/dom"; import { convertRemToPixels } from "../../../utils/numbers"; import { debounce } from "throttle-debounce"; import { Conditional } from "../../common/Conditional"; @@ -92,8 +92,8 @@ export function Banners(): JSXElement { qsr("#notificationCenter").setStyle({ marginTop: offset + "px", }); - qsr("#ad-vertical-left-wrapper").setStyle({ marginTop: offset + "px" }); - qsr("#ad-vertical-right-wrapper").setStyle({ marginTop: offset + "px" }); + qs("#ad-vertical-left-wrapper")?.setStyle({ marginTop: offset + "px" }); + qs("#ad-vertical-right-wrapper")?.setStyle({ marginTop: offset + "px" }); }; const debouncedMarginUpdate = debounce(100, updateMargin); From 27344ed269714ea4f034bec7beed38c6dabfaaa8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 19 Jan 2026 13:54:27 +0100 Subject: [PATCH 18/24] fix --- frontend/src/ts/components/layout/overlays/Banners.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx index cfe7c0d8d104..dc5c368b52a9 100644 --- a/frontend/src/ts/components/layout/overlays/Banners.tsx +++ b/frontend/src/ts/components/layout/overlays/Banners.tsx @@ -24,7 +24,7 @@ function Banner(props: BannerType): JSXElement { removeBanner(props.id); // }); }; - const icon = + const icon = (): string => props.icon === undefined || props.icon === "" ? "fa fa-fw fa-bullhorn" : props.icon; @@ -50,10 +50,10 @@ function Banner(props: BannerType): JSXElement { alt="Banner Image" class="hidden aspect-6/1 h-full max-h-9 self-center xl:block" /> - + } - else={} + else={} /> } + then={} else={