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 @@
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/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";
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 {
diff --git a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts
index a6f45d1666ed..54d6d5ed3bc9 100644
--- a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts
+++ b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts
@@ -10,7 +10,7 @@ import {
COMPATIBILITY_CHECK,
COMPATIBILITY_CHECK_HEADER,
} from "@monkeytype/contracts";
-import * as Notifications from "../../elements/notifications";
+import { addBanner } from "../../stores/banners";
let bannerShownThisSession = false;
@@ -63,9 +63,12 @@ function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{
if (backendCheck !== COMPATIBILITY_CHECK) {
const message =
backendCheck > COMPATIBILITY_CHECK
- ? `Looks like the client and server versions are mismatched (backend is newer). Please
refresh the page.`
+ ? `Looks like the client and server versions are mismatched (backend is newer). Please refresh the page.`
: `Looks like our monkeys didn't deploy the new server version correctly. If this message persists contact support.`;
- Notifications.addPSA(message, 1, undefined, false, undefined, true);
+ addBanner({
+ level: "error",
+ text: message,
+ });
bannerShownThisSession = true;
}
}
diff --git a/frontend/src/ts/auth.ts b/frontend/src/ts/auth.tsx
similarity index 95%
rename from frontend/src/ts/auth.ts
rename to frontend/src/ts/auth.tsx
index 2d119fbcb039..f271aad7095f 100644
--- a/frontend/src/ts/auth.ts
+++ b/frontend/src/ts/auth.tsx
@@ -1,11 +1,4 @@
-import Ape from "./ape";
-import * as Notifications from "./elements/notifications";
-import Config, { applyConfig, saveFullConfigToLocalStorage } from "./config";
-import * as Misc from "./utils/misc";
-import * as DB from "./db";
-import { showLoaderBar, hideLoaderBar } from "./signals/loader-bar";
-import * as LoginPage from "./pages/login";
-import * as RegisterCaptchaModal from "./modals/register-captcha";
+import { tryCatch } from "@monkeytype/util/trycatch";
import {
GoogleAuthProvider,
GithubAuthProvider,
@@ -14,6 +7,12 @@ import {
User as UserType,
AuthProvider,
} from "firebase/auth";
+
+import Ape from "./ape";
+import Config, { applyConfig, saveFullConfigToLocalStorage } from "./config";
+import { navigate } from "./controllers/route-controller";
+import * as DB from "./db";
+import * as Notifications from "./elements/notifications";
import {
isAuthAvailable,
getAuthenticatedUser,
@@ -24,13 +23,17 @@ import {
signInWithPopup,
resetIgnoreAuthCallback,
} from "./firebase";
+import * as RegisterCaptchaModal from "./modals/register-captcha";
+import { showPopup } from "./modals/simple-modals-base";
+import * as AuthEvent from "./observables/auth-event";
+import * as LoginPage from "./pages/login";
+import * as Sentry from "./sentry";
+import { showLoaderBar, hideLoaderBar } from "./signals/loader-bar";
import * as ConnectionState from "./states/connection";
-import { navigate } from "./controllers/route-controller";
+import { addBanner } from "./stores/banners";
import { getActiveFunboxesWithFunction } from "./test/funbox/list";
-import * as Sentry from "./sentry";
-import { tryCatch } from "@monkeytype/util/trycatch";
-import * as AuthEvent from "./observables/auth-event";
import { qs, qsa } from "./utils/dom";
+import * as Misc from "./utils/misc";
export const gmailProvider = new GoogleAuthProvider();
export const githubProvider = new GithubAuthProvider();
@@ -69,14 +72,26 @@ async function getDataAndInit(): Promise
{
void Sentry.setUser(snapshot.uid, snapshot.name);
if (snapshot.needsToChangeName) {
- Notifications.addPSA(
- "You need to update your account name. Click here to change it and learn more about why.",
- -1,
- undefined,
- true,
- undefined,
- true,
- );
+ addBanner({
+ level: "error",
+ icon: "fas fa-exclamation-triangle",
+ customContent: (
+ <>
+ You need to update your account name.{" "}
+ {" "}
+ to change it and learn more about why.
+ >
+ ),
+ important: true,
+ });
}
const areConfigsEqual =
diff --git a/frontend/src/ts/components/layout/overlays/Banners.tsx b/frontend/src/ts/components/layout/overlays/Banners.tsx
new file mode 100644
index 000000000000..6ec5f545287b
--- /dev/null
+++ b/frontend/src/ts/components/layout/overlays/Banners.tsx
@@ -0,0 +1,108 @@
+import {
+ createEffect,
+ For,
+ JSXElement,
+ on,
+ onCleanup,
+ onMount,
+} from "solid-js";
+import { debounce } from "throttle-debounce";
+
+import { useRefWithUtils } from "../../../hooks/useRefWithUtils";
+import { setGlobalOffsetTop } from "../../../signals/core";
+import {
+ Banner as BannerType,
+ getBanners,
+ removeBanner,
+} from "../../../stores/banners";
+import { cn } from "../../../utils/cn";
+import { Conditional } from "../../common/Conditional";
+
+function Banner(props: BannerType): JSXElement {
+ const remove = (): void => {
+ // document.startViewTransition(() => {
+ removeBanner(props.id);
+ // });
+ };
+ const icon = (): string =>
+ props.icon === undefined || props.icon === ""
+ ? "fa fa-fw fa-bullhorn"
+ : props.icon;
+
+ return (
+
+
+
+
+
+ >
+ }
+ else={}
+ />
+ {props.customContent}}
+ else={
{props.text}
}
+ />
+
}
+ else={
+
+ }
+ />
+
+
+ );
+}
+
+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(`
`);
- 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") {