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; + setGlobalOffsetTop(height); + }; + + 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 a9bded4bf1ba..f7626e5fe8f3 100644 --- a/frontend/src/ts/components/layout/overlays/Overlays.tsx +++ b/frontend/src/ts/components/layout/overlays/Overlays.tsx @@ -2,12 +2,14 @@ import { JSXElement } from "solid-js"; import { TailwindMediaQueryDebugger } from "../../utils/TailwindMediaQueryDebugger"; +import { Banners } from "./Banners"; import { FpsCounter } from "./FpsCounter"; import { LoaderBar } from "./LoaderBar"; 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..0c56463804eb 100644 --- a/frontend/src/ts/controllers/ad-controller.ts +++ b/frontend/src/ts/controllers/ad-controller.ts @@ -1,13 +1,12 @@ /* oxlint-disable no-unsafe-member-access */ 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 } from "solid-js"; const breakpoint = 900; let widerThanBreakpoint = true; @@ -86,13 +85,6 @@ 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 updateBreakpoint(noReinstate = false): void { const beforeUpdate = widerThanBreakpoint; @@ -286,12 +278,10 @@ export function destroyResult(): void { // $("#ad-result-small-wrapper").empty(); } -const debouncedMarginUpdate = debounce(500, updateVerticalMargin); const debouncedBreakpointUpdate = debounce(500, updateBreakpoint); const debouncedBreakpoint2Update = debounce(500, updateBreakpoint2); window.addEventListener("resize", () => { - debouncedMarginUpdate(); debouncedBreakpointUpdate(); debouncedBreakpoint2Update(); }); @@ -309,9 +299,14 @@ ConfigEvent.subscribe(({ key, newValue }) => { } }); -BannerEvent.subscribe(() => { - updateVerticalMargin(); -}); +// createEffect(() => { +// qs("#ad-vertical-left-wrapper")?.setStyle({ +// marginTop: getGlobalOffsetTop() + "px", +// }); +// qs("#ad-vertical-right-wrapper")?.setStyle({ +// marginTop: getGlobalOffsetTop() + "px", +// }); +// }); onDOMReady(() => { updateBreakpoint(true); diff --git a/frontend/src/ts/elements/merch-banner.ts b/frontend/src/ts/elements/merch-banner.ts deleted file mode 100644 index 9cf21f622b70..000000000000 --- a/frontend/src/ts/elements/merch-banner.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from "zod"; -import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; -import * as Notifications from "./notifications"; - -const closed = new LocalStorageWithSchema({ - key: "merchBannerClosed3", - schema: z.boolean(), - fallback: false, -}); - -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, - () => { - closed.set(true); - }, - true, - ); - } -} diff --git a/frontend/src/ts/elements/merch-banner.tsx b/frontend/src/ts/elements/merch-banner.tsx new file mode 100644 index 000000000000..8f9279b23f84 --- /dev/null +++ b/frontend/src/ts/elements/merch-banner.tsx @@ -0,0 +1,31 @@ +import { z } from "zod"; + +import { addBanner } from "../stores/banners"; +import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; + +const closed = new LocalStorageWithSchema({ + key: "merchBannerClosed3", + schema: z.boolean(), + fallback: false, +}); + +export function showIfNotClosedBefore(): void { + if (!closed.get()) { + addBanner({ + level: "success", + icon: "fas fa-fw fa-shopping-bag", + 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/notifications.ts b/frontend/src/ts/elements/notifications.ts index e85593a5d57d..31e14ec8f06c 100644 --- a/frontend/src/ts/elements/notifications.ts +++ b/frontend/src/ts/elements/notifications.ts @@ -1,31 +1,20 @@ -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"; +import { createEffect } from "solid-js"; +import { getGlobalOffsetTop } from "../signals/core"; 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 +23,6 @@ class Notification { customIcon?: string; closeCallback: () => void; constructor( - type: NotificationType, message: string, level: number, important: boolean | undefined, @@ -46,23 +34,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 +79,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 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(); - }); + const notif = notificationCenter.qs(`.notif[id='${this.id}']`); + if (notif === null) return; - 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}
`; + const notifHeight = notif.native.offsetHeight; + const duration = Misc.applyReducedMotion(250); - 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 +132,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 +218,6 @@ export function add( }); new Notification( - "notification", message, level, options.important, @@ -313,64 +229,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(); @@ -378,3 +242,9 @@ notificationCenter.qs(".clearAll")?.on("click", () => { visibleStickyNotifications = 0; updateClearAllButton(); }); + +createEffect(() => { + notificationCenter.setStyle({ + marginTop: getGlobalOffsetTop() + "px", + }); +}); diff --git a/frontend/src/ts/elements/psa.ts b/frontend/src/ts/elements/psa.tsx similarity index 68% rename from frontend/src/ts/elements/psa.ts rename to frontend/src/ts/elements/psa.tsx index 673c4e3ff813..46b5bab1c548 100644 --- a/frontend/src/ts/elements/psa.ts +++ b/frontend/src/ts/elements/psa.tsx @@ -1,16 +1,18 @@ -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"; -import { z } from "zod"; -import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; import { IdSchema } from "@monkeytype/schemas/util"; -import { tryCatch } from "@monkeytype/util/trycatch"; import { isSafeNumber } from "@monkeytype/util/numbers"; +import { tryCatch } from "@monkeytype/util/trycatch"; +import { format } from "date-fns/format"; +import { z } from "zod"; + +import Ape from "../ape"; import * as AuthEvent from "../observables/auth-event"; +import { addBanner } from "../stores/banners"; +import { secondsToString } from "../utils/date-and-time"; +import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; +import { isDevEnvironment } from "../utils/misc"; + +import * as Alerts from "./alerts"; const confirmedPSAs = new LocalStorageWithSchema({ key: "confirmedPSAs", @@ -37,12 +39,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: "notice", + text: "Dev Info: Backend server not running", + icon: "fas fa-exclamation-triangle", + }); } else { type InstatusSummary = { page: { @@ -93,35 +94,52 @@ 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: "error", + customContent: ( + <> + Server is currently offline for scheduled maintenance.{" "} + + Check the status page + {" "} + for more info. + + ), + icon: "fas fa-bullhorn", + }); } 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: "error", + icon: "fas fa-exclamation-triangle", + customContent: ( + <> + Looks like the server is experiencing unexpected down time. +
+ Check the{" "} + + status page + {" "} + for more information. + + ), + }); } } 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: "error", + icon: "fas fa-bullhorn", + customContent: ( + <> + Server is currently under maintenance.{" "} + + Check the status page + {" "} + for more info. + + ), + }); return null; } else if (response.status !== 200) { return null; @@ -166,16 +184,24 @@ export async function show(): Promise { return; } - Notifications.addPSA( - psa.message, - psa.level, - "bullhorn", - psa.sticky, - () => { + let level: "error" | "notice" | "success"; + if (psa.level === -1) { + level = "error"; + } else if (psa.level === 1) { + level = "success"; + } else { + level = "notice"; + } + + addBanner({ + level, + text: psa.message, + icon: "fas fa-bullhorn", + important: psa.sticky ?? false, + onClose: () => { setMemory(psa._id); }, - true, - ); + }); }); } diff --git a/frontend/src/ts/firebase.ts b/frontend/src/ts/firebase.ts index dd2a6354201b..85b562673397 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,11 @@ 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: "notice", + text: "Dev Info: Firebase failed to initialize", + icon: "fas fa-exclamation-triangle", + }); } } finally { resolveAuthPromise(); 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/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/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/signals/core.ts b/frontend/src/ts/signals/core.ts index 32d0d350a3b8..ff04891be873 100644 --- a/frontend/src/ts/signals/core.ts +++ b/frontend/src/ts/signals/core.ts @@ -23,3 +23,4 @@ export const [getCommandlineSubgroup, setCommandlineSubgroup] = createSignal< >(null); export const [getFocus, setFocus] = createSignal(false); +export const [getGlobalOffsetTop, setGlobalOffsetTop] = createSignal(0); diff --git a/frontend/src/ts/states/connection.ts b/frontend/src/ts/states/connection.ts index 9ceb511887d0..d5318df92700 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: "notice", + text: "No internet connection", + icon: "fas fa-exclamation-triangle", + onClose: () => { bannerAlreadyClosed = true; noInternetBannerId = undefined; }, - ); + }); } const throttledHandleState = debounce(5000, () => { @@ -34,9 +34,8 @@ const throttledHandleState = debounce(5000, () => { Notifications.add("You're back online", 1, { customTitle: "Connection", }); - qs( - `#bannerCenter .psa.notice[id="${noInternetBannerId}"] .closeButton`, - )?.dispatch("click"); + removeBanner(noInternetBannerId); + noInternetBannerId = undefined; } 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..04739348c6d7 --- /dev/null +++ b/frontend/src/ts/stores/banners.ts @@ -0,0 +1,43 @@ +import { JSXElement } from "solid-js"; +import { createStore } from "solid-js/store"; + +export type Banner = { + id: number; + level: "error" | "notice" | "success"; + icon?: string; + imagePath?: string; + important?: 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 } as Banner]); + 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; +} diff --git a/frontend/src/ts/ui.ts b/frontend/src/ts/ui.ts index 36de047bb40d..570e0c72ec55 100644 --- a/frontend/src/ts/ui.ts +++ b/frontend/src/ts/ui.ts @@ -5,13 +5,15 @@ import * as TestState from "./test/test-state"; import * as ConfigEvent from "./observables/config-event"; import { debounce, throttle } from "throttle-debounce"; import * as TestUI from "./test/test-ui"; -import { getActivePage } from "./signals/core"; +import { getActivePage, getGlobalOffsetTop } from "./signals/core"; import { isDevEnvironment } from "./utils/misc"; import { isCustomTextLong } from "./states/custom-text-name"; import { canQuickRestart } from "./utils/quick-restart"; import { FontName } from "@monkeytype/schemas/fonts"; import { applyFontFamily } from "./controllers/theme-controller"; -import { qs } from "./utils/dom"; +import { qs, qsr } from "./utils/dom"; +import { createEffect } from "solid-js"; +import { convertRemToPixels } from "./utils/numbers"; let isPreviewingFont = false; export function previewFontFamily(font: FontName): void { @@ -115,6 +117,12 @@ window.addEventListener("resize", () => { debouncedEvent(); }); +createEffect(() => { + qsr("#app").setStyle({ + paddingTop: getGlobalOffsetTop() + convertRemToPixels(2) + "px", + }); +}); + ConfigEvent.subscribe(async ({ key }) => { if (key === "quickRestart") updateKeytips(); if (key === "showKeyTips") {